rename service to action

This commit is contained in:
Joseph Abbey
2025-10-20 09:24:12 +01:00
parent 2fca0ef3a3
commit cac94fecd4
17 changed files with 249 additions and 198 deletions

View File

@@ -81,7 +81,7 @@ Example schema:
"name": "Food is Ready!", "name": "Food is Ready!",
"type": "tap", "type": "tap",
"tap_action": { "tap_action": {
"service": "script.turn_on", "action": "script.turn_on",
"confirm": true "confirm": true
} }
}, },
@@ -132,7 +132,7 @@ Example schema:
"name": "Turn off USBs", "name": "Turn off USBs",
"type": "tap", "type": "tap",
"tap_action": { "tap_action": {
"service": "automation.trigger" "action": "automation.trigger"
} }
}, },
{ {
@@ -140,7 +140,7 @@ Example schema:
"name": "TV Lights Scene", "name": "TV Lights Scene",
"type": "tap", "type": "tap",
"tap_action": { "tap_action": {
"service": "scene.turn_on", "action": "scene.turn_on",
"pin": true "pin": true
} }
}, },
@@ -150,13 +150,13 @@ Example schema:
"type": "numeric", "type": "numeric",
"entity": "climate.room", "entity": "climate.room",
"tap_action": { "tap_action": {
"service": "climate.set_temperature", "action": "climate.set_temperature",
"picker": { "picker": {
"step": 0.5, "step": 0.5,
"start": 10, "start": 10,
"stop": 30, "stop": 30,
"attribute": "temperature", "attribute": "temperature",
"data_attribute": "temperature" "data_attribute": "temperature"
} }
} }
} }
@@ -171,7 +171,7 @@ The example above illustrates how to configure:
* Lights or switches (`toggle`), <img src="images/toggle_icon.png" height="20"> * Lights or switches (`toggle`), <img src="images/toggle_icon.png" height="20">
* Enables for automations (`toggle`), <img src="images/toggle_icon.png" height="20"> * Enables for automations (`toggle`), <img src="images/toggle_icon.png" height="20">
* Script invocation (`tap`) * Script invocation (`tap`)
* Service invocation, e.g. Scene setting, (`tap`) * Action invocation, e.g. Scene setting, (`tap`)
* A sub-menu to open (`group`) * A sub-menu to open (`group`)
* A numeric item (`numeric`), which allows you to set a numeric value e.g. for heating or a dimmer. This is [explained more fully](examples/Numeric.md) in its own examples page. * A numeric item (`numeric`), which allows you to set a numeric value e.g. for heating or a dimmer. This is [explained more fully](examples/Numeric.md) in its own examples page.
* You can also display the status of devices (`info`) which is essentially a `tap` with no action * You can also display the status of devices (`info`) which is essentially a `tap` with no action
@@ -192,9 +192,9 @@ The following table indicates how HomeAssistant entity types can map to the Garm
| Thermostat | ❌ | ❌ | ✅ | ✅ | | Thermostat | ❌ | ❌ | ✅ | ✅ |
| Amplifier | ❌ | ❌ | ✅ | ✅ | | Amplifier | ❌ | ❌ | ✅ | ✅ |
| Any other entity | ❌ | ❌ | ✅ | ❌ | | Any other entity | ❌ | ❌ | ✅ | ❌ |
| Any service | ✅ | ❌ | ❌ | ❌ | | Any action | ✅ | ❌ | ❌ | ❌ |
Multiple templates are evaluated in a single HTTP request to update their status. Only the toggle items have the on/off <img src="images/toggle_icon.png" height="20"> icon. NB. All `tap` and `numeric` items must specify a `service` tag in the `tap_action` object (see example below). Multiple templates are evaluated in a single HTTP request to update their status. Only the toggle items have the on/off <img src="images/toggle_icon.png" height="20"> icon. NB. All `tap` and `numeric` items must specify a `action` tag in the `tap_action` object (see example below).
You can now specify alternative texts to use instead of "On" and "Off", e.g. "Locked" and "Unlocked" or "Open" and "Closed" through the use of a [template menu item](examples/Templates.md). But wouldn't having locks operated from your watch be a security concern ;-) ? You can now specify alternative texts to use instead of "On" and "Off", e.g. "Locked" and "Unlocked" or "Open" and "Closed" through the use of a [template menu item](examples/Templates.md). But wouldn't having locks operated from your watch be a security concern ;-) ?
@@ -202,7 +202,9 @@ The [schema](https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistan
### Old deprecated format ### Old deprecated format
Version 1.5 brought in a change to the JSON schema so the following old format remains useable but is no longer favoured. The schema now marks it as 'deprecated' to nudge people over. Version 1.5 brought in a change to the JSON schema so the following old format remains useable but is no longer favoured.
> [!IMPORTANT] Deprecated:
```json ```json
{ {
@@ -213,7 +215,9 @@ Version 1.5 brought in a change to the JSON schema so the following old format r
} }
``` ```
The above should be replaced by the following: Version 3.6 brought another change to the JSON schema to follow HomeAssistant's renaming of `service` to `action`.
> [!IMPORTANT] Deprecated:
```json ```json
{ {
@@ -226,7 +230,20 @@ The above should be replaced by the following:
} }
``` ```
This allows the `confirm` and `pin` fields to be accommodated in the `tap_action` along side the `service` tag, and follows the HomeAssistant YAML format more closely. The above should be replaced by the following:
```json
{
"entity": "scene.tv_light",
"name": "TV Lights Scene",
"type": "tap",
"tap_action": {
"action": "scene.turn_on"
}
}
```
This allows the `confirm` and `pin` fields to be accommodated in the `tap_action` along side the `action` tag, and follows the HomeAssistant YAML format more closely.
### More Examples ### More Examples
@@ -237,7 +254,7 @@ This allows the `confirm` and `pin` fields to be accommodated in the `tap_action
## Editing the JSON file ## Editing the JSON file
You have options. The first is what we use. You have options. The first is what we use.
1. **Best!** Use the GarminHomeAssistant [Web-based Editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) which includes `entity` and `service` name completion and validation by fetching data from your own HomeAssistant instance. _Pretty nifty eh?_ The other method listed below do not add this convenience and checking. 1. **Best!** Use the GarminHomeAssistant [Web-based Editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) which includes `entity` and `action` name completion and validation by fetching data from your own HomeAssistant instance. _Pretty nifty eh?_ The other method listed below do not add this convenience and checking.
2. Use the [Studio Code Server](https://community.home-assistant.io/t/home-assistant-community-add-on-visual-studio-code/107863) addon for HomeAssistant. You can then edit your JSON file in place. 2. Use the [Studio Code Server](https://community.home-assistant.io/t/home-assistant-community-add-on-visual-studio-code/107863) addon for HomeAssistant. You can then edit your JSON file in place.
3. Locally installed VSCode, or if not installed, try 3. Locally installed VSCode, or if not installed, try
4. The on-line version at https://vscode.dev/, which works really well. 4. The on-line version at https://vscode.dev/, which works really well.

View File

@@ -112,20 +112,7 @@
"description": "Use 'info' or 'tap' instead." "description": "Use 'info' or 'tap' instead."
}, },
"tap_action": { "tap_action": {
"$ref": "#/$defs/tap_action", "$ref": "#/$defs/tap_action_tap"
"properties": {
"service": {
"$ref": "#/$defs/service"
},
"data": {
"type": "object",
"title": "Your services's parameters",
"description": "The object containing the parameters and their values to be passed to the entity. No schema checking can be done here, you are on your own! On application crash, remove the parameters."
}
},
"required": [
"service"
]
}, },
"enabled": { "enabled": {
"$ref": "#/$defs/enabled" "$ref": "#/$defs/enabled"
@@ -185,26 +172,13 @@
"$ref": "#/$defs/content" "$ref": "#/$defs/content"
}, },
"service": { "service": {
"$ref": "#/$defs/service", "$ref": "#/$defs/action",
"deprecated": true, "deprecated": true,
"title": "Schema change:", "title": "Schema change:",
"description": "Use 'tap_action' instead to mirror Home Assistant." "description": "Use 'tap_action' instead to mirror Home Assistant."
}, },
"tap_action": { "tap_action": {
"$ref": "#/$defs/tap_action", "$ref": "#/$defs/tap_action_tap"
"properties": {
"service": {
"$ref": "#/$defs/service"
},
"data": {
"type": "object",
"title": "Your services's parameters",
"description": "The object containing the parameters and their values to be passed to the entity. No schema checking can be done here, you are on your own! On application crash, remove the parameters."
}
},
"required": [
"service"
]
}, },
"enabled": { "enabled": {
"$ref": "#/$defs/enabled" "$ref": "#/$defs/enabled"
@@ -277,8 +251,8 @@
"tap_action": { "tap_action": {
"$ref": "#/$defs/tap_action", "$ref": "#/$defs/tap_action",
"properties": { "properties": {
"service": { "action": {
"$ref": "#/$defs/service" "$ref": "#/$defs/action"
}, },
"picker": { "picker": {
"type": "object", "type": "object",
@@ -304,8 +278,8 @@
}, },
"data_attribute": { "data_attribute": {
"type": "string", "type": "string",
"title": "Attribute on the service data", "title": "Attribute on the action data",
"description": "Attribute on the service data for the value to set." "description": "Attribute on the action data for the value to set."
} }
}, },
"required": [ "required": [
@@ -317,7 +291,7 @@
} }
}, },
"required": [ "required": [
"service", "action",
"picker" "picker"
] ]
}, },
@@ -356,7 +330,7 @@
"properties": { "properties": {
"tap_action": { "tap_action": {
"properties": { "properties": {
"service": { "action": {
"const": "light.turn_on" "const": "light.turn_on"
}, },
"picker": { "picker": {
@@ -392,7 +366,7 @@
"properties": { "properties": {
"tap_action": { "tap_action": {
"properties": { "properties": {
"service": { "action": {
"const": "input_number.set_value" "const": "input_number.set_value"
}, },
"picker": { "picker": {
@@ -423,7 +397,7 @@
"properties": { "properties": {
"tap_action": { "tap_action": {
"properties": { "properties": {
"service": { "action": {
"const": "number.set_value" "const": "number.set_value"
}, },
"picker": { "picker": {
@@ -454,7 +428,7 @@
"properties": { "properties": {
"tap_action": { "tap_action": {
"properties": { "properties": {
"service": { "action": {
"const": "fan.set_percentage" "const": "fan.set_percentage"
}, },
"picker": { "picker": {
@@ -489,7 +463,7 @@
"properties": { "properties": {
"tap_action": { "tap_action": {
"properties": { "properties": {
"service": { "action": {
"const": "valve.set_valve_position" "const": "valve.set_valve_position"
}, },
"picker": { "picker": {
@@ -526,7 +500,7 @@
"properties": { "properties": {
"tap_action": { "tap_action": {
"properties": { "properties": {
"service": { "action": {
"enum": [ "enum": [
"cover.set_position", "cover.set_position",
"cover.set_tilt_position" "cover.set_tilt_position"
@@ -541,7 +515,7 @@
"properties": { "properties": {
"tap_action": { "tap_action": {
"properties": { "properties": {
"service": { "action": {
"const": "cover.set_tilt_position" "const": "cover.set_tilt_position"
} }
} }
@@ -552,7 +526,7 @@
"properties": { "properties": {
"tap_action": { "tap_action": {
"properties": { "properties": {
"service": { "action": {
"const": "cover.set_tilt_position" "const": "cover.set_tilt_position"
}, },
"picker": { "picker": {
@@ -579,7 +553,7 @@
"properties": { "properties": {
"tap_action": { "tap_action": {
"properties": { "properties": {
"service": { "action": {
"const": "cover.set_position" "const": "cover.set_position"
}, },
"picker": { "picker": {
@@ -617,7 +591,7 @@
"properties": { "properties": {
"tap_action": { "tap_action": {
"properties": { "properties": {
"service": { "action": {
"const": "media_player.volume_set" "const": "media_player.volume_set"
}, },
"picker": { "picker": {
@@ -652,7 +626,7 @@
"tap_action": { "tap_action": {
"properties": { "properties": {
"properties": { "properties": {
"service": { "action": {
"const": "climate.set_temperature" "const": "climate.set_temperature"
}, },
"picker": { "picker": {
@@ -686,7 +660,7 @@
}, },
"type": { "type": {
"title": "Menu item type", "title": "Menu item type",
"description": "One of 'info', 'tap', 'toggle' or 'group'." "description": "One of 'info', 'tap', 'toggle', 'group' or 'numeric'."
}, },
"items": { "items": {
"type": "array", "type": "array",
@@ -696,7 +670,7 @@
"type": "tap", "type": "tap",
"name": "Example", "name": "Example",
"tap_action": { "tap_action": {
"service": "notify.notify", "action": "notify.notify",
"data": { "data": {
"message": "Example" "message": "Example"
} }
@@ -718,7 +692,7 @@
"name": "Example", "name": "Example",
"entity": "light.example", "entity": "light.example",
"tap_action": { "tap_action": {
"service": "light.turn_on", "action": "light.turn_on",
"picker": { "picker": {
"attribute": "brightness", "attribute": "brightness",
"data_attribute": "brightness", "data_attribute": "brightness",
@@ -733,7 +707,7 @@
"name": "Example", "name": "Example",
"entity": "input_number.example", "entity": "input_number.example",
"tap_action": { "tap_action": {
"service": "input_number.set_value", "action": "input_number.set_value",
"picker": { "picker": {
"data_attribute": "value", "data_attribute": "value",
"min": 0, "min": 0,
@@ -747,7 +721,7 @@
"name": "Example", "name": "Example",
"entity": "number.example", "entity": "number.example",
"tap_action": { "tap_action": {
"service": "number.set_value", "action": "number.set_value",
"picker": { "picker": {
"data_attribute": "value", "data_attribute": "value",
"min": 0, "min": 0,
@@ -761,14 +735,13 @@
"name": "Example", "name": "Example",
"entity": "fan.example", "entity": "fan.example",
"tap_action": { "tap_action": {
"service": "fan.set_percentage", "action": "fan.set_percentage",
"picker": { "picker": {
"attribute": "percentage", "attribute": "percentage",
"data_attribute": "percentage", "data_attribute": "percentage",
"min": 0, "min": 0,
"max": 100, "max": 100,
"step": 1, "step": 1
"display_format": "%.0f%%"
} }
} }
}, },
@@ -777,7 +750,7 @@
"name": "Example", "name": "Example",
"entity": "valve.example", "entity": "valve.example",
"tap_action": { "tap_action": {
"service": "valve.set_valve_position", "action": "valve.set_valve_position",
"picker": { "picker": {
"attribute": "position", "attribute": "position",
"data_attribute": "position", "data_attribute": "position",
@@ -792,7 +765,7 @@
"name": "Example", "name": "Example",
"entity": "cover.example", "entity": "cover.example",
"tap_action": { "tap_action": {
"service": "cover.set_position", "action": "cover.set_position",
"picker": { "picker": {
"attribute": "position", "attribute": "position",
"data_attribute": "position", "data_attribute": "position",
@@ -807,7 +780,7 @@
"name": "Example", "name": "Example",
"entity": "cover.example", "entity": "cover.example",
"tap_action": { "tap_action": {
"service": "cover.set_tilt_position", "action": "cover.set_tilt_position",
"picker": { "picker": {
"attribute": "tilt_position", "attribute": "tilt_position",
"data_attribute": "tilt_position", "data_attribute": "tilt_position",
@@ -822,7 +795,7 @@
"name": "Example", "name": "Example",
"entity": "media_player.example", "entity": "media_player.example",
"tap_action": { "tap_action": {
"service": "media_player.volume_set", "action": "media_player.volume_set",
"picker": { "picker": {
"attribute": "volume_level", "attribute": "volume_level",
"data_attribute": "volume_level", "data_attribute": "volume_level",
@@ -837,7 +810,7 @@
"name": "Example", "name": "Example",
"entity": "climate.example", "entity": "climate.example",
"tap_action": { "tap_action": {
"service": "climate.set_temperature", "action": "climate.set_temperature",
"picker": { "picker": {
"attribute": "temperature", "attribute": "temperature",
"data_attribute": "temperature" "data_attribute": "temperature"
@@ -944,9 +917,9 @@
"title": "Home Assistant entity name", "title": "Home Assistant entity name",
"pattern": "^[^.]+\\.[^.]+$" "pattern": "^[^.]+\\.[^.]+$"
}, },
"service": { "action": {
"type": "string", "type": "string",
"title": "Home Assistant service name", "title": "Home Assistant action name",
"pattern": "^[^.]+\\.[^.]+$" "pattern": "^[^.]+\\.[^.]+$"
}, },
"tap_action": { "tap_action": {
@@ -962,13 +935,49 @@
} }
} }
}, },
"tap_action_tap": {
"$ref": "#/$defs/tap_action",
"properties": {
"service": {
"$ref": "#/$defs/action",
"deprecated": true
},
"action": {
"$ref": "#/$defs/action"
},
"data": {
"type": "object",
"title": "Your actions's parameters",
"description": "The object containing the parameters and their values to be passed to the entity. No schema checking can be done here, you are on your own! On application crash, remove the parameters."
},
"anyOf": [
{
"required": [
"service"
]
},
{
"required": [
"action"
]
}
]
}
},
"content": { "content": {
"title": "Home Assistant Template", "title": "Home Assistant Template",
"description": "Jinja2 template defining the text to display. Must be included in an 'info'. Optional in a 'toggle', 'tap' and 'group'. Special characters may not render in the glance context.", "description": "Jinja2 template defining the text to display. Must be included in an 'info'. Optional in a 'toggle', 'tap' and 'group'. Special characters may not render in the glance context.",
"type": "string" "type": "string"
}, },
"confirm": { "confirm": {
"type": "boolean", "oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
],
"default": false, "default": false,
"title": "Confirmation", "title": "Confirmation",
"description": "Optional confirmation of the action before execution as a precaution." "description": "Optional confirmation of the action before execution as a precaution."

View File

@@ -11,7 +11,7 @@ A simple example using a scene as a `tap` menu item.
"name": "Telly Scene", "name": "Telly Scene",
"type": "tap", "type": "tap",
"tap_action": { "tap_action": {
"service": "scene.turn_on" "action": "scene.turn_on"
} }
}, },
``` ```
@@ -62,7 +62,7 @@ Note that for notify events, you _must_ not supply an `entity_id` or the API cal
"name": "Message", "name": "Message",
"type": "tap", "type": "tap",
"tap_action": { "tap_action": {
"service": "notify.mobile_app_on_phone", "action": "notify.mobile_app_on_phone",
"data": { "data": {
"title": "This is a title", "title": "This is a title",
"message": "This is the message" "message": "This is the message"
@@ -73,9 +73,9 @@ Note that for notify events, you _must_ not supply an `entity_id` or the API cal
``` ```
> [!IMPORTANT] > [!IMPORTANT]
> Be careful with the value of the `service` field. > Be careful with the value of the `action` field.
Note that the `service` field will need to be a locally custom `script.<something>` as soon as any `data` fields are populated and not something more generic like `script.turn_on`. If the `service` field is wrong, the application will fail with a [`Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE`](https://developer.garmin.com/connect-iq/api-docs/Toybox/Communications.html) error in the response from your HomeAssistant and show the error message as _"No JSON returned from HTTP request"_ on your device. In the [web-based editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) you can use the standard developer tools to observe an `HTTP 400` error which the application does not see. Here we are limited by the [Garmin Connect IQ](https://developer.garmin.com/connect-iq/overview/) software development kit (SDK). We do not have enough information at the point of execution in the application to determine the cause of the error. Nor is there an immediately obvious way of identifying this issue using the JSON schema checks. Note that the `action` field will need to be a locally custom `script.<something>` as soon as any `data` fields are populated and not something more generic like `script.turn_on`. If the `action` field is wrong, the application will fail with a [`Communications.INVALID_HTTP_BODY_IN_NETWORK_RESPONSE`](https://developer.garmin.com/connect-iq/api-docs/Toybox/Communications.html) error in the response from your HomeAssistant and show the error message as _"No JSON returned from HTTP request"_ on your device. In the [web-based editor](https://house-of-abbey.github.io/GarminHomeAssistant/web/) you can use the standard developer tools to observe an `HTTP 400` error which the application does not see. Here we are limited by the [Garmin Connect IQ](https://developer.garmin.com/connect-iq/overview/) software development kit (SDK). We do not have enough information at the point of execution in the application to determine the cause of the error. Nor is there an immediately obvious way of identifying this issue using the JSON schema checks.
## Exit on Tap ## Exit on Tap
@@ -87,7 +87,7 @@ You can choose individual items that will quit after they have completed their a
"name": "Turn off Stuff", "name": "Turn off Stuff",
"type": "tap", "type": "tap",
"tap_action": { "tap_action": {
"service": "automation.trigger" "action": "automation.trigger"
}, },
"exit": true "exit": true
} }
@@ -103,7 +103,7 @@ If you would like to temporarily disable an item in your menu, e.g. for seasonal
"name": "Turn off Stuff", "name": "Turn off Stuff",
"type": "tap", "type": "tap",
"tap_action": { "tap_action": {
"service": "automation.trigger" "action": "automation.trigger"
}, },
"enabled": false "enabled": false
} }

View File

@@ -48,12 +48,12 @@ switch:
friendly_name: <name> friendly_name: <name>
value_template: <value> value_template: <value>
turn_on: turn_on:
service: <service> action: <action>
data: data:
entity_id: <entity> entity_id: <entity>
<attribute>: <value> <attribute>: <value>
turn_off: turn_off:
service: <service> action: <action>
data: data:
entity_id: <entity> entity_id: <entity>
<attribute>: <value> <attribute>: <value>
@@ -90,11 +90,11 @@ switch:
friendly_name: Cover friendly_name: Cover
value_template: "{{ is_state('cover.cover', 'open') }}" value_template: "{{ is_state('cover.cover', 'open') }}"
turn_on: turn_on:
service: cover.open_cover action: cover.open_cover
data: data:
entity_id: cover.cover entity_id: cover.cover
turn_off: turn_off:
service: cover.close_cover action: cover.close_cover
data: data:
entity_id: cover.cover entity_id: cover.cover
``` ```

View File

@@ -116,7 +116,7 @@ Note: Only when you use the `tap_action` field do you also need to include the `
"type": "tap", "type": "tap",
"content": "{% if is_state('binary_sensor.garage_connected', 'on') %}{{state_translated('cover.garage_door')}} - {{state_attr('cover.garage_door', 'current_position')}}%{%else%}Unconnected{% endif %}", "content": "{% if is_state('binary_sensor.garage_connected', 'on') %}{{state_translated('cover.garage_door')}} - {{state_attr('cover.garage_door', 'current_position')}}%{%else%}Unconnected{% endif %}",
"tap_action": { "tap_action": {
"service": "cover.toggle", "action": "cover.toggle",
"pin": true "pin": true
} }
} }
@@ -173,7 +173,7 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
"type": "tap", "type": "tap",
"content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0)) | int }}%{% else %}Off{% endif %}", "content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0)) | int }}%{% else %}Off{% endif %}",
"tap_action": { "tap_action": {
"service": "light.turn_on", "action": "light.turn_on",
"data": { "data": {
"brightness_pct": 12 "brightness_pct": 12
} }
@@ -184,7 +184,7 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
"name": "LEDs 1", "name": "LEDs 1",
"type": "tap", "type": "tap",
"tap_action": { "tap_action": {
"service": "light.turn_on", "action": "light.turn_on",
"data": { "data": {
"brightness_pct": 37 "brightness_pct": 37
} }
@@ -196,7 +196,7 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
"type": "tap", "type": "tap",
"content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0)) | int }}%{% else %}Off{% endif %}", "content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0)) | int }}%{% else %}Off{% endif %}",
"tap_action": { "tap_action": {
"service": "light.turn_on", "action": "light.turn_on",
"data": { "data": {
"brightness_pct": 62 "brightness_pct": 62
} }
@@ -208,7 +208,7 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
"type": "tap", "type": "tap",
"content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0))| int }}%{% else %}Off{% endif %}", "content": "{% if not (is_state('light.green_house', 'off') or is_state('light.green_house', 'unavailable')) %}{{ (((state_attr('light.green_house', 'brightness') | float) / 255 * 100) | round(0))| int }}%{% else %}Off{% endif %}",
"tap_action": { "tap_action": {
"service": "light.turn_on", "action": "light.turn_on",
"data": { "data": {
"brightness_pct": 87 "brightness_pct": 87
} }

View File

@@ -97,7 +97,7 @@ class HomeAssistantMenuItemFactory {
//! @param label Menu item label. //! @param label Menu item label.
//! @param entity_id Home Assistant Entity ID (optional) //! @param entity_id Home Assistant Entity ID (optional)
//! @param template Template for Home Assistant to render (optional) //! @param template Template for Home Assistant to render (optional)
//! @param service Template for Home Assistant to render (optional) //! @param action Action to run on Home Assistant (optional)
//! @param data Sourced from the menu JSON, this is the `data` field from the `tap_action` field. //! @param data Sourced from the menu JSON, this is the `data` field from the `tap_action` field.
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin. //! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
// //
@@ -105,7 +105,7 @@ class HomeAssistantMenuItemFactory {
label as Lang.String or Lang.Symbol, label as Lang.String or Lang.Symbol,
entity_id as Lang.String?, entity_id as Lang.String?,
template as Lang.String?, template as Lang.String?,
service as Lang.String?, action as Lang.String?,
data as Lang.Dictionary?, data as Lang.Dictionary?,
options as { options as {
:exit as Lang.Boolean, :exit as Lang.Boolean,
@@ -124,12 +124,12 @@ class HomeAssistantMenuItemFactory {
for (var i = 0; i < keys.size(); i++) { for (var i = 0; i < keys.size(); i++) {
options[keys[i]] = mMenuItemOptions.get(keys[i]); options[keys[i]] = mMenuItemOptions.get(keys[i]);
} }
if (service != null) { if (action != null) {
options[:icon] = mTapTypeIcon; options.put(:icon, mTapTypeIcon);
return new HomeAssistantTapMenuItem( return new HomeAssistantTapMenuItem(
label, label,
template, template,
service, action,
data, data,
options, options,
mHomeAssistantService mHomeAssistantService
@@ -155,8 +155,8 @@ class HomeAssistantMenuItemFactory {
label as Lang.String or Lang.Symbol, label as Lang.String or Lang.Symbol,
entity_id as Lang.String?, entity_id as Lang.String?,
template as Lang.String?, template as Lang.String?,
service as Lang.String?, action as Lang.String?,
picker as Lang.Dictionary, data as Lang.Dictionary?,
options as { options as {
:exit as Lang.Boolean, :exit as Lang.Boolean,
:confirm as Lang.Boolean, :confirm as Lang.Boolean,
@@ -176,7 +176,7 @@ class HomeAssistantMenuItemFactory {
return new HomeAssistantNumericMenuItem( return new HomeAssistantNumericMenuItem(
label, label,
template, template,
service, action,
data, data,
picker, picker,
options, options,

View File

@@ -22,7 +22,7 @@ using Toybox.Graphics;
// //
class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem { class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
private var mHomeAssistantService as HomeAssistantService?; private var mHomeAssistantService as HomeAssistantService?;
private var mService as Lang.String?; private var mAction as Lang.String?;
private var mConfirm as Lang.Boolean; private var mConfirm as Lang.Boolean;
private var mExit as Lang.Boolean; private var mExit as Lang.Boolean;
private var mPin as Lang.Boolean; private var mPin as Lang.Boolean;
@@ -35,11 +35,11 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
//! //!
//! @param label Menu item label. //! @param label Menu item label.
//! @param template Menu item template. //! @param template Menu item template.
//! @param service Menu item service. //! @param action Menu item action.
//! @param data Data to supply to the service call. //! @param data Data to supply to the action call.
//! @param exit Should the service call complete and then exit? //! @param exit Should the action call complete and then exit?
//! @param confirm Should the service call be confirmed to avoid accidental invocation? //! @param confirm Should the action call be confirmed to avoid accidental invocation?
//! @param pin Should the service call be protected with a PIN for some low level of security? //! @param pin Should the action call be protected with a PIN for some low level of security?
//! @param icon Icon to use for the menu item. //! @param icon Icon to use for the menu item.
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin. //! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
//! @param haService Shared Home Assistant service object that will perform the required call. Only //! @param haService Shared Home Assistant service object that will perform the required call. Only
@@ -48,7 +48,7 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
function initialize( function initialize(
label as Lang.String or Lang.Symbol, label as Lang.String or Lang.Symbol,
template as Lang.String, template as Lang.String,
service as Lang.String?, action as Lang.String?,
data as Lang.Dictionary?, data as Lang.Dictionary?,
picker as Lang.Dictionary, picker as Lang.Dictionary,
options as { options as {
@@ -60,7 +60,7 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
}?, }?,
haService as HomeAssistantService haService as HomeAssistantService
) { ) {
mService = service; mAction = action;
mData = data; mData = data;
mPicker = picker; mPicker = picker;
mExit = options[:exit]; mExit = options[:exit];
@@ -93,7 +93,8 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
} }
} }
function callService() as Void {
function callAction() as Void {
var hasTouchScreen = System.getDeviceSettings().isTouchScreen; var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
if (mPin && hasTouchScreen) { if (mPin && hasTouchScreen) {
var pin = Settings.getPin(); var pin = Settings.getPin();
@@ -119,8 +120,8 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
WatchUi.pushView( WatchUi.pushView(
dialog, dialog,
new WifiLteExecutionConfirmDelegate({ new WifiLteExecutionConfirmDelegate({
:type => "service", :type => "action",
:service => mService, :action => mAction,
:data => mData, :data => mData,
:exit => mExit, :exit => mExit,
}, dialog), }, dialog),
@@ -150,18 +151,18 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
function onConfirm(b as Lang.Boolean) as Void { function onConfirm(b as Lang.Boolean) as Void {
var dataAttribute = mPicker["data_attribute"]; var dataAttribute = mPicker["data_attribute"];
if (dataAttribute == null) { if (dataAttribute == null) {
//return without call service if no data attribute is set to avoid crash //return without call action if no data attribute is set to avoid crash
WatchUi.popView(WatchUi.SLIDE_RIGHT); WatchUi.popView(WatchUi.SLIDE_RIGHT);
return; return;
} }
var entity_id = mData["entity_id"]; var entity_id = mData["entity_id"];
if (entity_id == null) { if (entity_id == null) {
//return without call service if no entity_id is set to avoid crash //return without call action if no entity_id is set to avoid crash
WatchUi.popView(WatchUi.SLIDE_RIGHT); WatchUi.popView(WatchUi.SLIDE_RIGHT);
return; return;
} }
mHomeAssistantService.call( mHomeAssistantService.call(
mService, mAction,
{ {
"entity_id" => entity_id.toString(), "entity_id" => entity_id.toString(),
dataAttribute.toString() => mValue dataAttribute.toString() => mValue
@@ -213,7 +214,7 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
WatchUi.requestUpdate(); WatchUi.requestUpdate();
} }
//! Set the Picker's value. Needed to set new value via the Service call //! Set the Picker's value. Needed to set new value via the Action call
//! //!
//! @param value New value to set. //! @param value New value to set.
// //
@@ -223,7 +224,7 @@ class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
//! Get the Picker's value. //! Get the Picker's value.
//! //!
//! Needed to set new value via the Service call //! Needed to set new value via the Action call
// //
public function getValue() as Lang.Number or Lang.Float { public function getValue() as Lang.Number or Lang.Float {
return mValue; return mValue;

View File

@@ -61,7 +61,7 @@ class HomeAssistantNumericPicker extends WatchUi.Picker {
// //
public function onConfirm(value as Lang.Number or Lang.Float) as Void { public function onConfirm(value as Lang.Number or Lang.Float) as Void {
mItem.setValue(value); mItem.setValue(value);
mItem.callService(); mItem.callAction();
} }
} }

View File

@@ -34,7 +34,7 @@ class HomeAssistantService {
} }
} }
//! Callback function after completing the POST request to call a service. //! Callback function after completing the POST request to call an action.
//! //!
//! @param responseCode Response code. //! @param responseCode Response code.
//! @param data Response data. //! @param data Response data.
@@ -87,7 +87,7 @@ class HomeAssistantService {
break; break;
case 200: case 200:
// System.println("HomeAssistantService onReturnCall(): Service executed."); // System.println("HomeAssistantService onReturnCall(): Action executed.");
getApp().forceStatusUpdates(); getApp().forceStatusUpdates();
var d = data as Lang.Array; var d = data as Lang.Array;
var toast = WatchUi.loadResource($.Rez.Strings.Executed) as Lang.String; var toast = WatchUi.loadResource($.Rez.Strings.Executed) as Lang.String;
@@ -118,13 +118,13 @@ class HomeAssistantService {
} }
} }
//! Invoke a service call for a menu item. //! Invoke a action call for a menu item.
//! //!
//! @param service The Home Assistant service to be run, e.g. from the JSON `service` field. //! @param action The Home Assistant action to be run, e.g. from the JSON `action` field.
//! @param data Data to be supplied to the service call. //! @param data Data to be supplied to the action call.
// //
function call( function call(
service as Lang.String, action as Lang.String,
data as Lang.Dictionary?, data as Lang.Dictionary?,
exit as Lang.Boolean exit as Lang.Boolean
) as Void { ) as Void {
@@ -136,8 +136,8 @@ class HomeAssistantService {
WatchUi.pushView( WatchUi.pushView(
dialog, dialog,
new WifiLteExecutionConfirmDelegate({ new WifiLteExecutionConfirmDelegate({
:type => "service", :type => "action",
:service => service, :action => action,
:data => data, :data => data,
:exit => exit, :exit => exit,
}, dialog), }, dialog),
@@ -151,9 +151,9 @@ class HomeAssistantService {
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String); ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
} else { } else {
// Can't use null for substring() parameters due to API version level. // Can't use null for substring() parameters due to API version level.
var url = Settings.getApiUrl() + "/services/" + service.substring(0, service.find(".")) + "/" + service.substring(service.find(".")+1, service.length()); var url = Settings.getApiUrl() + "/services/" + action.substring(0, action.find(".")) + "/" + action.substring(action.find(".")+1, action.length());
// System.println("HomeAssistantService call() URL=" + url); // System.println("HomeAssistantService call() URL=" + url);
// System.println("HomeAssistantService call() service=" + service); // System.println("HomeAssistantService call() action=" + action);
var entity_id = ""; var entity_id = "";
if (data != null) { if (data != null) {

View File

@@ -50,9 +50,9 @@ class HomeAssistantSyncDelegate extends Communications.SyncDelegate {
var url; var url;
switch (type) { switch (type) {
case "service": case "action":
var service = WifiLteExecutionConfirmDelegate.mCommandData[:service]; var action = WifiLteExecutionConfirmDelegate.mCommandData[:action];
url = Settings.getApiUrl() + "/services/" + service.substring(0, service.find(".")) + "/" + service.substring(service.find(".")+1, service.length()); url = Settings.getApiUrl() + "/services/" + action.substring(0, action.find(".")) + "/" + action.substring(action.find(".")+1, action.length());
var entity_id = ""; var entity_id = "";
if (data != null) { if (data != null) {
entity_id = data.get("entity_id"); entity_id = data.get("entity_id");
@@ -78,7 +78,7 @@ class HomeAssistantSyncDelegate extends Communications.SyncDelegate {
private function performRequest(url as Lang.String, data as Lang.Dictionary?) { private function performRequest(url as Lang.String, data as Lang.Dictionary?) {
Communications.makeWebRequest( Communications.makeWebRequest(
url, url,
data, // May include {"entity_id": xxxx} for service calls data, // May include {"entity_id": xxxx} for action calls
{ {
:method => Communications.HTTP_REQUEST_METHOD_POST, :method => Communications.HTTP_REQUEST_METHOD_POST,
:headers => Settings.augmentHttpHeaders({ :headers => Settings.augmentHttpHeaders({

View File

@@ -17,11 +17,11 @@ using Toybox.Lang;
using Toybox.WatchUi; using Toybox.WatchUi;
using Toybox.Graphics; using Toybox.Graphics;
//! Menu button that triggers a service. //! Menu button that triggers an action.
// //
class HomeAssistantTapMenuItem extends HomeAssistantMenuItem { class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
private var mHomeAssistantService as HomeAssistantService; private var mHomeAssistantService as HomeAssistantService;
private var mService as Lang.String?; private var mAction as Lang.String?;
private var mConfirm as Lang.Boolean; private var mConfirm as Lang.Boolean;
private var mExit as Lang.Boolean; private var mExit as Lang.Boolean;
private var mPin as Lang.Boolean; private var mPin as Lang.Boolean;
@@ -31,11 +31,11 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
//! //!
//! @param label Menu item label. //! @param label Menu item label.
//! @param template Menu item template. //! @param template Menu item template.
//! @param service Menu item service. //! @param action Menu item action.
//! @param data Data to supply to the service call. //! @param data Data to supply to the action call.
//! @param exit Should the service call complete and then exit? //! @param exit Should the action call complete and then exit?
//! @param confirm Should the service call be confirmed to avoid accidental invocation? //! @param confirm Should the action call be confirmed to avoid accidental invocation?
//! @param pin Should the service call be protected with a PIN for some low level of security? //! @param pin Should the action call be protected with a PIN for some low level of security?
//! @param icon Icon to use for the menu item. //! @param icon Icon to use for the menu item.
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin. //! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
//! @param haService Shared Home Assistant service object that will perform the required call. Only //! @param haService Shared Home Assistant service object that will perform the required call. Only
@@ -44,7 +44,7 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
function initialize( function initialize(
label as Lang.String or Lang.Symbol, label as Lang.String or Lang.Symbol,
template as Lang.String, template as Lang.String,
service as Lang.String?, action as Lang.String?,
data as Lang.Dictionary?, data as Lang.Dictionary?,
options as { options as {
:alignment as WatchUi.MenuItem.Alignment, :alignment as WatchUi.MenuItem.Alignment,
@@ -65,16 +65,16 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
); );
mHomeAssistantService = haService; mHomeAssistantService = haService;
mService = service; mAction = action;
mData = data; mData = data;
mExit = options[:exit]; mExit = options[:exit];
mConfirm = options[:confirm]; mConfirm = options[:confirm];
mPin = options[:pin]; mPin = options[:pin];
} }
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry. //! Call a Home Assistant action only after checks have been done for confirmation or PIN entry.
// //
function callService() as Void { function callAction() as Void {
var hasTouchScreen = System.getDeviceSettings().isTouchScreen; var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
if (mPin && hasTouchScreen) { if (mPin && hasTouchScreen) {
var pin = Settings.getPin(); var pin = Settings.getPin();
@@ -100,10 +100,10 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
WatchUi.pushView( WatchUi.pushView(
dialog, dialog,
new WifiLteExecutionConfirmDelegate({ new WifiLteExecutionConfirmDelegate({
:type => "service", :type => "action",
:service => mService, :action => mAction,
:data => mData, :data => mData,
:exit => mExit, :exit => mExit,
}, dialog), }, dialog),
WatchUi.SLIDE_LEFT WatchUi.SLIDE_LEFT
); );
@@ -129,8 +129,8 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
//! @param b Ignored. It is included in order to match the expected function prototype of the callback method. //! @param b Ignored. It is included in order to match the expected function prototype of the callback method.
// //
public function onConfirm(b as Lang.Boolean) as Void { public function onConfirm(b as Lang.Boolean) as Void {
if (mService != null) { if (mAction != null) {
mHomeAssistantService.call(mService, mData, mExit); mHomeAssistantService.call(mAction, mData, mExit);
} }
} }

View File

@@ -33,7 +33,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
//! //!
//! @param label Menu item label. //! @param label Menu item label.
//! @param template Menu item template. //! @param template Menu item template.
//! @param data Data to supply to the service call. //! @param data Data to supply to the action call.
//! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin. //! @param options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
// //
function initialize( function initialize(
@@ -220,7 +220,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
} }
} }
//! Handles the response from a Home Assistant service or state call and updates the toggle UI. //! Handles the response from a Home Assistant action or state call and updates the toggle UI.
//! //!
//! @param data An array of dictionaries, each representing a Home Assistant entity state. //! @param data An array of dictionaries, each representing a Home Assistant entity state.
// //
@@ -293,9 +293,9 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
} }
} }
//! Call a Home Assistant service only after checks have been done for confirmation or PIN entry. //! Call a Home Assistant action only after checks have been done for confirmation or PIN entry.
// //
function callService(b as Lang.Boolean) as Void { function callAction(b as Lang.Boolean) as Void {
var hasTouchScreen = System.getDeviceSettings().isTouchScreen; var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
if (mPin && hasTouchScreen) { if (mPin && hasTouchScreen) {
// Undo the toggle // Undo the toggle
@@ -349,7 +349,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
setState(b); setState(b);
} }
//! Displays a confirmation dialog before executing a service call via Wi-Fi/LTE. //! Displays a confirmation dialog before executing an action call via Wi-Fi/LTE.
//! //!
//! @param s Desired state: `true` to turn on, `false` to turn off. //! @param s Desired state: `true` to turn on, `false` to turn off.
// //
@@ -377,7 +377,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
//! @param id The entity ID, e.g., `"switch.kitchen"`. //! @param id The entity ID, e.g., `"switch.kitchen"`.
//! @param s Desired state: `true` for "turn_on", `false` for "turn_off". //! @param s Desired state: `true` for "turn_on", `false` for "turn_off".
//! //!
//! @return Full service URL string. //! @return Full action URL string.
// //
private static function getUrl(id as Lang.String, s as Lang.Boolean) as Lang.String { private static function getUrl(id as Lang.String, s as Lang.Boolean) as Lang.String {
var url = Settings.getApiUrl() + "/services/"; var url = Settings.getApiUrl() + "/services/";

View File

@@ -47,7 +47,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
var content = items[i].get("content") as Lang.String?; var content = items[i].get("content") as Lang.String?;
var entity = items[i].get("entity") as Lang.String?; var entity = items[i].get("entity") as Lang.String?;
var tap_action = items[i].get("tap_action") as Lang.Dictionary?; var tap_action = items[i].get("tap_action") as Lang.Dictionary?;
var service = items[i].get("service") as Lang.String?; // Deprecated schema var action = items[i].get("service") as Lang.String?; // Deprecated schema
var confirm = false as Lang.Boolean?; var confirm = false as Lang.Boolean?;
var pin = false as Lang.Boolean?; var pin = false as Lang.Boolean?;
var data = null as Lang.Dictionary?; var data = null as Lang.Dictionary?;
@@ -60,7 +60,10 @@ class HomeAssistantView extends WatchUi.Menu2 {
exit = items[i].get("exit"); // Optional exit = items[i].get("exit"); // Optional
} }
if (tap_action != null) { if (tap_action != null) {
service = tap_action.get("service"); action = tap_action.get("service"); // Deprecated
if (tap_action.get("action") != null) {
action = tap_action.get("action"); // Optional
}
data = tap_action.get("data"); // Optional data = tap_action.get("data"); // Optional
if (tap_action.get("confirm") != null) { if (tap_action.get("confirm") != null) {
confirm = tap_action.get("confirm"); // Optional confirm = tap_action.get("confirm"); // Optional
@@ -81,12 +84,12 @@ class HomeAssistantView extends WatchUi.Menu2 {
:pin => pin :pin => pin
} }
)); ));
} else if (type.equals("tap") && service != null) { } else if (type.equals("tap") && action != null) {
addItem(HomeAssistantMenuItemFactory.create().tap( addItem(HomeAssistantMenuItemFactory.create().tap(
name, name,
entity, entity,
content, content,
service, action,
data, data,
{ {
:exit => exit, :exit => exit,
@@ -103,7 +106,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
name, name,
entity, entity,
content, content,
service, action,
data, data,
{ {
:exit => false, :exit => false,
@@ -117,7 +120,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
name, name,
entity, entity,
content, content,
service, action,
data, data,
{ {
:exit => exit, :exit => exit,
@@ -126,7 +129,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
} }
)); ));
} }
} else if (type.equals("numeric") && service != null) { } else if (type.equals("numeric") && action != null) {
if (tap_action != null) { if (tap_action != null) {
var picker = tap_action.get("picker") as Lang.Dictionary?; var picker = tap_action.get("picker") as Lang.Dictionary?;
if (picker != null) { if (picker != null) {
@@ -134,7 +137,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
name, name,
entity, entity,
content, content,
service, action,
picker, picker,
{ {
:exit => exit, :exit => exit,
@@ -150,7 +153,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
name, name,
entity, entity,
content, content,
service, action,
data, data,
{ {
:exit => false, :exit => false,
@@ -267,11 +270,11 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
if (item instanceof HomeAssistantToggleMenuItem) { if (item instanceof HomeAssistantToggleMenuItem) {
var haToggleItem = item as HomeAssistantToggleMenuItem; var haToggleItem = item as HomeAssistantToggleMenuItem;
// System.println(haToggleItem.getLabel() + " " + haToggleItem.getId() + " " + haToggleItem.isEnabled()); // System.println(haToggleItem.getLabel() + " " + haToggleItem.getId() + " " + haToggleItem.isEnabled());
haToggleItem.callService(haToggleItem.isEnabled()); haToggleItem.callAction(haToggleItem.isEnabled());
} else if (item instanceof HomeAssistantTapMenuItem) { } else if (item instanceof HomeAssistantTapMenuItem) {
var haItem = item as HomeAssistantTapMenuItem; var haItem = item as HomeAssistantTapMenuItem;
// System.println(haItem.getLabel() + " " + haItem.getId()); // System.println(haItem.getLabel() + " " + haItem.getId());
haItem.callService(); haItem.callAction();
} else if (item instanceof HomeAssistantNumericMenuItem) { } else if (item instanceof HomeAssistantNumericMenuItem) {
var haItem = item as HomeAssistantNumericMenuItem; var haItem = item as HomeAssistantNumericMenuItem;
// System.println(haItem.getLabel() + " " + haItem.getId()); // System.println(haItem.getLabel() + " " + haItem.getId());

View File

@@ -25,7 +25,7 @@ using Toybox.Timer;
class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate { class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
public static var mCommandData as { public static var mCommandData as {
:type as Lang.String, :type as Lang.String,
:service as Lang.String?, :action as Lang.String?,
:data as Lang.Dictionary?, :data as Lang.Dictionary?,
:url as Lang.String?, :url as Lang.String?,
:id as Lang.Number?, :id as Lang.Number?,
@@ -40,8 +40,8 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
//! //!
//! @param options A dictionary describing the command to be executed:<br> //! @param options A dictionary describing the command to be executed:<br>
//! `{`<br> //! `{`<br>
//! &emsp; `:type: as Lang.String,` // The command type, either `"service"` or `"entity"`.<br> //! &emsp; `:type: as Lang.String,` // The command type, either `"action"` or `"entity"`.<br>
//! &emsp; `:service: as Lang.String?,` // (For type `"service"`) The Home Assistant service to call (e.g., "light.turn_on").<br> //! &emsp; `:action: as Lang.String?,` // (For type `"action"`) The Home Assistant action to call (e.g., "light.turn_on").<br>
//! &emsp; `:url: as Lang.Dictionary?,` // (For type `"entity"`) The full Home Assistant entity API URL.<br> //! &emsp; `:url: as Lang.Dictionary?,` // (For type `"entity"`) The full Home Assistant entity API URL.<br>
//! &emsp; `:callback: as Lang.String?,` // (For type `"entity"`) A callback method (Method<data as Dictionary>) to handle the response.<br> //! &emsp; `:callback: as Lang.String?,` // (For type `"entity"`) A callback method (Method<data as Dictionary>) to handle the response.<br>
//! &emsp; `:data: as Lang.Method?,` // (Optional) A dictionary of data to send with the request.<br> //! &emsp; `:data: as Lang.Method?,` // (Optional) A dictionary of data to send with the request.<br>
@@ -52,7 +52,7 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
function initialize( function initialize(
cOptions as { cOptions as {
:type as Lang.String, :type as Lang.String,
:service as Lang.String?, :action as Lang.String?,
:data as Lang.Dictionary?, :data as Lang.Dictionary?,
:url as Lang.String?, :url as Lang.String?,
:callback as Lang.Method?, :callback as Lang.Method?,
@@ -73,7 +73,7 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
mConfirmationView = view; mConfirmationView = view;
mCommandData = { mCommandData = {
:type => cOptions[:type], :type => cOptions[:type],
:service => cOptions[:service], :action => cOptions[:action],
:data => cOptions[:data], :data => cOptions[:data],
:url => cOptions[:url], :url => cOptions[:url],
:callback => cOptions[:callback], :callback => cOptions[:callback],

View File

@@ -1,3 +1,4 @@
<!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
@@ -7,7 +8,7 @@
<link <link
rel="stylesheet" rel="stylesheet"
data-name="vs/editor/editor.main" data-name="vs/editor/editor.main"
href="https://www.unpkg.com/monaco-editor@0.52.2/min/vs/editor/editor.main.css" /> href="https://www.unpkg.com/monaco-editor@0.54.0/min/vs/editor/editor.main.css" />
<link <link
rel="stylesheet" rel="stylesheet"
type="text/css" type="text/css"
@@ -453,7 +454,7 @@ http:
</div> </div>
</dialog> </dialog>
<script src="https://www.unpkg.com/monaco-editor@0.52.2/min/vs/loader.js"></script> <script src="https://www.unpkg.com/monaco-editor@0.54.0/min/vs/loader.js"></script>
<script src="https://www.unpkg.com/json-ast-comments@1.1.1/lib/json.js"></script> <script src="https://www.unpkg.com/json-ast-comments@1.1.1/lib/json.js"></script>
<script src="https://www.unpkg.com/toastify-js@1.12.0/src/toastify.js"></script> <script src="https://www.unpkg.com/toastify-js@1.12.0/src/toastify.js"></script>
<script src="https://code.iconify.design/1/1.0.6/iconify.min.js"></script> <script src="https://code.iconify.design/1/1.0.6/iconify.min.js"></script>

View File

@@ -101,12 +101,12 @@ async function get_areas() {
} }
/** /**
* Get all services in HomeAssistant. * Get all actions in HomeAssistant.
* @returns {Promise<[string, { name: string; description: string; fields: * @returns {Promise<[string, { name: string; description: string; fields:
* Record<string, { name: string; description: string; example: string; * Record<string, { name: string; description: string; example: string;
* selector: unknown; required?: boolean }> }][]>} [id, data] * selector: unknown; required?: boolean }> }][]>} [id, data]
*/ */
async function get_services() { async function get_actions() {
try { try {
const res = await fetch(api_url + '/services', { const res = await fetch(api_url + '/services', {
method: 'GET', method: 'GET',
@@ -122,15 +122,15 @@ async function get_services() {
document.querySelector('#api_url').classList.remove('invalid'); document.querySelector('#api_url').classList.remove('invalid');
document.querySelector('#api_token').classList.remove('invalid'); document.querySelector('#api_token').classList.remove('invalid');
const data = await res.json(); const data = await res.json();
const services = []; const actions = [];
for (const d of data) { for (const d of data) {
for (const service in d.services) { for (const action in d.services) {
services.push([`${d.domain}.${service}`, d.services[service]]); actions.push([`${d.domain}.${action}`, d.services[action]]);
} }
} }
return services; return actions;
} catch (e) { } catch (e) {
console.error('Error fetching services:', e); console.error('Error fetching actions:', e);
document.querySelector('#api_url').classList.add('invalid'); document.querySelector('#api_url').classList.add('invalid');
return []; return [];
} }
@@ -176,11 +176,11 @@ async function get_schema() {
* @param {Record<string, string>} areas * @param {Record<string, string>} areas
* @param {[string, { name: string; description: string; fields: * @param {[string, { name: string; description: string; fields:
* Record<string, { name: string; description: string; example: string; * Record<string, { name: string; description: string; example: string;
* selector: unknown; required?: boolean }> }][]} services * selector: unknown; required?: boolean }> }][]} actions
* @param {{}} schema * @param {{}} schema
* @returns {Promise<{}>} * @returns {Promise<{}>}
*/ */
async function generate_schema(entities, devices, areas, services, schema) { async function generate_schema(entities, devices, areas, actions, schema) {
schema.$defs.entity = { schema.$defs.entity = {
enum: Object.keys(entities), enum: Object.keys(entities),
}; };
@@ -192,12 +192,18 @@ async function generate_schema(entities, devices, areas, services, schema) {
}; };
const oneOf = []; const oneOf = [];
for (const [id, data] of services) { for (const [id, data] of actions) {
const i_properties = { const i_properties = {
action: {
title: data.name,
description: data.description,
const: id,
},
service: { service: {
title: data.name, title: data.name,
description: data.description, description: data.description,
const: id, const: id,
deprecated: true,
}, },
data: { data: {
type: 'object', type: 'object',
@@ -393,12 +399,16 @@ async function generate_schema(entities, devices, areas, services, schema) {
properties: i_properties, properties: i_properties,
}); });
} }
schema.$defs.tap_action = { schema.$defs.tap_action_tap = {
type: 'object', type: 'object',
oneOf: oneOf, oneOf: oneOf,
properties: { properties: {
action: {
type: 'string',
},
service: { service: {
type: 'string', type: 'string',
deprecated: true,
}, },
confirm: { confirm: {
$ref: '#/$defs/confirm', $ref: '#/$defs/confirm',
@@ -411,8 +421,16 @@ async function generate_schema(entities, devices, areas, services, schema) {
properties: {}, properties: {},
}, },
}, },
anyOf: [
{
required: ['action'],
},
{
required: ['service'],
},
],
}; };
delete schema.$defs.tap.properties.service; delete schema.$defs.tap.properties.action;
delete schema.$schema; delete schema.$schema;
return schema; return schema;
@@ -450,22 +468,22 @@ let entities;
let devices; let devices;
/** @type {Awaited<ReturnType<typeof get_areas>>} */ /** @type {Awaited<ReturnType<typeof get_areas>>} */
let areas; let areas;
/** @type {Awaited<ReturnType<typeof get_services>>} */ /** @type {Awaited<ReturnType<typeof get_actions>>} */
let services; let actions;
let schema; let schema;
async function loadSchema() { async function loadSchema() {
[entities, devices, areas, services, schema] = await Promise.all([ [entities, devices, areas, actions, schema] = await Promise.all([
get_entities(), get_entities(),
get_devices(), get_devices(),
get_areas(), get_areas(),
get_services(), get_actions(),
get_schema(), get_schema(),
]); ]);
if (window.makeMarkers) { if (window.makeMarkers) {
window.makeMarkers(); window.makeMarkers();
} }
try { try {
schema = await generate_schema(entities, devices, areas, services, schema); schema = await generate_schema(entities, devices, areas, actions, schema);
} catch {} } catch {}
console.log(schema); console.log(schema);
if (window.m && window.modelUri) { if (window.m && window.modelUri) {
@@ -811,14 +829,16 @@ require(['vs/editor/editor.main'], async () => {
const runAction = editor.addCommand( const runAction = editor.addCommand(
0, 0,
async function (_, action) { async function (_, tap) {
const service = action.tap_action.service.split('.'); const action = (tap.tap_action.action ?? tap.tap_action.service).split(
let data = action.tap_action.data; '.'
);
let data = tap.tap_action.data;
if (data) { if (data) {
data.entity_id = action.entity; data.entity_id = tap.entity;
} else { } else {
data = { data = {
entity_id: action.entity, entity_id: tap.entity,
}; };
} }
const t = toast({ const t = toast({
@@ -826,7 +846,7 @@ require(['vs/editor/editor.main'], async () => {
}); });
try { try {
const res = await fetch( const res = await fetch(
api_url + '/services/' + service[0] + '/' + service[1], api_url + '/services/' + action[0] + '/' + action[1],
{ {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -1152,7 +1172,7 @@ require(['vs/editor/editor.main'], async () => {
if (node.type === 'property') { if (node.type === 'property') {
if (node.key[0].value === 'tap_action') { if (node.key[0].value === 'tap_action') {
const d = get(data, path); const d = get(data, path);
if (d.tap_action.service) { if (d.tap_action.action ?? d.tap_action.service) {
lenses.push({ lenses.push({
range: { range: {
startLineNumber: node.key[0].range.start.line + 1, startLineNumber: node.key[0].range.start.line + 1,

View File

@@ -13,8 +13,8 @@
"@types/toastify-js": "1.12.0", "@types/toastify-js": "1.12.0",
"@vscode/webview-ui-toolkit": "1.4.0", "@vscode/webview-ui-toolkit": "1.4.0",
"json-ast-comments": "1.1.1", "json-ast-comments": "1.1.1",
"monaco-editor": "0.52.2", "monaco-editor": "0.54.2",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"serve": "^14.2.1" "serve": "^14.2.1"
} }
} }