mirror of
https://github.com/house-of-abbey/GarminHomeAssistant.git
synced 2025-11-13 20:48:14 +00:00
Compare commits
24 Commits
v3.4
...
support-mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aafd4fcb1 | ||
|
|
a52d6188be | ||
|
|
e697f75ce3 | ||
|
|
4fbe4135b1 | ||
|
|
edef4ef464 | ||
|
|
92e4278332 | ||
|
|
4348c899ae | ||
|
|
b34291e41f | ||
|
|
7453b40cb1 | ||
|
|
483603a44d | ||
|
|
bc3271ba63 | ||
|
|
5ccd1be4e7 | ||
|
|
7871334b4a | ||
|
|
2f3ee236e8 | ||
|
|
6609fed35d | ||
|
|
d68aecd19c | ||
|
|
9a61c9ce77 | ||
|
|
2981893af7 | ||
|
|
52e2efddd8 | ||
|
|
cb382d820f | ||
|
|
236b09d7f6 | ||
|
|
06e7c89b60 | ||
|
|
6fc6be2eef | ||
|
|
ac97a8af0d |
8
.vscode/launch.json
vendored
8
.vscode/launch.json
vendored
@@ -4,14 +4,6 @@
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: Current File",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "${file}",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
},
|
||||
{
|
||||
"type": "monkeyc",
|
||||
"request": "launch",
|
||||
|
||||
@@ -50,3 +50,4 @@
|
||||
| 3.2 | Only enable or disable sensors on HomeAssistant when the background service options is changed, i.e. do not call the API to enable on start up every time. |
|
||||
| 3.3 | Providing automatic detection for menu definition updates, but still requires an application restart. |
|
||||
| 3.4 | Fixed a bug where templates failed to display in toggle menu items (at least on some devices). Fixed a bug where a menu item requesting to exit on completion appeared to indicate failure when using Wi-Fi or LTE. The fix uses a delay in exiting the application modelled as sufficient for a Venu 2 device, so this might need tweaking for other devices. Attempt to fixed an "Out of Memory" bug caused by v3.3 by making automatic checking for menu updates both optional and automatically turned off when insufficient memory is available. This last bug is device dependent and may require another attempt. Internationalisation improvements with thanks to @krzys_h for a new automated translations script. |
|
||||
| 3.5 | Added support for Edge 550, 850 & MTB, Fenix 8 Pro 47mm, GPSMAP H1, Instinct Crossover AMOLED, Venu 4 41mm & 45mm, & Venu X1 devices which also required an SDK update to 8.3.0. The simulation of the Edge 850 device was off, as it failed to update the display and text was the wrong colour, but the buttons menu items operated HA correctly. The assumption is the simulation model is buggy until someone [reports](https://github.com/house-of-abbey/GarminHomeAssistant/issues) otherwise. |
|
||||
|
||||
26
README.md
26
README.md
@@ -81,7 +81,7 @@ Example schema:
|
||||
"name": "Food is Ready!",
|
||||
"type": "tap",
|
||||
"tap_action": {
|
||||
"service": "script.turn_on",
|
||||
"action": "script.turn_on",
|
||||
"confirm": true
|
||||
}
|
||||
},
|
||||
@@ -132,7 +132,7 @@ Example schema:
|
||||
"name": "Turn off USBs",
|
||||
"type": "tap",
|
||||
"tap_action": {
|
||||
"service": "automation.trigger"
|
||||
"action": "automation.trigger"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -140,10 +140,27 @@ Example schema:
|
||||
"name": "TV Lights Scene",
|
||||
"type": "tap",
|
||||
"tap_action": {
|
||||
"service": "scene.turn_on",
|
||||
"action": "scene.turn_on",
|
||||
"pin": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Heating",
|
||||
"content": "{{ ' %.1f' | format(state_attr('climate.myheating','temperature')) }}",
|
||||
"type": "numeric",
|
||||
"entity": "climate.myheating",
|
||||
"tap_action": {
|
||||
"action": "climate.set_temperature",
|
||||
"data": {
|
||||
"step": "0.5",
|
||||
"start": "10",
|
||||
"stop": "30",
|
||||
"valueLabel": "temperature",
|
||||
"formatString": "%.1f"
|
||||
}
|
||||
},
|
||||
"pin": false
|
||||
} ,
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -157,6 +174,7 @@ The example above illustrates how to configure:
|
||||
* Script invocation (`tap`)
|
||||
* Service invocation, e.g. Scene setting, (`tap`)
|
||||
* 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. ValueLabel defines the variable to return. You can optionally set the minimum (start) and maximum (stop) value as well as the step to increase/decrease and a tepmlate how to format the value.
|
||||
* You can also display the status of devices (`info`) which is essentially a `tap` with no action
|
||||
* All menu items can display the results of evaluating [templates](examples/Templates.md).
|
||||
|
||||
@@ -354,7 +372,7 @@ Check the latest unresolved [issues](https://github.com/house-of-abbey/GarminHom
|
||||
|
||||
5. Parameters to tap menu items cannot have their parameter usage verified. If you get this wrong and crash the application, that's your fault not the application's. In this case, start by removing the parameters for the menu item causing the crash, and add them back one at a time until you find your fault. **Please don't give the application a poor review for your bad parameter definition!**
|
||||
|
||||
6. We are unable to support Edge 540, Edge 840 and Edge 1050 devices at this time. The simulation of these devices has two unexpected errors when toggling or executing taps. We get both `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` and `Communications.BLE_QUEUE_FULL` even though the memory usage is about 6% of the available RAM. Based on a lead from user @Petucky, both devices are being re-enabled as testing on a real Edge 840 device has proven successful, however we remain unable to support either devices until the simulator is fixed.
|
||||
6. We are unable to support Edge 540, Edge 840 and Edge 1050 devices at this time. The simulation of these devices has two unexpected errors when toggling or executing taps. We get both `Communications.NETWORK_RESPONSE_OUT_OF_MEMORY` and `Communications.BLE_QUEUE_FULL` even though the memory usage is about 6% of the available RAM. Based on a lead from user @Petucky, both devices are being re-enabled as testing on a real Edge 840 device has proven successful, however we remain unable to support either devices until the simulator is fixed. The Edge 850 device has different display update issues but is functional. Again the assumption is the simulation model is buggy. Please [report](https://github.com/house-of-abbey/GarminHomeAssistant/issues) your experience on real devices to us so we can withdraw support or confirm they are working.
|
||||
|
||||
7. We are unable to support HTTP natively (without the workaround specified earlier). This is a limitation placed upon us by the Connect IQ API which for security reasons refuses to work with HTTP requests. There is nothing developers can do about this limitation. See the [Trouble Shooting](TroubleShooting.md#do-it-yourself-setup) guide for an example setup. We would appreciate it if users did not leave poor reviews for the lack of this feature which is beyond our control to fix.
|
||||
|
||||
|
||||
@@ -17,11 +17,21 @@
|
||||
"$ref": "#/$defs/items"
|
||||
}
|
||||
},
|
||||
"required": ["title", "items"],
|
||||
"required": [
|
||||
"title",
|
||||
"items"
|
||||
],
|
||||
"additionalProperties": false,
|
||||
"$defs": {
|
||||
"toggle": {
|
||||
"type": "object",
|
||||
"examples": [
|
||||
{
|
||||
"type": "toggle",
|
||||
"name": "Example",
|
||||
"entity": "switch.example"
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"entity": {
|
||||
"$ref": "#/$defs/entity"
|
||||
@@ -37,15 +47,7 @@
|
||||
"$ref": "#/$defs/content"
|
||||
},
|
||||
"tap_action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirm": {
|
||||
"$ref": "#/$defs/confirm"
|
||||
},
|
||||
"pin": {
|
||||
"$ref": "#/$defs/pin"
|
||||
}
|
||||
},
|
||||
"$ref": "#/$defs/tap_action",
|
||||
"additionalProperties": false
|
||||
},
|
||||
"enabled": {
|
||||
@@ -55,7 +57,11 @@
|
||||
"$ref": "#/$defs/exit"
|
||||
}
|
||||
},
|
||||
"required": ["entity", "name", "type"],
|
||||
"required": [
|
||||
"entity",
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"template": {
|
||||
@@ -86,7 +92,11 @@
|
||||
"$ref": "#/$defs/enabled"
|
||||
}
|
||||
},
|
||||
"required": ["name", "content", "type"],
|
||||
"required": [
|
||||
"name",
|
||||
"content",
|
||||
"type"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
@@ -109,7 +119,33 @@
|
||||
"description": "Use 'info' or 'tap' instead."
|
||||
},
|
||||
"tap_action": {
|
||||
"$ref": "#/$defs/tap_action"
|
||||
"$ref": "#/$defs/tap_action",
|
||||
"properties": {
|
||||
"service": {
|
||||
"$ref": "#/$defs/action",
|
||||
"deprecated": true
|
||||
},
|
||||
"action": {
|
||||
"$ref": "#/$defs/action"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"title": "Your action'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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/$defs/enabled"
|
||||
@@ -118,7 +154,12 @@
|
||||
"$ref": "#/$defs/exit"
|
||||
}
|
||||
},
|
||||
"required": ["name", "content", "type", "tap_action"],
|
||||
"required": [
|
||||
"name",
|
||||
"content",
|
||||
"type",
|
||||
"tap_action"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
@@ -140,11 +181,27 @@
|
||||
"$ref": "#/$defs/enabled"
|
||||
}
|
||||
},
|
||||
"required": ["name", "content", "type"],
|
||||
"required": [
|
||||
"name",
|
||||
"content",
|
||||
"type"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"tap": {
|
||||
"type": "object",
|
||||
"examples": [
|
||||
{
|
||||
"type": "tap",
|
||||
"name": "Example",
|
||||
"tap_action": {
|
||||
"action": "notify.notify",
|
||||
"data": {
|
||||
"message": "Example"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"entity": {
|
||||
"$ref": "#/$defs/entity"
|
||||
@@ -160,13 +217,39 @@
|
||||
"$ref": "#/$defs/content"
|
||||
},
|
||||
"service": {
|
||||
"$ref": "#/$defs/entity",
|
||||
"$ref": "#/$defs/action",
|
||||
"deprecated": true,
|
||||
"title": "Schema change:",
|
||||
"description": "Use 'tap_action' instead to mirror Home Assistant."
|
||||
},
|
||||
"tap_action": {
|
||||
"$ref": "#/$defs/tap_action"
|
||||
"$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"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/$defs/enabled"
|
||||
@@ -175,11 +258,22 @@
|
||||
"$ref": "#/$defs/exit"
|
||||
}
|
||||
},
|
||||
"required": ["name", "type"],
|
||||
"required": [
|
||||
"name",
|
||||
"type"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"group": {
|
||||
"type": "object",
|
||||
"examples": [
|
||||
{
|
||||
"type": "group",
|
||||
"name": "Example",
|
||||
"title": "Example",
|
||||
"items": []
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"entity": {
|
||||
"$ref": "#/$defs/entity",
|
||||
@@ -210,9 +304,435 @@
|
||||
"$ref": "#/$defs/enabled"
|
||||
}
|
||||
},
|
||||
"required": ["name", "title", "type", "items"],
|
||||
"required": [
|
||||
"name",
|
||||
"title",
|
||||
"type",
|
||||
"items"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"numeric": {
|
||||
"type": "object",
|
||||
"examples": [
|
||||
{
|
||||
"type": "numeric",
|
||||
"name": "Example",
|
||||
"entity": "light.example",
|
||||
"attribute": "brightness",
|
||||
"action": "light.turn_on",
|
||||
"action_attribute": "brightness",
|
||||
"min": 0,
|
||||
"max": 255,
|
||||
"step": 1
|
||||
},
|
||||
{
|
||||
"type": "numeric",
|
||||
"name": "Example",
|
||||
"entity": "input_number.example",
|
||||
"action": "input_number.set_value",
|
||||
"action_attribute": "value",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1
|
||||
},
|
||||
{
|
||||
"type": "numeric",
|
||||
"name": "Example",
|
||||
"entity": "number.example",
|
||||
"action": "number.set_value",
|
||||
"action_attribute": "value",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1
|
||||
},
|
||||
{
|
||||
"type": "numeric",
|
||||
"name": "Example",
|
||||
"entity": "fan.example",
|
||||
"attribute": "percentage",
|
||||
"action": "fan.set_percentage",
|
||||
"action_attribute": "percentage",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1
|
||||
},
|
||||
{
|
||||
"type": "numeric",
|
||||
"name": "Example",
|
||||
"entity": "valve.example",
|
||||
"attribute": "position",
|
||||
"action": "valve.set_valve_position",
|
||||
"action_attribute": "position",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1
|
||||
},
|
||||
{
|
||||
"type": "numeric",
|
||||
"name": "Example",
|
||||
"entity": "cover.example",
|
||||
"attribute": "position",
|
||||
"action": "cover.set_position",
|
||||
"action_attribute": "position",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1
|
||||
},
|
||||
{
|
||||
"type": "numeric",
|
||||
"name": "Example",
|
||||
"entity": "cover.example",
|
||||
"attribute": "tilt_position",
|
||||
"action": "cover.set_tilt_position",
|
||||
"action_attribute": "tilt_position",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1
|
||||
},
|
||||
{
|
||||
"type": "numeric",
|
||||
"name": "Example",
|
||||
"entity": "media_player.example",
|
||||
"attribute": "volume_level",
|
||||
"action": "media_player.volume_set",
|
||||
"action_attribute": "volume_level",
|
||||
"min": 0,
|
||||
"max": 1,
|
||||
"step": 0.01
|
||||
},
|
||||
{
|
||||
"type": "numeric",
|
||||
"name": "Example",
|
||||
"entity": "climate.example",
|
||||
"attribute": "temperature",
|
||||
"action": "climate.set_temperature",
|
||||
"action_attribute": "temperature",
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"step": 1
|
||||
}
|
||||
],
|
||||
"allOf": [
|
||||
{
|
||||
"properties": {
|
||||
"name": {
|
||||
"$ref": "#/$defs/name"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/$defs/type",
|
||||
"const": "numeric"
|
||||
},
|
||||
"content": {
|
||||
"$ref": "#/$defs/content"
|
||||
},
|
||||
"tap_action": {
|
||||
"$ref": "#/$defs/tap_action"
|
||||
},
|
||||
"enabled": {
|
||||
"$ref": "#/$defs/enabled"
|
||||
},
|
||||
"exit": {
|
||||
"$ref": "#/$defs/exit"
|
||||
},
|
||||
"min": {
|
||||
"type": "number",
|
||||
"title": "Minimum value"
|
||||
},
|
||||
"max": {
|
||||
"type": "number",
|
||||
"title": "Maximum value"
|
||||
},
|
||||
"step": {
|
||||
"type": "number",
|
||||
"title": "Step size"
|
||||
},
|
||||
"decimals": {
|
||||
"type": "number",
|
||||
"title": "Number of decimals to display",
|
||||
"minimum": 0,
|
||||
"maximum": 10,
|
||||
"default": 1
|
||||
},
|
||||
"entity": {
|
||||
"$ref": "#/$defs/entity"
|
||||
},
|
||||
"attribute": {
|
||||
"type": "string",
|
||||
"title": "Attribute on the entity with the current numeric value. To use the state of the entity, do not specify."
|
||||
},
|
||||
"action": {
|
||||
"$ref": "#/$defs/action"
|
||||
},
|
||||
"action_attribute": {
|
||||
"type": "string",
|
||||
"title": "Attribute on the action data for the value to set."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name",
|
||||
"type",
|
||||
"min",
|
||||
"max",
|
||||
"step",
|
||||
"entity",
|
||||
"action",
|
||||
"action_attribute"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"entity": {
|
||||
"pattern": "^(light|input_number|number|fan|valve|cover|media_player|climate)\\.[^.]+$"
|
||||
}
|
||||
},
|
||||
"if": {
|
||||
"properties": {
|
||||
"entity": {
|
||||
"pattern": "^light\\.[^.]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"const": "brightness"
|
||||
},
|
||||
"action": {
|
||||
"const": "light.turn_on"
|
||||
},
|
||||
"action_attribute": {
|
||||
"const": "brightness"
|
||||
},
|
||||
"min": {
|
||||
"const": 0
|
||||
},
|
||||
"max": {
|
||||
"const": 255,
|
||||
"description": "Lights are not a percentage."
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"entity": {
|
||||
"pattern": "^input_number\\.[^.]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"const": "value"
|
||||
},
|
||||
"action": {
|
||||
"const": "input_number.set_value"
|
||||
},
|
||||
"action_attribute": {
|
||||
"const": "value"
|
||||
}
|
||||
},
|
||||
"not": {
|
||||
"required": [
|
||||
"attribute"
|
||||
]
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"entity": {
|
||||
"pattern": "^number\\.[^.]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"const": "value"
|
||||
},
|
||||
"action": {
|
||||
"const": "number.set_value"
|
||||
},
|
||||
"action_attribute": {
|
||||
"const": "value"
|
||||
}
|
||||
},
|
||||
"not": {
|
||||
"required": [
|
||||
"attribute"
|
||||
]
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"entity": {
|
||||
"pattern": "^fan\\.[^.]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"const": "percentage"
|
||||
},
|
||||
"action": {
|
||||
"const": "fan.set_percentage"
|
||||
},
|
||||
"action_attribute": {
|
||||
"const": "percentage"
|
||||
},
|
||||
"min": {
|
||||
"const": 0
|
||||
},
|
||||
"max": {
|
||||
"const": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"entity": {
|
||||
"pattern": "^valve\\.[^.]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"const": "position"
|
||||
},
|
||||
"action": {
|
||||
"const": "valve.set_valve_position"
|
||||
},
|
||||
"action_attribute": {
|
||||
"const": "position"
|
||||
},
|
||||
"min": {
|
||||
"const": 0
|
||||
},
|
||||
"max": {
|
||||
"const": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"entity": {
|
||||
"pattern": "^cover\\.[^.]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"const": "position"
|
||||
},
|
||||
"action": {
|
||||
"const": "cover.set_position"
|
||||
},
|
||||
"action_attribute": {
|
||||
"const": "position"
|
||||
},
|
||||
"min": {
|
||||
"const": 0
|
||||
},
|
||||
"max": {
|
||||
"const": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"entity": {
|
||||
"pattern": "^cover\\.[^.]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"const": "tilt_position"
|
||||
},
|
||||
"action": {
|
||||
"const": "cover.set_tilt_position"
|
||||
},
|
||||
"action_attribute": {
|
||||
"const": "tilt_position"
|
||||
},
|
||||
"min": {
|
||||
"const": 0
|
||||
},
|
||||
"max": {
|
||||
"const": 100
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"entity": {
|
||||
"pattern": "^media_player\\.[^.]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"const": "volume_level"
|
||||
},
|
||||
"action": {
|
||||
"const": "media_player.volume_set"
|
||||
},
|
||||
"action_attribute": {
|
||||
"const": "volume_level"
|
||||
},
|
||||
"min": {
|
||||
"const": 0
|
||||
},
|
||||
"max": {
|
||||
"const": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"else": {
|
||||
"if": {
|
||||
"properties": {
|
||||
"entity": {
|
||||
"pattern": "^climate\\.[^.]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"then": {
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"const": "temperature"
|
||||
},
|
||||
"action": {
|
||||
"const": "climate.set_temperature"
|
||||
},
|
||||
"action_attribute": {
|
||||
"const": "temperature"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"title": "Menu item type",
|
||||
"description": "One of 'info', 'tap', 'toggle' or 'group'."
|
||||
@@ -235,6 +755,9 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/group"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/numeric"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -248,9 +771,9 @@
|
||||
"title": "Home Assistant entity name",
|
||||
"pattern": "^[^.]+\\.[^.]+$"
|
||||
},
|
||||
"service": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"title": "Home Assistant service name",
|
||||
"title": "Home Assistant action name",
|
||||
"pattern": "^[^.]+\\.[^.]+$"
|
||||
},
|
||||
"tap_action": {
|
||||
@@ -258,22 +781,13 @@
|
||||
"title": "Action",
|
||||
"description": "'confirm' field is optional.",
|
||||
"properties": {
|
||||
"service": {
|
||||
"$ref": "#/$defs/service"
|
||||
},
|
||||
"confirm": {
|
||||
"$ref": "#/$defs/confirm"
|
||||
},
|
||||
"pin": {
|
||||
"$ref": "#/$defs/pin"
|
||||
},
|
||||
"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"]
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"title": "Home Assistant Template",
|
||||
@@ -307,7 +821,10 @@
|
||||
"$ref": "#/$defs/content"
|
||||
}
|
||||
},
|
||||
"required": ["type", "content"]
|
||||
"required": [
|
||||
"type",
|
||||
"content"
|
||||
]
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
@@ -317,7 +834,9 @@
|
||||
"const": "status"
|
||||
}
|
||||
},
|
||||
"required": ["type"]
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ A simple example using a scene as a `tap` menu item.
|
||||
"name": "Telly Scene",
|
||||
"type": "tap",
|
||||
"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",
|
||||
"type": "tap",
|
||||
"tap_action": {
|
||||
"service": "notify.mobile_app_on_phone",
|
||||
"action": "notify.mobile_app_on_phone",
|
||||
"data": {
|
||||
"title": "This is a title",
|
||||
"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]
|
||||
> 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
|
||||
|
||||
@@ -87,7 +87,7 @@ You can choose individual items that will quit after they have completed their a
|
||||
"name": "Turn off Stuff",
|
||||
"type": "tap",
|
||||
"tap_action": {
|
||||
"service": "automation.trigger"
|
||||
"action": "automation.trigger"
|
||||
},
|
||||
"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",
|
||||
"type": "tap",
|
||||
"tap_action": {
|
||||
"service": "automation.trigger"
|
||||
"action": "automation.trigger"
|
||||
},
|
||||
"enabled": false
|
||||
}
|
||||
|
||||
@@ -48,12 +48,12 @@ switch:
|
||||
friendly_name: <name>
|
||||
value_template: <value>
|
||||
turn_on:
|
||||
service: <service>
|
||||
action: <action>
|
||||
data:
|
||||
entity_id: <entity>
|
||||
<attribute>: <value>
|
||||
turn_off:
|
||||
service: <service>
|
||||
action: <action>
|
||||
data:
|
||||
entity_id: <entity>
|
||||
<attribute>: <value>
|
||||
@@ -90,11 +90,11 @@ switch:
|
||||
friendly_name: Cover
|
||||
value_template: "{{ is_state('cover.cover', 'open') }}"
|
||||
turn_on:
|
||||
service: cover.open_cover
|
||||
action: cover.open_cover
|
||||
data:
|
||||
entity_id: cover.cover
|
||||
turn_off:
|
||||
service: cover.close_cover
|
||||
action: cover.close_cover
|
||||
data:
|
||||
entity_id: cover.cover
|
||||
```
|
||||
|
||||
@@ -116,7 +116,7 @@ Note: Only when you use the `tap_action` field do you also need to include the `
|
||||
"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 %}",
|
||||
"tap_action": {
|
||||
"service": "cover.toggle",
|
||||
"action": "cover.toggle",
|
||||
"pin": true
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,7 @@ An example of a dimmer light with 4 brightness settings 0..3. Here our light wor
|
||||
"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 %}",
|
||||
"tap_action": {
|
||||
"service": "light.turn_on",
|
||||
"action": "light.turn_on",
|
||||
"data": {
|
||||
"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",
|
||||
"type": "tap",
|
||||
"tap_action": {
|
||||
"service": "light.turn_on",
|
||||
"action": "light.turn_on",
|
||||
"data": {
|
||||
"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",
|
||||
"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": {
|
||||
"service": "light.turn_on",
|
||||
"action": "light.turn_on",
|
||||
"data": {
|
||||
"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",
|
||||
"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": {
|
||||
"service": "light.turn_on",
|
||||
"action": "light.turn_on",
|
||||
"data": {
|
||||
"brightness_pct": 87
|
||||
}
|
||||
|
||||
15
manifest.xml
15
manifest.xml
@@ -13,7 +13,7 @@
|
||||
|
||||
Device Information & References:
|
||||
* https://developer.garmin.com/connect-iq/compatible-devices/
|
||||
* https://developer.garmin.com/connect-iq/reference-guides/devices-reference/
|
||||
* https://developer.garmin.com/connect-iq/device-reference/
|
||||
|
||||
philipabbey's Test App id="98c36259-498a-4458-9cef-74a273ad2bc3" type="watch-app"
|
||||
Live Application id="40131e87-31ff-454b-a8e2-92276ee399d6" type="watch-app"
|
||||
@@ -24,7 +24,7 @@
|
||||
Use "Monkey C: Edit Application" from the Visual Studio Code command palette
|
||||
to update the application attributes.
|
||||
-->
|
||||
<iq:application id="98c36259-498a-4458-9cef-74a273ad2bc3" type="watch-app" name="@Strings.AppName" entry="HomeAssistantApp" launcherIcon="@Drawables.LauncherIcon" minApiLevel="3.1.0">
|
||||
<iq:application id="971834c4-e4fc-4825-801f-7ac9db0e3044" type="watch-app" name="@Strings.AppName" entry="HomeAssistantApp" launcherIcon="@Drawables.LauncherIcon" minApiLevel="3.1.0">
|
||||
<!--
|
||||
Use the following from the Visual Studio Code command palette to edit
|
||||
the build targets:
|
||||
@@ -44,7 +44,6 @@
|
||||
<iq:product id="d2mach1"/>
|
||||
<iq:product id="descentg1"/>
|
||||
<iq:product id="descentg2"/>
|
||||
<iq:product id="descentmk1"/>
|
||||
<iq:product id="descentmk2"/>
|
||||
<iq:product id="descentmk2s"/>
|
||||
<iq:product id="descentmk343mm"/>
|
||||
@@ -57,16 +56,20 @@
|
||||
<iq:product id="edge520plus"/>
|
||||
<iq:product id="edge530"/>
|
||||
<iq:product id="edge540"/>
|
||||
<iq:product id="edge550"/>
|
||||
<iq:product id="edge820"/>
|
||||
<iq:product id="edge830"/>
|
||||
<iq:product id="edge840"/>
|
||||
<iq:product id="edge850"/>
|
||||
<iq:product id="edgeexplore"/>
|
||||
<iq:product id="edgeexplore2"/>
|
||||
<iq:product id="edgemtb"/>
|
||||
<iq:product id="enduro"/>
|
||||
<iq:product id="enduro3"/>
|
||||
<iq:product id="epix2"/>
|
||||
<iq:product id="epix2pro42mm"/>
|
||||
<iq:product id="epix2pro47mm"/>
|
||||
<iq:product id="epix2pro47mmsystem7preview"/>
|
||||
<iq:product id="epix2pro51mm"/>
|
||||
<iq:product id="fenix5"/>
|
||||
<iq:product id="fenix5plus"/>
|
||||
@@ -89,6 +92,7 @@
|
||||
<iq:product id="fenix7xpronowifi"/>
|
||||
<iq:product id="fenix843mm"/>
|
||||
<iq:product id="fenix847mm"/>
|
||||
<iq:product id="fenix8pro47mm"/>
|
||||
<iq:product id="fenix8solar47mm"/>
|
||||
<iq:product id="fenix8solar51mm"/>
|
||||
<iq:product id="fenixchronos"/>
|
||||
@@ -117,6 +121,7 @@
|
||||
<iq:product id="fr970"/>
|
||||
<iq:product id="gpsmap66"/>
|
||||
<iq:product id="gpsmap67"/>
|
||||
<iq:product id="gpsmaph1"/>
|
||||
<iq:product id="instinct2"/>
|
||||
<iq:product id="instinct2s"/>
|
||||
<iq:product id="instinct2x"/>
|
||||
@@ -124,6 +129,7 @@
|
||||
<iq:product id="instinct3amoled50mm"/>
|
||||
<iq:product id="instinct3solar45mm"/>
|
||||
<iq:product id="instinctcrossover"/>
|
||||
<iq:product id="instinctcrossoveramoled"/>
|
||||
<iq:product id="instincte40mm"/>
|
||||
<iq:product id="instincte45mm"/>
|
||||
<iq:product id="legacyherocaptainmarvel"/>
|
||||
@@ -147,11 +153,14 @@
|
||||
<iq:product id="venu2s"/>
|
||||
<iq:product id="venu3"/>
|
||||
<iq:product id="venu3s"/>
|
||||
<iq:product id="venu441mm"/>
|
||||
<iq:product id="venu445mm"/>
|
||||
<iq:product id="venud"/>
|
||||
<iq:product id="venusq"/>
|
||||
<iq:product id="venusq2"/>
|
||||
<iq:product id="venusq2m"/>
|
||||
<iq:product id="venusqm"/>
|
||||
<iq:product id="venux1"/>
|
||||
<iq:product id="vivoactive3"/>
|
||||
<iq:product id="vivoactive3m"/>
|
||||
<iq:product id="vivoactive3mlte"/>
|
||||
|
||||
@@ -20,7 +20,7 @@ project.manifest = manifest.xml
|
||||
|
||||
# Device References
|
||||
# * https://developer.garmin.com/connect-iq/compatible-devices/
|
||||
# * https://developer.garmin.com/connect-iq/reference-guides/devices-reference/
|
||||
# * https://developer.garmin.com/connect-iq/device-reference/
|
||||
#
|
||||
# Widget launcher icon, multiple resolutions
|
||||
# https://forums.garmin.com/developer/connect-iq/f/discussion/255433/widget-launcher-icon-multiple-resolutions/1563305
|
||||
@@ -77,14 +77,20 @@ edge520plus.resourcePath = $(edge520plus.resourcePath);resources-launcher-35-35;
|
||||
# Screen Size 246x322 launcher icon size 35x35
|
||||
edge530.resourcePath = $(edge530.resourcePath);resources-launcher-35-35;resources-icons-28
|
||||
edge540.resourcePath = $(edge540.resourcePath);resources-launcher-35-35;resources-icons-28
|
||||
# Screen Size 420x600 launcher icon size 56x56
|
||||
edge550.resourcePath = $(edge550.resourcePath);resources-launcher-56-56;resources-icons-55
|
||||
# Screen Size 200x265 launcher icon size 35x35
|
||||
edge820.resourcePath = $(edge820.resourcePath);resources-launcher-35-35;resources-icons-24
|
||||
# Screen Size 246x322 launcher icon size 35x35
|
||||
edge830.resourcePath = $(edge830.resourcePath);resources-launcher-35-35;resources-icons-28
|
||||
edge840.resourcePath = $(edge840.resourcePath);resources-launcher-35-35;resources-icons-28
|
||||
# Screen Size 420x600 launcher icon size 56x56
|
||||
edge850.resourcePath = $(edge850.resourcePath);resources-launcher-56-56;resources-icons-55
|
||||
# Screen Size 240x400 launcher icon size 36x36
|
||||
edgeexplore.resourcePath = $(edgeexplore.resourcePath);resources-launcher-36-36;resources-icons-28
|
||||
edgeexplore2.resourcePath = $(edgeexplore2.resourcePath);resources-launcher-36-36;resources-icons-28
|
||||
# Screen Size 240x320 launcher icon size 36x36
|
||||
edgemtb.resourcePath = $(edgemtb.resourcePath);resources-launcher-36-36;resources-icons-32
|
||||
# Screen Size 280x280 launcher icon size 40x40
|
||||
enduro.resourcePath = $(enduro.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
enduro3.resourcePath = $(enduro3.resourcePath);resources-launcher-40-40;resources-icons-32
|
||||
@@ -128,6 +134,7 @@ fenix7xpronowifi.resourcePath = $(fenix7xpronowifi.resourcePath);resources-launc
|
||||
fenix843mm.resourcePath = $(fenix843mm.resourcePath);resources-launcher-60-60;resources-icons-48
|
||||
# Screen Size 454x454 launcher icon size 65x65
|
||||
fenix847mm.resourcePath = $(fenix847mm.resourcePath);resources-launcher-65-65;resources-icons-53
|
||||
fenix8pro47mm.resourcePath = $(fenix8pro47mm.resourcePath);resources-launcher-65-65;resources-icons-53
|
||||
# Screen Size 260x260 launcher icon size 40x40
|
||||
fenix8solar47mm.resourcePath = $(fenix8solar47mm.resourcePath);resources-launcher-40-40;resources-icons-30
|
||||
# Screen Size 280x280 launcher icon size 40x40
|
||||
@@ -173,6 +180,7 @@ fr970.resourcePath = $(fr970.resourcePath);resources-launcher-65-65;resources-ic
|
||||
# Screen Size 240x400 launcher icon size 38x33
|
||||
gpsmap66.resourcePath = $(gpsmap66.resourcePath);resources-launcher-33-33;resources-icons-28
|
||||
gpsmap67.resourcePath = $(gpsmap67.resourcePath);resources-launcher-33-33;resources-icons-28
|
||||
gpsmaph1.resourcePath = $(gpsmaph1.resourcePath);resources-launcher-33-33;resources-icons-28
|
||||
# Screen Size 176x176 launcher icon size 62x62
|
||||
instinct2.resourcePath = $(instinct2.resourcePath);resources-launcher-62-62;resources-icons-21-w
|
||||
# Screen Size 163x156 launcher icon size 54x54
|
||||
@@ -187,6 +195,8 @@ instinct3amoled50mm.resourcePath = $(instinct3amoled50mm.resourcePath);resources
|
||||
instinct3solar45mm.resourcePath = $(instinct3solar45mm.resourcePath);resources-launcher-62-62;resources-icons-18-w
|
||||
# Screen Size 176x176 launcher icon size 26x26
|
||||
instinctcrossover.resourcePath = $(instinctcrossover.resourcePath);resources-launcher-26-26;resources-icons-21-w
|
||||
# Screen Size 390x390 launcher icon size 38x38
|
||||
instinctcrossoveramoled.resourcePath = $(instinctcrossoveramoled.resourcePath);resources-launcher-38-38;resources-icons-46
|
||||
# Screen Size 166x166 launcher icon size 52x52, but the icon size used here is reduced as the menu items were clipped.
|
||||
instincte40mm.resourcePath = $(instincte40mm.resourcePath);resources-launcher-52-52;resources-icons-18-w
|
||||
# Screen Size 176x176 launcher icon size 62x62, but the icon size used here is reduced as the menu items were clipped.
|
||||
@@ -223,6 +233,10 @@ venu2s.resourcePath = $(venu2s.resourcePath);resources-launcher-61-61;resources-
|
||||
venu3.resourcePath = $(venu3.resourcePath);resources-launcher-70-70;resources-icons-53
|
||||
# Screen Size 390x390 launcher icon size 70x70
|
||||
venu3s.resourcePath = $(venu3s.resourcePath);resources-launcher-70-70;resources-icons-46
|
||||
# Screen Size 390x390 launcher icon size 54x54
|
||||
venu441mm.resourcePath = $(venu441mm.resourcePath);resources-launcher-54-54;resources-icons-46
|
||||
# Screen Size 454x454 launcher icon size 65x65
|
||||
venu445mm.resourcePath = $(venu445mm.resourcePath);resources-launcher-65-65;resources-icons-53
|
||||
# Screen Size 390x390 launcher icon size 60x60
|
||||
venud.resourcePath = $(venud.resourcePath);resources-launcher-60-60;resources-icons-46
|
||||
# Screen Size 240x240 launcher icon size 36x36
|
||||
@@ -232,6 +246,8 @@ venusq2.resourcePath = $(venusq2.resourcePath);resources-launcher-40-40;resource
|
||||
venusq2m.resourcePath = $(venusq2m.resourcePath);resources-launcher-40-40;resources-icons-38
|
||||
# Screen Size 240x240 launcher icon size 36x36
|
||||
venusqm.resourcePath = $(venusqm.resourcePath);resources-launcher-36-36;resources-icons-28
|
||||
# Screen Size 448x486 launcher icon size 65x65
|
||||
venux1.resourcePath = $(venux1.resourcePath);resources-launcher-65-65;resources-icons-53
|
||||
# Screen Size 240x240 launcher icon size 40x33
|
||||
vivoactive3.resourcePath = $(vivoactive3.resourcePath);resources-launcher-33-33;resources-icons-28
|
||||
vivoactive3m.resourcePath = $(vivoactive3m.resourcePath);resources-launcher-33-33;resources-icons-28
|
||||
|
||||
17
resources-launcher-38-38/drawables.xml
Normal file
17
resources-launcher-38-38/drawables.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<!--
|
||||
|
||||
Distributed under MIT Licence
|
||||
See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
|
||||
|
||||
GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
tested on a Venu 2 device. The source code is provided at:
|
||||
https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
|
||||
P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
|
||||
-->
|
||||
|
||||
<drawables>
|
||||
<bitmap id="LauncherIcon" filename="launcher.svg" />
|
||||
</drawables>
|
||||
4
resources-launcher-38-38/launcher.svg
Normal file
4
resources-launcher-38-38/launcher.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg fill="none" height="38" viewBox="0 0 400 400" width="38" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M320 301.762C320 310.012 313.25 316.762 305 316.762H95C86.75 316.762 80 310.012 80 301.762V211.762C80 203.512 84.77 191.993 90.61 186.153L189.39 87.3725C195.22 81.5425 204.77 81.5425 210.6 87.3725L309.39 186.162C315.22 191.992 320 203.522 320 211.772V301.772V301.762Z" fill="#F2F4F9"/>
|
||||
<path d="M309.39 186.153L210.61 87.3725C204.78 81.5425 195.23 81.5425 189.4 87.3725L90.61 186.153C84.78 191.983 80 203.512 80 211.762V301.762C80 310.012 86.75 316.762 95 316.762H187.27L146.64 276.132C144.55 276.852 142.32 277.262 140 277.262C128.7 277.262 119.5 268.062 119.5 256.762C119.5 245.462 128.7 236.262 140 236.262C151.3 236.262 160.5 245.462 160.5 256.762C160.5 259.092 160.09 261.322 159.37 263.412L191 295.042V179.162C184.2 175.822 179.5 168.842 179.5 160.772C179.5 149.472 188.7 140.272 200 140.272C211.3 140.272 220.5 149.472 220.5 160.772C220.5 168.842 215.8 175.822 209 179.162V260.432L240.46 228.972C239.84 227.012 239.5 224.932 239.5 222.772C239.5 211.472 248.7 202.272 260 202.272C271.3 202.272 280.5 211.472 280.5 222.772C280.5 234.072 271.3 243.272 260 243.272C257.5 243.272 255.12 242.802 252.91 241.982L209 285.892V316.772H305C313.25 316.772 320 310.022 320 301.772V211.772C320 203.522 315.23 192.002 309.39 186.162V186.153Z" fill="#18BCF2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -46,27 +46,27 @@ class Alert extends WatchUi.View {
|
||||
function initialize(params as Lang.Dictionary) {
|
||||
View.initialize();
|
||||
|
||||
mText = params[:text] as Lang.String;
|
||||
mText = params[:text] as Lang.String?;
|
||||
if (mText == null) {
|
||||
mText = "Alert";
|
||||
}
|
||||
|
||||
mFont = params[:font] as Graphics.FontType;
|
||||
mFont = params[:font] as Graphics.FontType?;
|
||||
if (mFont == null) {
|
||||
mFont = Graphics.FONT_MEDIUM;
|
||||
}
|
||||
|
||||
mFgcolor = params[:fgcolor] as Graphics.ColorType;
|
||||
mFgcolor = params[:fgcolor] as Graphics.ColorType?;
|
||||
if (mFgcolor == null) {
|
||||
mFgcolor = Graphics.COLOR_BLACK;
|
||||
}
|
||||
|
||||
mBgcolor = params[:bgcolor] as Graphics.ColorType;
|
||||
mBgcolor = params[:bgcolor] as Graphics.ColorType?;
|
||||
if (mBgcolor == null) {
|
||||
mBgcolor = Graphics.COLOR_WHITE;
|
||||
}
|
||||
|
||||
mTimeout = params[:timeout] as Lang.Number;
|
||||
mTimeout = params[:timeout] as Lang.Number?;
|
||||
if (mTimeout == null) {
|
||||
mTimeout = 2000;
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
private var mGlanceTimer as Timer.Timer?;
|
||||
private var mUpdateTimer as Timer.Timer?;
|
||||
// Array initialised by onReturnFetchMenuConfig()
|
||||
private var mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem>?;
|
||||
private var mItemsToUpdate as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem or HomeAssistantNumericMenuItem>?;
|
||||
private var mIsApp as Lang.Boolean = false; // Or Widget
|
||||
private var mUpdating as Lang.Boolean = false; // Don't start a second chain of updates
|
||||
private var mTemplates as Lang.Dictionary? = null; // Cache of compiled templates
|
||||
@@ -797,6 +797,7 @@ class HomeAssistantApp extends Application.AppBase {
|
||||
break;
|
||||
|
||||
case 200:
|
||||
// System.println("HomeAssistantApp onReturnFetchApiStatus() Response Code: 200.");
|
||||
if ((data != null) && (data instanceof Lang.Dictionary) && data["message"].equals("API running.")) {
|
||||
mApiStatus = WatchUi.loadResource($.Rez.Strings.Available) as Lang.String;
|
||||
} else {
|
||||
|
||||
@@ -99,7 +99,7 @@ class HomeAssistantMenuItemFactory {
|
||||
//! @param label Menu item label.
|
||||
//! @param entity_id Home Assistant Entity ID (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 options Menu item options to be passed on, including both SDK and menu options, e.g. exit, confirm & pin.
|
||||
//
|
||||
@@ -107,7 +107,7 @@ class HomeAssistantMenuItemFactory {
|
||||
label as Lang.String or Lang.Symbol,
|
||||
entity_id as Lang.String?,
|
||||
template as Lang.String?,
|
||||
service as Lang.String?,
|
||||
action as Lang.String?,
|
||||
data as Lang.Dictionary?,
|
||||
options as {
|
||||
:exit as Lang.Boolean,
|
||||
@@ -126,12 +126,12 @@ class HomeAssistantMenuItemFactory {
|
||||
for (var i = 0; i < keys.size(); i++) {
|
||||
options.put(keys[i], mMenuItemOptions.get(keys[i]));
|
||||
}
|
||||
if (service != null) {
|
||||
if (action != null) {
|
||||
options.put(:icon, mTapTypeIcon);
|
||||
return new HomeAssistantTapMenuItem(
|
||||
label,
|
||||
template,
|
||||
service,
|
||||
action,
|
||||
data,
|
||||
options,
|
||||
mHomeAssistantService
|
||||
@@ -148,7 +148,47 @@ class HomeAssistantMenuItemFactory {
|
||||
);
|
||||
}
|
||||
}
|
||||
//! Numeric menu item.
|
||||
//!
|
||||
//! @param definition Items array from the JSON that defines this sub menu.
|
||||
//! @param template Template for Home Assistant to render (optional)
|
||||
//
|
||||
function numeric(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
entity_id as Lang.String?,
|
||||
template as Lang.String?,
|
||||
action as Lang.String?,
|
||||
data as Lang.Dictionary?,
|
||||
options as {
|
||||
:exit as Lang.Boolean,
|
||||
:confirm as Lang.Boolean,
|
||||
:pin as Lang.Boolean,
|
||||
:icon as WatchUi.Bitmap
|
||||
}
|
||||
) as WatchUi.MenuItem {
|
||||
if (entity_id != null) {
|
||||
if (data == null) {
|
||||
data = { "entity_id" => entity_id };
|
||||
|
||||
} else {
|
||||
data.put("entity_id", entity_id);
|
||||
}
|
||||
}
|
||||
var keys = mMenuItemOptions.keys();
|
||||
for (var i = 0; i < keys.size(); i++) {
|
||||
options.put(keys[i], mMenuItemOptions.get(keys[i]));
|
||||
}
|
||||
options.put(:icon, mTapTypeIcon);
|
||||
|
||||
return new HomeAssistantNumericMenuItem(
|
||||
label,
|
||||
template,
|
||||
action,
|
||||
data,
|
||||
options,
|
||||
mHomeAssistantService
|
||||
);
|
||||
}
|
||||
//! Group menu item.
|
||||
//!
|
||||
//! @param definition Items array from the JSON that defines this sub menu.
|
||||
|
||||
182
source/HomeAssistantNumericMenuItem.mc
Normal file
182
source/HomeAssistantNumericMenuItem.mc
Normal file
@@ -0,0 +1,182 @@
|
||||
//-----------------------------------------------------------------------------------
|
||||
//
|
||||
// Distributed under MIT Licence
|
||||
// See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
//
|
||||
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
|
||||
using Toybox.Lang;
|
||||
using Toybox.WatchUi;
|
||||
using Toybox.Graphics;
|
||||
|
||||
|
||||
//! Menu button with an icon that opens a sub-menu, i.e. group, and optionally renders
|
||||
//! a Home Assistant Template.
|
||||
//
|
||||
class HomeAssistantNumericMenuItem extends HomeAssistantMenuItem {
|
||||
private var mHomeAssistantService as HomeAssistantService?;
|
||||
private var mAction as Lang.String?;
|
||||
private var mConfirm as Lang.Boolean;
|
||||
private var mExit as Lang.Boolean;
|
||||
private var mPin as Lang.Boolean;
|
||||
private var mData as Lang.Dictionary?;
|
||||
private var mValue as Lang.String?;
|
||||
private var mFormatString as Lang.String="%.1f";
|
||||
|
||||
|
||||
//! Class Constructor
|
||||
//!
|
||||
//! @param label Menu item label.
|
||||
//! @param template Menu item template.
|
||||
//! @param action Menu item action.
|
||||
//! @param data Data to supply to the action call.
|
||||
//! @param exit Should the action call complete and then exit?
|
||||
//! @param confirm Should the action call be confirmed to avoid accidental invocation?
|
||||
//! @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 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
|
||||
//! one of these objects is created for all menu items to re-use.
|
||||
//
|
||||
function initialize(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
template as Lang.String,
|
||||
action as Lang.String?,
|
||||
data as Lang.Dictionary?,
|
||||
options as {
|
||||
:alignment as WatchUi.MenuItem.Alignment,
|
||||
:icon as Graphics.BitmapType or WatchUi.Drawable or Lang.Symbol,
|
||||
:exit as Lang.Boolean,
|
||||
:confirm as Lang.Boolean,
|
||||
:pin as Lang.Boolean
|
||||
}?,
|
||||
haService as HomeAssistantService
|
||||
) {
|
||||
mAction = action;
|
||||
mData = data;
|
||||
mExit = options[:exit];
|
||||
mConfirm = options[:confirm];
|
||||
mPin = options[:pin];
|
||||
mLabel = label;
|
||||
mHomeAssistantService = haService;
|
||||
|
||||
HomeAssistantMenuItem.initialize(
|
||||
label,
|
||||
template,
|
||||
{
|
||||
:alignment => options[:alignment],
|
||||
:icon => options[:icon]
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function callAction() as Void {
|
||||
var hasTouchScreen = System.getDeviceSettings().isTouchScreen;
|
||||
if (mPin && hasTouchScreen) {
|
||||
var pin = Settings.getPin();
|
||||
if (pin != null) {
|
||||
var pinConfirmationView = new HomeAssistantPinConfirmationView();
|
||||
WatchUi.pushView(
|
||||
pinConfirmationView,
|
||||
new HomeAssistantPinConfirmationDelegate({
|
||||
:callback => method(:onConfirm),
|
||||
:pin => pin,
|
||||
:state => false,
|
||||
:view => pinConfirmationView,
|
||||
}),
|
||||
WatchUi.SLIDE_IMMEDIATE
|
||||
);
|
||||
}
|
||||
} else if (mConfirm) {
|
||||
if ((! System.getDeviceSettings().phoneConnected ||
|
||||
! System.getDeviceSettings().connectionAvailable) &&
|
||||
Settings.getWifiLteExecutionEnabled()) {
|
||||
var dialogMsg = WatchUi.loadResource($.Rez.Strings.WifiLtePrompt) as Lang.String;
|
||||
var dialog = new WatchUi.Confirmation(dialogMsg);
|
||||
WatchUi.pushView(
|
||||
dialog,
|
||||
new WifiLteExecutionConfirmDelegate({
|
||||
:type => "action",
|
||||
:action => mAction,
|
||||
:data => mData,
|
||||
:exit => mExit,
|
||||
}, dialog),
|
||||
WatchUi.SLIDE_LEFT
|
||||
);
|
||||
} else {
|
||||
var view = new HomeAssistantConfirmation();
|
||||
WatchUi.pushView(
|
||||
view,
|
||||
new HomeAssistantConfirmationDelegate({
|
||||
:callback => method(:onConfirm),
|
||||
:confirmationView => view,
|
||||
:state => false,
|
||||
}),
|
||||
WatchUi.SLIDE_IMMEDIATE
|
||||
);
|
||||
}
|
||||
} else {
|
||||
onConfirm(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
//! Callback function after the menu items selection has been (optionally) confirmed.
|
||||
//!
|
||||
//! @param b Ignored. It is included in order to match the expected function prototype of the callback method.
|
||||
//
|
||||
function onConfirm(b as Lang.Boolean) as Void {
|
||||
mHomeAssistantService.call(mAction, {"entity_id" => mData.get("entity_id").toString(),mData.get("valueLabel").toString() => mValue}, mExit);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
//! Update the menu item's sub label to display the template rendered by Home Assistant.
|
||||
//!
|
||||
//! @param data The rendered template (typically a string) to be placed in the sub label. This may
|
||||
//! unusually be a number if the SDK interprets the JSON returned by Home Assistant as such.
|
||||
//
|
||||
function updateState(data as Lang.String or Lang.Dictionary or Lang.Number or Lang.Float or Null) as Void {
|
||||
if (data == null) {
|
||||
setSubLabel($.Rez.Strings.Empty);
|
||||
} else if(data instanceof Lang.Float) {
|
||||
var f = data as Lang.Float;
|
||||
setSubLabel(f.format(mFormatString));
|
||||
} else if(data instanceof Lang.Number) {
|
||||
var f = data.toFloat() as Lang.Float;
|
||||
setSubLabel(f.format(mFormatString));
|
||||
} else if (data instanceof Lang.String){
|
||||
setSubLabel(data);
|
||||
}
|
||||
else {
|
||||
// The template must return a Float, or the item cannot be formatted locally without error.
|
||||
setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String);
|
||||
}
|
||||
WatchUi.requestUpdate();
|
||||
}
|
||||
|
||||
//! Set the mValue value.
|
||||
//!
|
||||
//! Needed to set new value via the Service call
|
||||
//
|
||||
function setValue(value as Lang.String) as Void {
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
function getData() as Lang.Dictionary {
|
||||
return mData;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -87,7 +87,7 @@ class HomeAssistantService {
|
||||
break;
|
||||
|
||||
case 200:
|
||||
// System.println("HomeAssistantService onReturnCall(): Service executed.");
|
||||
// System.println("HomeAssistantService onReturnCall(): Action executed.");
|
||||
getApp().forceStatusUpdates();
|
||||
var d = data as Lang.Array;
|
||||
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 data Data to be supplied to the service call.
|
||||
//! @param action The Home Assistant action to be run, e.g. from the JSON `action` field.
|
||||
//! @param data Data to be supplied to the action call.
|
||||
//
|
||||
function call(
|
||||
service as Lang.String,
|
||||
action as Lang.String,
|
||||
data as Lang.Dictionary?,
|
||||
exit as Lang.Boolean
|
||||
) as Void {
|
||||
@@ -136,8 +136,8 @@ class HomeAssistantService {
|
||||
WatchUi.pushView(
|
||||
dialog,
|
||||
new WifiLteExecutionConfirmDelegate({
|
||||
:type => "service",
|
||||
:service => service,
|
||||
:type => "action",
|
||||
:action => action,
|
||||
:data => data,
|
||||
:exit => exit,
|
||||
}, dialog),
|
||||
@@ -151,9 +151,9 @@ class HomeAssistantService {
|
||||
ErrorView.show(WatchUi.loadResource($.Rez.Strings.NoInternet) as Lang.String);
|
||||
} else {
|
||||
// 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() service=" + service);
|
||||
// System.println("HomeAssistantService call() action=" + action);
|
||||
|
||||
var entity_id = "";
|
||||
if (data != null) {
|
||||
|
||||
@@ -50,9 +50,9 @@ class HomeAssistantSyncDelegate extends Communications.SyncDelegate {
|
||||
var url;
|
||||
|
||||
switch (type) {
|
||||
case "service":
|
||||
var service = WifiLteExecutionConfirmDelegate.mCommandData[:service];
|
||||
url = Settings.getApiUrl() + "/services/" + service.substring(0, service.find(".")) + "/" + service.substring(service.find(".")+1, service.length());
|
||||
case "action":
|
||||
var action = WifiLteExecutionConfirmDelegate.mCommandData[:action];
|
||||
url = Settings.getApiUrl() + "/services/" + action.substring(0, action.find(".")) + "/" + action.substring(action.find(".")+1, action.length());
|
||||
var entity_id = "";
|
||||
if (data != null) {
|
||||
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?) {
|
||||
Communications.makeWebRequest(
|
||||
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,
|
||||
:headers => Settings.augmentHttpHeaders({
|
||||
|
||||
@@ -21,7 +21,7 @@ using Toybox.Graphics;
|
||||
//
|
||||
class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
||||
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 mExit as Lang.Boolean;
|
||||
private var mPin as Lang.Boolean;
|
||||
@@ -31,11 +31,11 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
||||
//!
|
||||
//! @param label Menu item label.
|
||||
//! @param template Menu item template.
|
||||
//! @param service Menu item service.
|
||||
//! @param data Data to supply to the service call.
|
||||
//! @param exit Should the service call complete and then exit?
|
||||
//! @param confirm Should the service 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 action Menu item action.
|
||||
//! @param data Data to supply to the action call.
|
||||
//! @param exit Should the action call complete and then exit?
|
||||
//! @param confirm Should the action call be confirmed to avoid accidental invocation?
|
||||
//! @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 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
|
||||
@@ -44,7 +44,7 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
||||
function initialize(
|
||||
label as Lang.String or Lang.Symbol,
|
||||
template as Lang.String,
|
||||
service as Lang.String?,
|
||||
action as Lang.String?,
|
||||
data as Lang.Dictionary?,
|
||||
options as {
|
||||
:alignment as WatchUi.MenuItem.Alignment,
|
||||
@@ -65,16 +65,16 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
||||
);
|
||||
|
||||
mHomeAssistantService = haService;
|
||||
mService = service;
|
||||
mAction = action;
|
||||
mData = data;
|
||||
mExit = options[:exit];
|
||||
mConfirm = options[:confirm];
|
||||
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;
|
||||
if (mPin && hasTouchScreen) {
|
||||
var pin = Settings.getPin();
|
||||
@@ -100,8 +100,8 @@ class HomeAssistantTapMenuItem extends HomeAssistantMenuItem {
|
||||
WatchUi.pushView(
|
||||
dialog,
|
||||
new WifiLteExecutionConfirmDelegate({
|
||||
:type => "service",
|
||||
:service => mService,
|
||||
:type => "action",
|
||||
:action => mAction,
|
||||
:data => mData,
|
||||
:exit => mExit,
|
||||
}, dialog),
|
||||
@@ -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.
|
||||
//
|
||||
function onConfirm(b as Lang.Boolean) as Void {
|
||||
if (mService != null) {
|
||||
mHomeAssistantService.call(mService, mData, mExit);
|
||||
if (mAction != null) {
|
||||
mHomeAssistantService.call(mAction, mData, mExit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ using Toybox.Timer;
|
||||
//
|
||||
class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
private var mData as Lang.Dictionary;
|
||||
private var mTemplate as Lang.String;
|
||||
private var mTemplate as Lang.String?;
|
||||
private var mExit as Lang.Boolean;
|
||||
private var mConfirm as Lang.Boolean;
|
||||
private var mPin as Lang.Boolean;
|
||||
@@ -33,7 +33,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
//!
|
||||
//! @param label Menu item label.
|
||||
//! @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.
|
||||
//
|
||||
function initialize(
|
||||
@@ -72,10 +72,12 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
//
|
||||
private function setUiToggle(state as Null or Lang.String) as Void {
|
||||
if (state != null) {
|
||||
if (state.equals("on") && !isEnabled()) {
|
||||
setEnabled(true);
|
||||
} else if (state.equals("off") && isEnabled()) {
|
||||
if (state.equals("unavailable" || "unknown")) {
|
||||
return;
|
||||
} else if ((state.equals("off") || state.equals("closed")) && isEnabled()) {
|
||||
setEnabled(false);
|
||||
} else if (!isEnabled()) {
|
||||
setEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -295,7 +297,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
|
||||
//! Call a Home Assistant service 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;
|
||||
if (mPin && hasTouchScreen) {
|
||||
// Undo the toggle
|
||||
@@ -377,7 +379,7 @@ class HomeAssistantToggleMenuItem extends WatchUi.ToggleMenuItem {
|
||||
//! @param id The entity ID, e.g., `"switch.kitchen"`.
|
||||
//! @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 {
|
||||
var url = Settings.getApiUrl() + "/services/";
|
||||
|
||||
@@ -47,7 +47,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
var content = items[i].get("content") as Lang.String?;
|
||||
var entity = items[i].get("entity") as Lang.String?;
|
||||
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 pin = false as Lang.Boolean?;
|
||||
var data = null as Lang.Dictionary?;
|
||||
@@ -60,7 +60,10 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
exit = items[i].get("exit"); // Optional
|
||||
}
|
||||
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
|
||||
if (tap_action.get("confirm") != null) {
|
||||
confirm = tap_action.get("confirm"); // Optional
|
||||
@@ -81,12 +84,12 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
:pin => pin
|
||||
}
|
||||
));
|
||||
} else if (type.equals("tap") && service != null) {
|
||||
} else if (type.equals("tap") && action != null) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().tap(
|
||||
name,
|
||||
entity,
|
||||
content,
|
||||
service,
|
||||
action,
|
||||
data,
|
||||
{
|
||||
:exit => exit,
|
||||
@@ -103,7 +106,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
name,
|
||||
entity,
|
||||
content,
|
||||
service,
|
||||
action,
|
||||
data,
|
||||
{
|
||||
:exit => false,
|
||||
@@ -117,7 +120,7 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
name,
|
||||
entity,
|
||||
content,
|
||||
service,
|
||||
action,
|
||||
data,
|
||||
{
|
||||
:exit => exit,
|
||||
@@ -126,13 +129,26 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
}
|
||||
));
|
||||
}
|
||||
} else if (type.equals("numeric") && action != null) {
|
||||
addItem(HomeAssistantMenuItemFactory.create().numeric(
|
||||
name,
|
||||
entity,
|
||||
content,
|
||||
action,
|
||||
data,
|
||||
{
|
||||
:exit => exit,
|
||||
:confirm => confirm,
|
||||
:pin => pin
|
||||
}
|
||||
));
|
||||
} else if (type.equals("info") && content != null) {
|
||||
// Cannot exit from a non-actionable information only menu item.
|
||||
addItem(HomeAssistantMenuItemFactory.create().tap(
|
||||
name,
|
||||
entity,
|
||||
content,
|
||||
service,
|
||||
action,
|
||||
data,
|
||||
{
|
||||
:exit => false,
|
||||
@@ -154,7 +170,8 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
//!
|
||||
//! @return An array of menu items that need to be updated periodically to reflect the latest Home Assistant state.
|
||||
//
|
||||
function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem or Null> {
|
||||
|
||||
function getItemsToUpdate() as Lang.Array<HomeAssistantToggleMenuItem or HomeAssistantTapMenuItem or HomeAssistantGroupMenuItem or HomeAssistantNumericMenuItem or Null> {
|
||||
var fullList = [];
|
||||
var lmi = mItems as Lang.Array<WatchUi.MenuItem>;
|
||||
|
||||
@@ -167,6 +184,12 @@ class HomeAssistantView extends WatchUi.Menu2 {
|
||||
fullList.add(item);
|
||||
}
|
||||
fullList.addAll(item.getMenuView().getItemsToUpdate());
|
||||
} else if (item instanceof HomeAssistantNumericMenuItem) {
|
||||
// Numeric items can have an optional template to evaluate
|
||||
var nmi = item as HomeAssistantNumericMenuItem;
|
||||
if (nmi.hasTemplate()) {
|
||||
fullList.add(item);
|
||||
}
|
||||
} else if (item instanceof HomeAssistantToggleMenuItem) {
|
||||
fullList.add(item);
|
||||
} else if (item instanceof HomeAssistantTapMenuItem) {
|
||||
@@ -243,11 +266,21 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate {
|
||||
if (item instanceof HomeAssistantToggleMenuItem) {
|
||||
var haToggleItem = item as HomeAssistantToggleMenuItem;
|
||||
// System.println(haToggleItem.getLabel() + " " + haToggleItem.getId() + " " + haToggleItem.isEnabled());
|
||||
haToggleItem.callService(haToggleItem.isEnabled());
|
||||
haToggleItem.callAction(haToggleItem.isEnabled());
|
||||
} else if (item instanceof HomeAssistantTapMenuItem) {
|
||||
var haItem = item as HomeAssistantTapMenuItem;
|
||||
// System.println(haItem.getLabel() + " " + haItem.getId());
|
||||
haItem.callService();
|
||||
haItem.callAction();
|
||||
} else if (item instanceof HomeAssistantNumericMenuItem) {
|
||||
var haItem = item as HomeAssistantNumericMenuItem;
|
||||
// System.println(haItem.getLabel() + " " + haItem.getId());
|
||||
// create new view to select new value
|
||||
|
||||
var mPickerFactory = new HomeAssistantNumericFactory(haItem.getData());
|
||||
|
||||
var mPicker = new HomeAssistantNumericPicker(mPickerFactory,haItem);
|
||||
var mPickerDelegate = new HomeAssistantNumericPickerDelegate(mPicker);
|
||||
WatchUi.pushView(mPicker,mPickerDelegate,WatchUi.SLIDE_LEFT);
|
||||
} else if (item instanceof HomeAssistantGroupMenuItem) {
|
||||
var haMenuItem = item as HomeAssistantGroupMenuItem;
|
||||
// System.println("IconMenu: " + haMenuItem.getLabel() + " " + haMenuItem.getId());
|
||||
|
||||
@@ -29,10 +29,10 @@ using Toybox.Time;
|
||||
//
|
||||
(:glance, :background)
|
||||
class Settings {
|
||||
private static var mApiKey as Lang.String = "";
|
||||
private static var mWebhookId as Lang.String = "";
|
||||
private static var mApiUrl as Lang.String = "";
|
||||
private static var mConfigUrl as Lang.String = "";
|
||||
private static var mApiKey as Lang.String? = "";
|
||||
private static var mWebhookId as Lang.String? = "";
|
||||
private static var mApiUrl as Lang.String? = "";
|
||||
private static var mConfigUrl as Lang.String? = "";
|
||||
private static var mCacheConfig as Lang.Boolean = false;
|
||||
private static var mClearCache as Lang.Boolean = false;
|
||||
private static var mMenuCheck as Lang.Boolean = false;
|
||||
|
||||
@@ -25,12 +25,12 @@ using Toybox.Timer;
|
||||
class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
|
||||
public static var mCommandData as {
|
||||
:type as Lang.String,
|
||||
:service as Lang.String?,
|
||||
:action as Lang.String?,
|
||||
:data as Lang.Dictionary?,
|
||||
:url as Lang.String?,
|
||||
:id as Lang.Number?,
|
||||
:exit as Lang.Boolean
|
||||
};
|
||||
}?;
|
||||
|
||||
private static var mTimer as Timer.Timer?;
|
||||
private var mHasToast as Lang.Boolean = false;
|
||||
@@ -40,8 +40,8 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
|
||||
//!
|
||||
//! @param options A dictionary describing the command to be executed:<br>
|
||||
//! `{`<br>
|
||||
//!   `:type: as Lang.String,` // The command type, either `"service"` or `"entity"`.<br>
|
||||
//!   `:service: as Lang.String?,` // (For type `"service"`) The Home Assistant service to call (e.g., "light.turn_on").<br>
|
||||
//!   `:type: as Lang.String,` // The command type, either `"action"` or `"entity"`.<br>
|
||||
//!   `:action: as Lang.String?,` // (For type `"action"`) The Home Assistant action to call (e.g., "light.turn_on").<br>
|
||||
//!   `:url: as Lang.Dictionary?,` // (For type `"entity"`) The full Home Assistant entity API URL.<br>
|
||||
//!   `:callback: as Lang.String?,` // (For type `"entity"`) A callback method (Method<data as Dictionary>) to handle the response.<br>
|
||||
//!   `: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(
|
||||
cOptions as {
|
||||
:type as Lang.String,
|
||||
:service as Lang.String?,
|
||||
:action as Lang.String?,
|
||||
:data as Lang.Dictionary?,
|
||||
:url as Lang.String?,
|
||||
:callback as Lang.Method?,
|
||||
@@ -73,7 +73,7 @@ class WifiLteExecutionConfirmDelegate extends WatchUi.ConfirmationDelegate {
|
||||
mConfirmationView = view;
|
||||
mCommandData = {
|
||||
:type => cOptions[:type],
|
||||
:service => cOptions[:service],
|
||||
:action => cOptions[:action],
|
||||
:data => cOptions[:data],
|
||||
:url => cOptions[:url],
|
||||
:callback => cOptions[:callback],
|
||||
|
||||
88
source/factory/HomeAssistantNumericFactory.mc
Normal file
88
source/factory/HomeAssistantNumericFactory.mc
Normal file
@@ -0,0 +1,88 @@
|
||||
//-----------------------------------------------------------------------------------
|
||||
//
|
||||
// Distributed under MIT Licence
|
||||
// See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
//
|
||||
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//------------------------------------------------------------
|
||||
|
||||
import Toybox.Graphics;
|
||||
import Toybox.Lang;
|
||||
import Toybox.WatchUi;
|
||||
|
||||
//! Factory that controls which numbers can be picked
|
||||
class HomeAssistantNumericFactory extends WatchUi.PickerFactory {
|
||||
// define default values in case not contained in data
|
||||
private var mStart as Lang.Float = 0.0;
|
||||
private var mStop as Lang.Float = 100.0;
|
||||
private var mStep as Lang.Float = 1.0;
|
||||
private var mFormatString as Lang.String = "%.2f";
|
||||
|
||||
//! Class Constructor
|
||||
//!
|
||||
public function initialize(data as Lang.Dictionary) {
|
||||
PickerFactory.initialize();
|
||||
|
||||
// Get values from data
|
||||
|
||||
var val = data.get("start");
|
||||
if (val != null) {
|
||||
mStart = val.toString().toFloat();
|
||||
}
|
||||
val = data.get("stop");
|
||||
if (val != null) {
|
||||
mStop = val.toString().toFloat();
|
||||
}
|
||||
val = data.get("step");
|
||||
if (val != null) {
|
||||
mStep = val.toString().toFloat();
|
||||
}
|
||||
val = data.get("formatString");
|
||||
if (val != null) {
|
||||
mFormatString = val.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//! Get the index of a number item
|
||||
//! @param value The number to get the index of
|
||||
//! @return The index of the number
|
||||
public function getIndex(value as Float) as Number {
|
||||
return ((value / mStep) - mStart).toNumber();
|
||||
}
|
||||
|
||||
//! Generate a Drawable instance for an item
|
||||
//! @param index The item index
|
||||
//! @param selected true if the current item is selected, false otherwise
|
||||
//! @return Drawable for the item
|
||||
public function getDrawable(index as Number, selected as Boolean) as Drawable? {
|
||||
var value = getValue(index);
|
||||
var text = "No item";
|
||||
if (value instanceof Lang.Float) {
|
||||
text = value.format(mFormatString);
|
||||
}
|
||||
return new WatchUi.Text({:text=>text, :color=>Graphics.COLOR_WHITE,
|
||||
:locX=>WatchUi.LAYOUT_HALIGN_CENTER, :locY=>WatchUi.LAYOUT_VALIGN_CENTER});
|
||||
}
|
||||
|
||||
//! Get the value of the item at the given index
|
||||
//! @param index Index of the item to get the value of
|
||||
//! @return Value of the item
|
||||
public function getValue(index as Number) as Object? {
|
||||
return mStart + (index * mStep);
|
||||
}
|
||||
|
||||
//! Get the number of picker items
|
||||
//! @return Number of items
|
||||
public function getSize() as Number {
|
||||
return ((mStop - mStart) / mStep).toNumber() + 1;
|
||||
}
|
||||
|
||||
}
|
||||
102
source/picker/HomeAssistantNumericPicker.mc
Normal file
102
source/picker/HomeAssistantNumericPicker.mc
Normal file
@@ -0,0 +1,102 @@
|
||||
//-----------------------------------------------------------------------------------
|
||||
//
|
||||
// Distributed under MIT Licence
|
||||
// See https://github.com/house-of-abbey/GarminHomeAssistant/blob/main/LICENSE.
|
||||
//
|
||||
//-----------------------------------------------------------------------------------
|
||||
//
|
||||
// GarminHomeAssistant is a Garmin IQ application written in Monkey C and routinely
|
||||
// tested on a Venu 2 device. The source code is provided at:
|
||||
// https://github.com/house-of-abbey/GarminHomeAssistant.
|
||||
//
|
||||
// P A Abbey & J D Abbey & Someone0nEarth, 31 October 2023
|
||||
//
|
||||
//------------------------------------------------------------
|
||||
|
||||
using Toybox.Application;
|
||||
using Toybox.Lang;
|
||||
using Toybox.Graphics;
|
||||
using Toybox.System;
|
||||
using Toybox.WatchUi;
|
||||
|
||||
//! Picker that allows the user to choose a float value
|
||||
class HomeAssistantNumericPicker extends WatchUi.Picker {
|
||||
|
||||
private var mFactory as HomeAssistantNumericFactory;
|
||||
private var mItem as HomeAssistantNumericMenuItem;
|
||||
|
||||
//! Constructor
|
||||
public function initialize(factory as HomeAssistantNumericFactory, haItem as HomeAssistantNumericMenuItem) {
|
||||
|
||||
mFactory = factory;
|
||||
|
||||
|
||||
var pickerOptions = {:pattern=>[mFactory]};
|
||||
mItem=haItem;
|
||||
|
||||
|
||||
var data = mItem.getData();
|
||||
|
||||
var start = 0.0;
|
||||
var val = data.get("start");
|
||||
if (val != null) {
|
||||
start = val.toString().toFloat();
|
||||
}
|
||||
var step = 1.0;
|
||||
val = data.get("step");
|
||||
if (val != null) {
|
||||
step = val.toString().toFloat();
|
||||
}
|
||||
val = haItem.getSubLabel().toFloat();
|
||||
var index = ((val -start) / step).toNumber();
|
||||
|
||||
pickerOptions[:defaults] =[index];
|
||||
|
||||
var title = new WatchUi.Text({:text=>haItem.getLabel(), :locX=>WatchUi.LAYOUT_HALIGN_CENTER,
|
||||
:locY=>WatchUi.LAYOUT_VALIGN_BOTTOM});
|
||||
pickerOptions[:title] = title;
|
||||
|
||||
|
||||
Picker.initialize(pickerOptions);
|
||||
|
||||
}
|
||||
|
||||
|
||||
//! Get whether the user is done picking
|
||||
//! @param value Value user selected
|
||||
//! @return true if user is done, false otherwise
|
||||
public function onConfirm(value as Lang.String) as Void {
|
||||
mItem.setValue(value);
|
||||
mItem.callAction();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
//! Responds to a numeric picker selection or cancellation
|
||||
class HomeAssistantNumericPickerDelegate extends WatchUi.PickerDelegate {
|
||||
private var mPicker as HomeAssistantNumericPicker;
|
||||
|
||||
//! Constructor
|
||||
public function initialize(picker as HomeAssistantNumericPicker) {
|
||||
PickerDelegate.initialize();
|
||||
mPicker = picker;
|
||||
}
|
||||
|
||||
//! Handle a cancel event from the picker
|
||||
//! @return true if handled, false otherwise
|
||||
public function onCancel() as Lang.Boolean {
|
||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||
return true;
|
||||
}
|
||||
|
||||
//! Handle a confirm event from the picker
|
||||
//! @param values The values chosen in the picker
|
||||
//! @return true if handled, false otherwise
|
||||
public function onAccept(values as Lang.Array) as Lang.Boolean {
|
||||
var chosenValue = values[0].toString();
|
||||
mPicker.onConfirm(chosenValue);
|
||||
WatchUi.popView(WatchUi.SLIDE_RIGHT);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
30
web/main.js
30
web/main.js
@@ -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:
|
||||
* Record<string, { name: string; description: string; example: string;
|
||||
* selector: unknown; required?: boolean }> }][]>} [id, data]
|
||||
*/
|
||||
async function get_services() {
|
||||
async function get_actions() {
|
||||
try {
|
||||
const res = await fetch(api_url + '/services', {
|
||||
method: 'GET',
|
||||
@@ -122,15 +122,15 @@ async function get_services() {
|
||||
document.querySelector('#api_url').classList.remove('invalid');
|
||||
document.querySelector('#api_token').classList.remove('invalid');
|
||||
const data = await res.json();
|
||||
const services = [];
|
||||
const actions = [];
|
||||
for (const d of data) {
|
||||
for (const service in d.services) {
|
||||
services.push([`${d.domain}.${service}`, d.services[service]]);
|
||||
for (const action in d.services) {
|
||||
actions.push([`${d.domain}.${action}`, d.services[action]]);
|
||||
}
|
||||
}
|
||||
return services;
|
||||
return actions;
|
||||
} catch (e) {
|
||||
console.error('Error fetching services:', e);
|
||||
console.error('Error fetching actions:', e);
|
||||
document.querySelector('#api_url').classList.add('invalid');
|
||||
return [];
|
||||
}
|
||||
@@ -154,11 +154,11 @@ async function get_schema() {
|
||||
* @param {Record<string, string>} areas
|
||||
* @param {[string, { name: string; description: string; fields:
|
||||
* Record<string, { name: string; description: string; example: string;
|
||||
* selector: unknown; required?: boolean }> }][]} services
|
||||
* selector: unknown; required?: boolean }> }][]} actions
|
||||
* @param {{}} schema
|
||||
* @returns {Promise<{}>}
|
||||
*/
|
||||
async function generate_schema(entities, devices, areas, services, schema) {
|
||||
async function generate_schema(entities, devices, areas, actions, schema) {
|
||||
schema.$defs.entity = {
|
||||
enum: Object.keys(entities),
|
||||
};
|
||||
@@ -170,7 +170,7 @@ async function generate_schema(entities, devices, areas, services, schema) {
|
||||
};
|
||||
|
||||
const oneOf = [];
|
||||
for (const [id, data] of services) {
|
||||
for (const [id, data] of actions) {
|
||||
const i_properties = {
|
||||
service: {
|
||||
title: data.name,
|
||||
@@ -428,22 +428,22 @@ let entities;
|
||||
let devices;
|
||||
/** @type {Awaited<ReturnType<typeof get_areas>>} */
|
||||
let areas;
|
||||
/** @type {Awaited<ReturnType<typeof get_services>>} */
|
||||
let services;
|
||||
/** @type {Awaited<ReturnType<typeof get_actions>>} */
|
||||
let actions;
|
||||
let schema;
|
||||
async function loadSchema() {
|
||||
[entities, devices, areas, services, schema] = await Promise.all([
|
||||
[entities, devices, areas, actions, schema] = await Promise.all([
|
||||
get_entities(),
|
||||
get_devices(),
|
||||
get_areas(),
|
||||
get_services(),
|
||||
get_actions(),
|
||||
get_schema(),
|
||||
]);
|
||||
if (window.makeMarkers) {
|
||||
window.makeMarkers();
|
||||
}
|
||||
try {
|
||||
schema = await generate_schema(entities, devices, areas, services, schema);
|
||||
schema = await generate_schema(entities, devices, areas, actions, schema);
|
||||
} catch {}
|
||||
console.log(schema);
|
||||
if (window.m && window.modelUri) {
|
||||
|
||||
Reference in New Issue
Block a user