mirror of
				https://github.com/house-of-abbey/GarminHomeAssistant.git
				synced 2025-10-31 15:48:13 +00:00 
			
		
		
		
	Add Numeric Menu Item (#298)
Added a new numeric menu item to set numeric values e.g. for heating, volume, dimmer etc.
This commit is contained in:
		
							
								
								
									
										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 |   // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||||||
|   "version": "0.2.0", |   "version": "0.2.0", | ||||||
|   "configurations": [ |   "configurations": [ | ||||||
|     { |  | ||||||
|       "name": "Python: Current File", |  | ||||||
|       "type": "python", |  | ||||||
|       "request": "launch", |  | ||||||
|       "program": "${file}", |  | ||||||
|       "console": "integratedTerminal", |  | ||||||
|       "justMyCode": true |  | ||||||
|     }, |  | ||||||
|     { |     { | ||||||
|       "type": "monkeyc", |       "type": "monkeyc", | ||||||
|       "request": "launch", |       "request": "launch", | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								README.md
									
									
									
									
									
								
							| @@ -143,7 +143,24 @@ Example schema: | |||||||
|         "service": "scene.turn_on", |         "service": "scene.turn_on", | ||||||
|         "pin": true |         "pin": true | ||||||
|       } |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |     "name": "Heating", | ||||||
|  |     "content": "{{ ' %.1f' | format(state_attr('climate.myheating','temperature'))  }}", | ||||||
|  |     "type": "numeric", | ||||||
|  |         "entity": "climate.myheating", | ||||||
|  |         "tap_action": { | ||||||
|  |           "service": "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`) | * Script invocation (`tap`) | ||||||
| * Service invocation, e.g. Scene setting, (`tap`) | * Service 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. 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 | * 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). | * All menu items can display the results of evaluating [templates](examples/Templates.md). | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,7 +17,10 @@ | |||||||
|       "$ref": "#/$defs/items" |       "$ref": "#/$defs/items" | ||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "required": ["title", "items"], |   "required": [ | ||||||
|  |     "title", | ||||||
|  |     "items" | ||||||
|  |   ], | ||||||
|   "additionalProperties": false, |   "additionalProperties": false, | ||||||
|   "$defs": { |   "$defs": { | ||||||
|     "toggle": { |     "toggle": { | ||||||
| @@ -37,15 +40,7 @@ | |||||||
|           "$ref": "#/$defs/content" |           "$ref": "#/$defs/content" | ||||||
|         }, |         }, | ||||||
|         "tap_action": { |         "tap_action": { | ||||||
|           "type": "object", |           "$ref": "#/$defs/tap_action", | ||||||
|           "properties": { |  | ||||||
|             "confirm": { |  | ||||||
|               "$ref": "#/$defs/confirm" |  | ||||||
|             }, |  | ||||||
|             "pin": { |  | ||||||
|               "$ref": "#/$defs/pin" |  | ||||||
|             } |  | ||||||
|           }, |  | ||||||
|           "additionalProperties": false |           "additionalProperties": false | ||||||
|         }, |         }, | ||||||
|         "enabled": { |         "enabled": { | ||||||
| @@ -55,7 +50,11 @@ | |||||||
|           "$ref": "#/$defs/exit" |           "$ref": "#/$defs/exit" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "required": ["entity", "name", "type"], |       "required": [ | ||||||
|  |         "entity", | ||||||
|  |         "name", | ||||||
|  |         "type" | ||||||
|  |       ], | ||||||
|       "additionalProperties": false |       "additionalProperties": false | ||||||
|     }, |     }, | ||||||
|     "template": { |     "template": { | ||||||
| @@ -86,7 +85,11 @@ | |||||||
|               "$ref": "#/$defs/enabled" |               "$ref": "#/$defs/enabled" | ||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|           "required": ["name", "content", "type"], |           "required": [ | ||||||
|  |             "name", | ||||||
|  |             "content", | ||||||
|  |             "type" | ||||||
|  |           ], | ||||||
|           "additionalProperties": false |           "additionalProperties": false | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
| @@ -109,7 +112,20 @@ | |||||||
|               "description": "Use 'info' or 'tap' instead." |               "description": "Use 'info' or 'tap' instead." | ||||||
|             }, |             }, | ||||||
|             "tap_action": { |             "tap_action": { | ||||||
|               "$ref": "#/$defs/tap_action" |               "$ref": "#/$defs/tap_action", | ||||||
|  |               "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" | ||||||
| @@ -118,7 +134,12 @@ | |||||||
|               "$ref": "#/$defs/exit" |               "$ref": "#/$defs/exit" | ||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|           "required": ["name", "content", "type", "tap_action"], |           "required": [ | ||||||
|  |             "name", | ||||||
|  |             "content", | ||||||
|  |             "type", | ||||||
|  |             "tap_action" | ||||||
|  |           ], | ||||||
|           "additionalProperties": false |           "additionalProperties": false | ||||||
|         } |         } | ||||||
|       ] |       ] | ||||||
| @@ -140,7 +161,11 @@ | |||||||
|           "$ref": "#/$defs/enabled" |           "$ref": "#/$defs/enabled" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "required": ["name", "content", "type"], |       "required": [ | ||||||
|  |         "name", | ||||||
|  |         "content", | ||||||
|  |         "type" | ||||||
|  |       ], | ||||||
|       "additionalProperties": false |       "additionalProperties": false | ||||||
|     }, |     }, | ||||||
|     "tap": { |     "tap": { | ||||||
| @@ -160,13 +185,26 @@ | |||||||
|           "$ref": "#/$defs/content" |           "$ref": "#/$defs/content" | ||||||
|         }, |         }, | ||||||
|         "service": { |         "service": { | ||||||
|           "$ref": "#/$defs/entity", |           "$ref": "#/$defs/service", | ||||||
|           "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", | ||||||
|  |           "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" | ||||||
| @@ -175,7 +213,10 @@ | |||||||
|           "$ref": "#/$defs/exit" |           "$ref": "#/$defs/exit" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "required": ["name", "type"], |       "required": [ | ||||||
|  |         "name", | ||||||
|  |         "type" | ||||||
|  |       ], | ||||||
|       "additionalProperties": false |       "additionalProperties": false | ||||||
|     }, |     }, | ||||||
|     "group": { |     "group": { | ||||||
| @@ -210,9 +251,301 @@ | |||||||
|           "$ref": "#/$defs/enabled" |           "$ref": "#/$defs/enabled" | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|       "required": ["name", "title", "type", "items"], |       "required": [ | ||||||
|  |         "name", | ||||||
|  |         "title", | ||||||
|  |         "type", | ||||||
|  |         "items" | ||||||
|  |       ], | ||||||
|       "additionalProperties": false |       "additionalProperties": false | ||||||
|     }, |     }, | ||||||
|  |     "numeric": { | ||||||
|  |       "type": "object", | ||||||
|  |       "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" | ||||||
|  |             }, | ||||||
|  |             "entity": { | ||||||
|  |               "$ref": "#/$defs/entity" | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           "required": [ | ||||||
|  |             "name", | ||||||
|  |             "type", | ||||||
|  |             "entity" | ||||||
|  |           ], | ||||||
|  |           "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" | ||||||
|  |               }, | ||||||
|  |               "service": { | ||||||
|  |                 "const": "light.turn_on" | ||||||
|  |               }, | ||||||
|  |               "data_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" | ||||||
|  |                 }, | ||||||
|  |                 "service": { | ||||||
|  |                   "const": "input_number.set_value" | ||||||
|  |                 }, | ||||||
|  |                 "data_attribute": { | ||||||
|  |                   "const": "value" | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |               "not": { | ||||||
|  |                 "required": [ | ||||||
|  |                   "attribute" | ||||||
|  |                 ] | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |             "else": { | ||||||
|  |               "if": { | ||||||
|  |                 "properties": { | ||||||
|  |                   "entity": { | ||||||
|  |                     "pattern": "^number\\.[^.]+$" | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |               "then": { | ||||||
|  |                 "properties": { | ||||||
|  |                   "attribute": { | ||||||
|  |                     "const": "value" | ||||||
|  |                   }, | ||||||
|  |                   "service": { | ||||||
|  |                     "const": "number.set_value" | ||||||
|  |                   }, | ||||||
|  |                   "data_attribute": { | ||||||
|  |                     "const": "value" | ||||||
|  |                   } | ||||||
|  |                 }, | ||||||
|  |                 "not": { | ||||||
|  |                   "required": [ | ||||||
|  |                     "attribute" | ||||||
|  |                   ] | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |               "else": { | ||||||
|  |                 "if": { | ||||||
|  |                   "properties": { | ||||||
|  |                     "entity": { | ||||||
|  |                       "pattern": "^fan\\.[^.]+$" | ||||||
|  |                     } | ||||||
|  |                   } | ||||||
|  |                 }, | ||||||
|  |                 "then": { | ||||||
|  |                   "properties": { | ||||||
|  |                     "attribute": { | ||||||
|  |                       "const": "percentage" | ||||||
|  |                     }, | ||||||
|  |                     "service": { | ||||||
|  |                       "const": "fan.set_percentage" | ||||||
|  |                     }, | ||||||
|  |                     "data_attribute": { | ||||||
|  |                       "const": "percentage" | ||||||
|  |                     }, | ||||||
|  |                     "min": { | ||||||
|  |                       "const": 0 | ||||||
|  |                     }, | ||||||
|  |                     "max": { | ||||||
|  |                       "const": 100 | ||||||
|  |                     } | ||||||
|  |                   } | ||||||
|  |                 }, | ||||||
|  |                 "else": { | ||||||
|  |                   "if": { | ||||||
|  |                     "properties": { | ||||||
|  |                       "entity": { | ||||||
|  |                         "pattern": "^valve\\.[^.]+$" | ||||||
|  |                       } | ||||||
|  |                     } | ||||||
|  |                   }, | ||||||
|  |                   "then": { | ||||||
|  |                     "properties": { | ||||||
|  |                       "attribute": { | ||||||
|  |                         "const": "position" | ||||||
|  |                       }, | ||||||
|  |                       "service": { | ||||||
|  |                         "const": "valve.set_valve_position" | ||||||
|  |                       }, | ||||||
|  |                       "data_attribute": { | ||||||
|  |                         "const": "position" | ||||||
|  |                       }, | ||||||
|  |                       "min": { | ||||||
|  |                         "const": 0 | ||||||
|  |                       }, | ||||||
|  |                       "max": { | ||||||
|  |                         "const": 100 | ||||||
|  |                       } | ||||||
|  |                     } | ||||||
|  |                   }, | ||||||
|  |                   "else": { | ||||||
|  |                     "if": { | ||||||
|  |                       "properties": { | ||||||
|  |                         "entity": { | ||||||
|  |                           "pattern": "^cover\\.[^.]+$" | ||||||
|  |                         } | ||||||
|  |                       } | ||||||
|  |                     }, | ||||||
|  |                     "then": { | ||||||
|  |                       "properties": { | ||||||
|  |                         "attribute": { | ||||||
|  |                           "const": "position" | ||||||
|  |                         }, | ||||||
|  |                         "service": { | ||||||
|  |                           "const": "cover.set_position" | ||||||
|  |                         }, | ||||||
|  |                         "data_attribute": { | ||||||
|  |                           "const": "position" | ||||||
|  |                         }, | ||||||
|  |                         "min": { | ||||||
|  |                           "const": 0 | ||||||
|  |                         }, | ||||||
|  |                         "max": { | ||||||
|  |                           "const": 100 | ||||||
|  |                         } | ||||||
|  |                       } | ||||||
|  |                     }, | ||||||
|  |                     "else": { | ||||||
|  |                       "if": { | ||||||
|  |                         "properties": { | ||||||
|  |                           "entity": { | ||||||
|  |                             "pattern": "^cover\\.[^.]+$" | ||||||
|  |                           } | ||||||
|  |                         } | ||||||
|  |                       }, | ||||||
|  |                       "then": { | ||||||
|  |                         "properties": { | ||||||
|  |                           "attribute": { | ||||||
|  |                             "const": "tilt_position" | ||||||
|  |                           }, | ||||||
|  |                           "service": { | ||||||
|  |                             "const": "cover.set_tilt_position" | ||||||
|  |                           }, | ||||||
|  |                           "data_attribute": { | ||||||
|  |                             "const": "tilt_position" | ||||||
|  |                           }, | ||||||
|  |                           "min": { | ||||||
|  |                             "const": 0 | ||||||
|  |                           }, | ||||||
|  |                           "max": { | ||||||
|  |                             "const": 100 | ||||||
|  |                           } | ||||||
|  |                         } | ||||||
|  |                       }, | ||||||
|  |                       "else": { | ||||||
|  |                         "if": { | ||||||
|  |                           "properties": { | ||||||
|  |                             "entity": { | ||||||
|  |                               "pattern": "^media_player\\.[^.]+$" | ||||||
|  |                             } | ||||||
|  |                           } | ||||||
|  |                         }, | ||||||
|  |                         "then": { | ||||||
|  |                           "properties": { | ||||||
|  |                             "attribute": { | ||||||
|  |                               "const": "volume_level" | ||||||
|  |                             }, | ||||||
|  |                             "service": { | ||||||
|  |                               "const": "media_player.volume_set" | ||||||
|  |                             }, | ||||||
|  |                             "data_attribute": { | ||||||
|  |                               "const": "volume_level" | ||||||
|  |                             }, | ||||||
|  |                             "min": { | ||||||
|  |                               "const": 0 | ||||||
|  |                             }, | ||||||
|  |                             "max": { | ||||||
|  |                               "const": 1 | ||||||
|  |                             } | ||||||
|  |                           } | ||||||
|  |                         }, | ||||||
|  |                         "else": { | ||||||
|  |                           "if": { | ||||||
|  |                             "properties": { | ||||||
|  |                               "entity": { | ||||||
|  |                                 "pattern": "^climate\\.[^.]+$" | ||||||
|  |                               } | ||||||
|  |                             } | ||||||
|  |                           }, | ||||||
|  |                           "then": { | ||||||
|  |                             "properties": { | ||||||
|  |                               "attribute": { | ||||||
|  |                                 "const": "temperature" | ||||||
|  |                               }, | ||||||
|  |                               "service": { | ||||||
|  |                                 "const": "climate.set_temperature" | ||||||
|  |                               }, | ||||||
|  |                               "data_attribute": { | ||||||
|  |                                 "const": "temperature" | ||||||
|  |                               } | ||||||
|  |                             } | ||||||
|  |                           } | ||||||
|  |                         } | ||||||
|  |                       } | ||||||
|  |                     } | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|  |     }, | ||||||
|     "type": { |     "type": { | ||||||
|       "title": "Menu item type", |       "title": "Menu item type", | ||||||
|       "description": "One of 'info', 'tap', 'toggle' or 'group'." |       "description": "One of 'info', 'tap', 'toggle' or 'group'." | ||||||
| @@ -220,21 +553,210 @@ | |||||||
|     "items": { |     "items": { | ||||||
|       "type": "array", |       "type": "array", | ||||||
|       "items": { |       "items": { | ||||||
|         "oneOf": [ |         "examples": [ | ||||||
|           { |           { | ||||||
|  |             "type": "tap", | ||||||
|  |             "name": "Example", | ||||||
|  |             "tap_action": { | ||||||
|  |               "service": "notify.notify", | ||||||
|  |               "data": { | ||||||
|  |                 "message": "Example" | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "toggle", | ||||||
|  |             "name": "Example", | ||||||
|  |             "entity": "switch.example" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "group", | ||||||
|  |             "name": "Example", | ||||||
|  |             "title": "Example", | ||||||
|  |             "items": [] | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "numeric", | ||||||
|  |             "name": "Example", | ||||||
|  |             "entity": "light.example", | ||||||
|  |             "attribute": "brightness", | ||||||
|  |             "service": "light.turn_on", | ||||||
|  |             "data_attribute": "brightness", | ||||||
|  |             "min": 0, | ||||||
|  |             "max": 255, | ||||||
|  |             "step": 1 | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "numeric", | ||||||
|  |             "name": "Example", | ||||||
|  |             "entity": "input_number.example", | ||||||
|  |             "service": "input_number.set_value", | ||||||
|  |             "data_attribute": "value", | ||||||
|  |             "min": 0, | ||||||
|  |             "max": 100, | ||||||
|  |             "step": 1 | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "numeric", | ||||||
|  |             "name": "Example", | ||||||
|  |             "entity": "number.example", | ||||||
|  |             "service": "number.set_value", | ||||||
|  |             "data_attribute": "value", | ||||||
|  |             "min": 0, | ||||||
|  |             "max": 100, | ||||||
|  |             "step": 1 | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "numeric", | ||||||
|  |             "name": "Example", | ||||||
|  |             "entity": "fan.example", | ||||||
|  |             "attribute": "percentage", | ||||||
|  |             "service": "fan.set_percentage", | ||||||
|  |             "data_attribute": "percentage", | ||||||
|  |             "min": 0, | ||||||
|  |             "max": 100, | ||||||
|  |             "step": 1, | ||||||
|  |             "display_format": "%.0f%%" | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "numeric", | ||||||
|  |             "name": "Example", | ||||||
|  |             "entity": "valve.example", | ||||||
|  |             "attribute": "position", | ||||||
|  |             "service": "valve.set_valve_position", | ||||||
|  |             "data_attribute": "position", | ||||||
|  |             "min": 0, | ||||||
|  |             "max": 100, | ||||||
|  |             "step": 1 | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "numeric", | ||||||
|  |             "name": "Example", | ||||||
|  |             "entity": "cover.example", | ||||||
|  |             "attribute": "position", | ||||||
|  |             "service": "cover.set_position", | ||||||
|  |             "data_attribute": "position", | ||||||
|  |             "min": 0, | ||||||
|  |             "max": 100, | ||||||
|  |             "step": 1 | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "numeric", | ||||||
|  |             "name": "Example", | ||||||
|  |             "entity": "cover.example", | ||||||
|  |             "attribute": "tilt_position", | ||||||
|  |             "service": "cover.set_tilt_position", | ||||||
|  |             "data_attribute": "tilt_position", | ||||||
|  |             "min": 0, | ||||||
|  |             "max": 100, | ||||||
|  |             "step": 1 | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "numeric", | ||||||
|  |             "name": "Example", | ||||||
|  |             "entity": "media_player.example", | ||||||
|  |             "attribute": "volume_level", | ||||||
|  |             "service": "media_player.volume_set", | ||||||
|  |             "data_attribute": "volume_level", | ||||||
|  |             "min": 0, | ||||||
|  |             "max": 1, | ||||||
|  |             "step": 0.01 | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "type": "numeric", | ||||||
|  |             "name": "Example", | ||||||
|  |             "entity": "climate.example", | ||||||
|  |             "attribute": "temperature", | ||||||
|  |             "service": "climate.set_temperature", | ||||||
|  |             "data_attribute": "temperature" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "allOf": [ | ||||||
|  |           { | ||||||
|  |             "properties": { | ||||||
|  |               "type": { | ||||||
|  |                 "enum": [ | ||||||
|  |                   "toggle", | ||||||
|  |                   "template", | ||||||
|  |                   "tap", | ||||||
|  |                   "info", | ||||||
|  |                   "group", | ||||||
|  |                   "numeric" | ||||||
|  |                 ] | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "if": { | ||||||
|  |               "properties": { | ||||||
|  |                 "type": { | ||||||
|  |                   "const": "toggle" | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             }, | ||||||
|  |             "then": { | ||||||
|               "$ref": "#/$defs/toggle" |               "$ref": "#/$defs/toggle" | ||||||
|             }, |             }, | ||||||
|           { |             "else": { | ||||||
|  |               "if": { | ||||||
|  |                 "properties": { | ||||||
|  |                   "type": { | ||||||
|  |                     "const": "template" | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               }, | ||||||
|  |               "then": { | ||||||
|                 "$ref": "#/$defs/template" |                 "$ref": "#/$defs/template" | ||||||
|               }, |               }, | ||||||
|           { |               "else": { | ||||||
|  |                 "if": { | ||||||
|  |                   "properties": { | ||||||
|  |                     "type": { | ||||||
|  |                       "const": "tap" | ||||||
|  |                     } | ||||||
|  |                   } | ||||||
|  |                 }, | ||||||
|  |                 "then": { | ||||||
|                   "$ref": "#/$defs/tap" |                   "$ref": "#/$defs/tap" | ||||||
|                 }, |                 }, | ||||||
|           { |                 "else": { | ||||||
|  |                   "if": { | ||||||
|  |                     "properties": { | ||||||
|  |                       "type": { | ||||||
|  |                         "const": "info" | ||||||
|  |                       } | ||||||
|  |                     } | ||||||
|  |                   }, | ||||||
|  |                   "then": { | ||||||
|                     "$ref": "#/$defs/info" |                     "$ref": "#/$defs/info" | ||||||
|                   }, |                   }, | ||||||
|           { |                   "else": { | ||||||
|  |                     "if": { | ||||||
|  |                       "properties": { | ||||||
|  |                         "type": { | ||||||
|  |                           "const": "group" | ||||||
|  |                         } | ||||||
|  |                       } | ||||||
|  |                     }, | ||||||
|  |                     "then": { | ||||||
|                       "$ref": "#/$defs/group" |                       "$ref": "#/$defs/group" | ||||||
|  |                     }, | ||||||
|  |                     "else": { | ||||||
|  |                       "if": { | ||||||
|  |                         "properties": { | ||||||
|  |                           "type": { | ||||||
|  |                             "const": "numeric" | ||||||
|  |                           } | ||||||
|  |                         } | ||||||
|  |                       }, | ||||||
|  |                       "then": { | ||||||
|  |                         "$ref": "#/$defs/numeric" | ||||||
|  |                       } | ||||||
|  |                     } | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             } | ||||||
|           } |           } | ||||||
|         ] |         ] | ||||||
|       } |       } | ||||||
| @@ -258,22 +780,51 @@ | |||||||
|       "title": "Action", |       "title": "Action", | ||||||
|       "description": "'confirm' field is optional.", |       "description": "'confirm' field is optional.", | ||||||
|       "properties": { |       "properties": { | ||||||
|         "service": { |         "picker": { | ||||||
|           "$ref": "#/$defs/service" |           "$ref": "#/$defs/picker" | ||||||
|         }, |         }, | ||||||
|         "confirm": { |         "confirm": { | ||||||
|           "$ref": "#/$defs/confirm" |           "$ref": "#/$defs/confirm" | ||||||
|         }, |         }, | ||||||
|         "pin": { |         "pin": { | ||||||
|           "$ref": "#/$defs/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"] |     "picker": { | ||||||
|  |       "type": "object", | ||||||
|  |       "title": "Number picker configuration", | ||||||
|  |       "description": "'attribute' field is optional.", | ||||||
|  |       "properties": { | ||||||
|  |         "min": { | ||||||
|  |           "type": "number", | ||||||
|  |           "title": "Minimum Value" | ||||||
|  |         }, | ||||||
|  |         "max": { | ||||||
|  |           "type": "number", | ||||||
|  |           "title": "Maximum Value" | ||||||
|  |         }, | ||||||
|  |         "step": { | ||||||
|  |           "type": "number", | ||||||
|  |           "title": "Step Size" | ||||||
|  |         }, | ||||||
|  |         "attribute": { | ||||||
|  |           "type": "string", | ||||||
|  |           "title": "Attribute on the entity", | ||||||
|  |           "description": "Attribute on the entity with the current numeric value. To use the state of the entity, do not specify." | ||||||
|  |         }, | ||||||
|  |         "data_attribute": { | ||||||
|  |           "type": "string", | ||||||
|  |           "title": "Attribute on the service data", | ||||||
|  |           "description": "Attribute on the service data for the value to set." | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "required": [ | ||||||
|  |         "min", | ||||||
|  |         "max", | ||||||
|  |         "step", | ||||||
|  |         "data_attribute" | ||||||
|  |       ] | ||||||
|     }, |     }, | ||||||
|     "content": { |     "content": { | ||||||
|       "title": "Home Assistant Template", |       "title": "Home Assistant Template", | ||||||
| @@ -307,7 +858,10 @@ | |||||||
|               "$ref": "#/$defs/content" |               "$ref": "#/$defs/content" | ||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|           "required": ["type", "content"] |           "required": [ | ||||||
|  |             "type", | ||||||
|  |             "content" | ||||||
|  |           ] | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|           "properties": { |           "properties": { | ||||||
| @@ -317,7 +871,9 @@ | |||||||
|               "const": "status" |               "const": "status" | ||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|           "required": ["type"] |           "required": [ | ||||||
|  |             "type" | ||||||
|  |           ] | ||||||
|         } |         } | ||||||
|       ] |       ] | ||||||
|     }, |     }, | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ | |||||||
|     Use "Monkey C: Edit Application" from the Visual Studio Code command palette |     Use "Monkey C: Edit Application" from the Visual Studio Code command palette | ||||||
|     to update the application attributes. |     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 |       Use the following from the Visual Studio Code command palette to edit | ||||||
|       the build targets: |       the build targets: | ||||||
| @@ -44,7 +44,6 @@ | |||||||
|             <iq:product id="d2mach1"/> |             <iq:product id="d2mach1"/> | ||||||
|             <iq:product id="descentg1"/> |             <iq:product id="descentg1"/> | ||||||
|             <iq:product id="descentg2"/> |             <iq:product id="descentg2"/> | ||||||
|       <iq:product id="descentmk1"/> |  | ||||||
|             <iq:product id="descentmk2"/> |             <iq:product id="descentmk2"/> | ||||||
|             <iq:product id="descentmk2s"/> |             <iq:product id="descentmk2s"/> | ||||||
|             <iq:product id="descentmk343mm"/> |             <iq:product id="descentmk343mm"/> | ||||||
| @@ -70,6 +69,7 @@ | |||||||
|             <iq:product id="epix2"/> |             <iq:product id="epix2"/> | ||||||
|             <iq:product id="epix2pro42mm"/> |             <iq:product id="epix2pro42mm"/> | ||||||
|             <iq:product id="epix2pro47mm"/> |             <iq:product id="epix2pro47mm"/> | ||||||
|  |             <iq:product id="epix2pro47mmsystem7preview"/> | ||||||
|             <iq:product id="epix2pro51mm"/> |             <iq:product id="epix2pro51mm"/> | ||||||
|             <iq:product id="fenix5"/> |             <iq:product id="fenix5"/> | ||||||
|             <iq:product id="fenix5plus"/> |             <iq:product id="fenix5plus"/> | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ class HomeAssistantApp extends Application.AppBase { | |||||||
|     private var mGlanceTimer    as Timer.Timer?; |     private var mGlanceTimer    as Timer.Timer?; | ||||||
|     private var mUpdateTimer    as Timer.Timer?; |     private var mUpdateTimer    as Timer.Timer?; | ||||||
|     // Array initialised by onReturnFetchMenuConfig() |     // 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 mIsApp          as Lang.Boolean     = false; // Or Widget | ||||||
|     private var mUpdating       as Lang.Boolean     = false; // Don't start a second chain of updates |     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 |     private var mTemplates      as Lang.Dictionary? = null;  // Cache of compiled templates | ||||||
| @@ -629,6 +629,12 @@ class HomeAssistantApp extends Application.AppBase { | |||||||
|                             if (item instanceof HomeAssistantToggleMenuItem) { |                             if (item instanceof HomeAssistantToggleMenuItem) { | ||||||
|                                 (item as HomeAssistantToggleMenuItem).updateToggleState(data[i.toString() + "t"]); |                                 (item as HomeAssistantToggleMenuItem).updateToggleState(data[i.toString() + "t"]); | ||||||
|                             } |                             } | ||||||
|  |                             if (item instanceof HomeAssistantNumericMenuItem) { | ||||||
|  |                                var s = data[i.toString() + "n"]; | ||||||
|  |                                if ((s instanceof Lang.Number) or (s instanceof Lang.Float)) { | ||||||
|  |                                    (item as HomeAssistantNumericMenuItem).setValue(s); | ||||||
|  |                                } | ||||||
|  |                             } | ||||||
|                         } |                         } | ||||||
|                         if (Settings.getMenuCheck() && Settings.getCacheConfig() && !mIsCacheChecked) { |                         if (Settings.getMenuCheck() && Settings.getCacheConfig() && !mIsCacheChecked) { | ||||||
|                             // We are caching the menu configuration, so let's fetch it and check if its been updated. |                             // We are caching the menu configuration, so let's fetch it and check if its been updated. | ||||||
| @@ -723,6 +729,11 @@ class HomeAssistantApp extends Application.AppBase { | |||||||
|                                 "template" => (item as HomeAssistantToggleMenuItem).getToggleTemplate() |                                 "template" => (item as HomeAssistantToggleMenuItem).getToggleTemplate() | ||||||
|                             }); |                             }); | ||||||
|                         } |                         } | ||||||
|  |                         if (item instanceof HomeAssistantNumericMenuItem) { | ||||||
|  |                             mTemplates.put(i.toString() + "n", { | ||||||
|  |                                 "template" => (item as HomeAssistantNumericMenuItem).getNumericTemplate() | ||||||
|  |                             }); | ||||||
|  |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 // https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates |                 // https://developers.home-assistant.io/docs/api/native-app-integration/sending-data/#render-templates | ||||||
| @@ -822,7 +833,7 @@ class HomeAssistantApp extends Application.AppBase { | |||||||
|         var phoneConnected = System.getDeviceSettings().phoneConnected; |         var phoneConnected = System.getDeviceSettings().phoneConnected; | ||||||
|         var connectionAvailable = System.getDeviceSettings().connectionAvailable; |         var connectionAvailable = System.getDeviceSettings().connectionAvailable; | ||||||
|  |  | ||||||
|         // System.println("API URL = " + Settings.getApiUrl()); |         // System.println("HomeAssistantApp fetchApiStatus(): API URL = " + Settings.getApiUrl()); | ||||||
|         if (Settings.getApiUrl().equals("")) { |         if (Settings.getApiUrl().equals("")) { | ||||||
|             mApiStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String; |             mApiStatus = WatchUi.loadResource($.Rez.Strings.Unconfigured) as Lang.String; | ||||||
|             WatchUi.requestUpdate(); |             WatchUi.requestUpdate(); | ||||||
|   | |||||||
| @@ -148,7 +148,43 @@ 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?, | ||||||
|  |         service   as Lang.String?, | ||||||
|  |         picker    as Lang.Dictionary, | ||||||
|  |         options   as { | ||||||
|  |             :exit    as Lang.Boolean, | ||||||
|  |             :confirm as Lang.Boolean, | ||||||
|  |             :pin     as Lang.Boolean, | ||||||
|  |             :icon    as WatchUi.Bitmap | ||||||
|  |         } | ||||||
|  |     ) as WatchUi.MenuItem { | ||||||
|  |         var data = null; | ||||||
|  |         if (entity_id != null) { | ||||||
|  |             data = { "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, | ||||||
|  |             service, | ||||||
|  |             data, | ||||||
|  |             picker, | ||||||
|  |             options, | ||||||
|  |             mHomeAssistantService | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|     //! Group menu item. |     //! Group menu item. | ||||||
|     //! |     //! | ||||||
|     //! @param definition Items array from the JSON that defines this sub menu. |     //! @param definition Items array from the JSON that defines this sub menu. | ||||||
|   | |||||||
							
								
								
									
										100
									
								
								source/HomeAssistantNumericFactory.mc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								source/HomeAssistantNumericFactory.mc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | |||||||
|  | //----------------------------------------------------------------------------------- | ||||||
|  | // | ||||||
|  | // 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 & @thmichel, 13 October 2025 | ||||||
|  | // | ||||||
|  | //------------------------------------------------------------ | ||||||
|  |  | ||||||
|  | using Toybox.Graphics; | ||||||
|  | using Toybox.Lang; | ||||||
|  | using 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 = "%d"; | ||||||
|  |  | ||||||
|  |     //! Class Constructor | ||||||
|  |     // | ||||||
|  |     public function initialize(picker as Lang.Dictionary) { | ||||||
|  |         PickerFactory.initialize(); | ||||||
|  |  | ||||||
|  |         // Get values from data | ||||||
|  |         var val = picker["min"]; | ||||||
|  |         if (val != null) { | ||||||
|  |             mStart = val.toString().toFloat(); | ||||||
|  |         } | ||||||
|  |         val = picker["max"]; | ||||||
|  |         if (val != null) { | ||||||
|  |             mStop = val.toString().toFloat(); | ||||||
|  |         }  | ||||||
|  |         val = picker["step"]; | ||||||
|  |         if (val != null) { | ||||||
|  |             mStep = val.toString().toFloat(); | ||||||
|  |         }  | ||||||
|  |         if (mStep > 0.0) { | ||||||
|  |             var s = mStep; | ||||||
|  |             var dp = 0; | ||||||
|  |             while (s < 1.0) { | ||||||
|  |                 s *= 10; | ||||||
|  |                 dp++; | ||||||
|  |                 // Assigned inside the loop and in each iteration to avoid clobbering the default '%d'. | ||||||
|  |                 mFormatString = "%." + dp.toString() + "f"; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             // The JSON menu definition defined a step size of 0, revert to the default. | ||||||
|  |             mStep = 1.0; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     //! 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 Lang.Number, | ||||||
|  |         selected as Lang.Boolean | ||||||
|  |     ) as WatchUi.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 Lang.Number) as Lang.Object? { | ||||||
|  |         return mStart + (index * mStep); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     //! Get the number of picker items | ||||||
|  |     //! | ||||||
|  |     //! @return Number of items | ||||||
|  |     // | ||||||
|  |     public function getSize() as Lang.Number { | ||||||
|  |         return ((mStop - mStart) / mStep).toNumber() + 1; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										247
									
								
								source/HomeAssistantNumericMenuItem.mc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								source/HomeAssistantNumericMenuItem.mc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,247 @@ | |||||||
|  | //----------------------------------------------------------------------------------- | ||||||
|  | // | ||||||
|  | // 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 & @thmichel, 13 October 2025 | ||||||
|  | // | ||||||
|  | //----------------------------------------------------------------------------------- | ||||||
|  |  | ||||||
|  | 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 mService              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 mPicker               as Lang.Dictionary?; | ||||||
|  |     private var mValue                as Lang.Number or Lang.Float = 0; | ||||||
|  |     private var mFormatString         as Lang.String = "%d"; | ||||||
|  |  | ||||||
|  |     //! Class Constructor | ||||||
|  |     //! | ||||||
|  |     //! @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 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, | ||||||
|  |         service   as Lang.String?, | ||||||
|  |         data      as Lang.Dictionary?, | ||||||
|  |         picker    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 | ||||||
|  |     ) { | ||||||
|  |         mService              = service; | ||||||
|  |         mData                 = data; | ||||||
|  |         mPicker               = picker; | ||||||
|  |         mExit                 = options[:exit]; | ||||||
|  |         mConfirm              = options[:confirm]; | ||||||
|  |         mPin                  = options[:pin]; | ||||||
|  |         mLabel                = label; | ||||||
|  |         mHomeAssistantService = haService; | ||||||
|  |  | ||||||
|  |         HomeAssistantMenuItem.initialize( | ||||||
|  |             label, | ||||||
|  |             template, | ||||||
|  |             { | ||||||
|  |                 :alignment => options[:alignment], | ||||||
|  |                 :icon      => options[:icon] | ||||||
|  |             } | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         if (picker != null) { | ||||||
|  |             var s = picker["step"]; | ||||||
|  |             if (s != null) { | ||||||
|  |                 var step = s.toFloat() as Lang.Float; | ||||||
|  |                 var dp = 0; | ||||||
|  |                 while (step < 1.0) { | ||||||
|  |                     step *= 10; | ||||||
|  |                     dp++; | ||||||
|  |                     // Assigned inside the loop and in each iteration to avoid clobbering the default '%d'. | ||||||
|  |                     mFormatString = "%." + dp.toString() + "f"; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function callService() 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    => "service", | ||||||
|  |                         :service => mService, | ||||||
|  |                         :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 { | ||||||
|  |         var dataAttribute = mPicker["data_attribute"]; | ||||||
|  |         if (dataAttribute == null) { | ||||||
|  |             //return without call service if no data attribute is set to avoid crash | ||||||
|  |             WatchUi.popView(WatchUi.SLIDE_RIGHT); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         var entity_id = mData["entity_id"]; | ||||||
|  |         if (entity_id == null) { | ||||||
|  |             //return without call service if no entity_id is set to avoid crash | ||||||
|  |             WatchUi.popView(WatchUi.SLIDE_RIGHT); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         mHomeAssistantService.call( | ||||||
|  |             mService, | ||||||
|  |             { | ||||||
|  |                 "entity_id"              => entity_id.toString(), | ||||||
|  |                 dataAttribute.toString() => mValue | ||||||
|  |             }, | ||||||
|  |             mExit | ||||||
|  |         ); | ||||||
|  |         WatchUi.popView(WatchUi.SLIDE_RIGHT); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     //! Return a numeric menu item's fetch state template. | ||||||
|  |     //! | ||||||
|  |     //! @return A string with the menu item's template definition (or null). | ||||||
|  |     // | ||||||
|  |     function getNumericTemplate() as Lang.String? { | ||||||
|  |         var entity_id = mData["entity_id"]; | ||||||
|  |         var attribute = mPicker["attribute"] as Lang.String?; | ||||||
|  |         if (entity_id == null) { | ||||||
|  |             return null; | ||||||
|  |         } else { | ||||||
|  |             if (attribute == null) { | ||||||
|  |                 return "{{states('" + entity_id.toString() + "')}}"; | ||||||
|  |             } else { | ||||||
|  |                 return "{{state_attr('" + entity_id.toString() + "','" + attribute + "')}}"; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     //! 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. | ||||||
|  |     // | ||||||
|  |     public 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) { | ||||||
|  |             // This should not happen | ||||||
|  |             setSubLabel(data); | ||||||
|  |         } else { | ||||||
|  |             // The template must return a Float on Numeric value, or the item cannot be formatted locally without error. | ||||||
|  |             setSubLabel(WatchUi.loadResource($.Rez.Strings.TemplateError) as Lang.String); | ||||||
|  |         } | ||||||
|  |         WatchUi.requestUpdate(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     //! Set the Picker's value. Needed to set new value via the Service call | ||||||
|  |     //! | ||||||
|  |     //! @param value New value to set. | ||||||
|  |     // | ||||||
|  |     public function setValue(value as Lang.Number or Lang.Float) as Void { | ||||||
|  |         mValue = value; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     //! Get the Picker's value. | ||||||
|  |     //! | ||||||
|  |     //! Needed to set new value via the Service call | ||||||
|  |     // | ||||||
|  |     public function getValue() as Lang.Number or Lang.Float { | ||||||
|  |         return mValue; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     //! Get the original 'data' field supplied by the JSON menu. | ||||||
|  |     //! | ||||||
|  |     //! @return Dictionary containing the 'data' field. | ||||||
|  |     // | ||||||
|  |     public function getData() as Lang.Dictionary { | ||||||
|  |         return mData; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Get the original 'picker' field supplied by the JSON menu. | ||||||
|  |     //! | ||||||
|  |     //! @return Dictionary containing the 'picker' field. | ||||||
|  |     // | ||||||
|  |     public function getPicker() as Lang.Dictionary { | ||||||
|  |         return mPicker; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										98
									
								
								source/HomeAssistantNumericPicker.mc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								source/HomeAssistantNumericPicker.mc
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | |||||||
|  | //----------------------------------------------------------------------------------- | ||||||
|  | // | ||||||
|  | // 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 & @thmichel, 13 October 2025 | ||||||
|  | // | ||||||
|  | //------------------------------------------------------------ | ||||||
|  |  | ||||||
|  | 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 mItem as HomeAssistantNumericMenuItem; | ||||||
|  |  | ||||||
|  |     //! Constructor | ||||||
|  |     // | ||||||
|  |     public function initialize( | ||||||
|  |         factory as HomeAssistantNumericFactory, | ||||||
|  |         haItem  as HomeAssistantNumericMenuItem | ||||||
|  |     ) { | ||||||
|  |         mItem      = haItem; | ||||||
|  |         var picker = mItem.getPicker(); | ||||||
|  |         var min    = (picker.get("min") as Lang.String).toFloat(); | ||||||
|  |         var step   = (picker.get("step") as Lang.String).toFloat(); | ||||||
|  |         var val    = haItem.getValue(); | ||||||
|  |  | ||||||
|  |         if (min == null) { | ||||||
|  |             min = 0.0; | ||||||
|  |         } | ||||||
|  |         if (step == null) { | ||||||
|  |             step = 1.0; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         WatchUi.Picker.initialize({ | ||||||
|  |             :title    => new WatchUi.Text({ | ||||||
|  |                 :text => haItem.getLabel(), | ||||||
|  |                 :locX => WatchUi.LAYOUT_HALIGN_CENTER, | ||||||
|  |                 :locY => WatchUi.LAYOUT_VALIGN_BOTTOM | ||||||
|  |             }), | ||||||
|  |             :pattern  => [factory], | ||||||
|  |             :defaults => [((val - min) / step).toNumber()] | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     //! Called when the user has completed picking. | ||||||
|  |     //! | ||||||
|  |     //! @param value Value user selected | ||||||
|  |     //! @return true if user is done, false otherwise | ||||||
|  |     // | ||||||
|  |     public function onConfirm(value as Lang.Number or Lang.Float) as Void { | ||||||
|  |         mItem.setValue(value); | ||||||
|  |         mItem.callService(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | //! 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 { | ||||||
|  |         mPicker.onConfirm(values[0]); | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -128,7 +128,7 @@ 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. | ||||||
|     // |     // | ||||||
|     function onConfirm(b as Lang.Boolean) as Void { |     public function onConfirm(b as Lang.Boolean) as Void { | ||||||
|         if (mService != null) { |         if (mService != null) { | ||||||
|             mHomeAssistantService.call(mService, mData, mExit); |             mHomeAssistantService.call(mService, mData, mExit); | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -126,6 +126,24 @@ class HomeAssistantView extends WatchUi.Menu2 { | |||||||
|                                 } |                                 } | ||||||
|                             )); |                             )); | ||||||
|                         } |                         } | ||||||
|  |                     } else if (type.equals("numeric") && service != null) { | ||||||
|  |                         if (tap_action != null) { | ||||||
|  |                             var picker = tap_action.get("picker") as Lang.Dictionary?; | ||||||
|  |                             if (picker != null) { | ||||||
|  |                                 addItem(HomeAssistantMenuItemFactory.create().numeric( | ||||||
|  |                                     name, | ||||||
|  |                                     entity, | ||||||
|  |                                     content, | ||||||
|  |                                     service, | ||||||
|  |                                     picker, | ||||||
|  |                                     { | ||||||
|  |                                         :exit    => exit, | ||||||
|  |                                         :confirm => confirm, | ||||||
|  |                                         :pin     => pin | ||||||
|  |                                     } | ||||||
|  |                                 )); | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|                     } else if (type.equals("info") && content != null) { |                     } else if (type.equals("info") && content != null) { | ||||||
|                         // Cannot exit from a non-actionable information only menu item. |                         // Cannot exit from a non-actionable information only menu item. | ||||||
|                         addItem(HomeAssistantMenuItemFactory.create().tap( |                         addItem(HomeAssistantMenuItemFactory.create().tap( | ||||||
| @@ -154,7 +172,7 @@ class HomeAssistantView extends WatchUi.Menu2 { | |||||||
|     //! |     //! | ||||||
|     //! @return An array of menu items that need to be updated periodically to reflect the latest Home Assistant state. |     //! @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 fullList = []; | ||||||
|         var lmi = mItems as Lang.Array<WatchUi.MenuItem>; |         var lmi = mItems as Lang.Array<WatchUi.MenuItem>; | ||||||
|  |  | ||||||
| @@ -167,6 +185,12 @@ class HomeAssistantView extends WatchUi.Menu2 { | |||||||
|                     fullList.add(item); |                     fullList.add(item); | ||||||
|                 } |                 } | ||||||
|                 fullList.addAll(item.getMenuView().getItemsToUpdate()); |                 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) { |             } else if (item instanceof HomeAssistantToggleMenuItem) { | ||||||
|                 fullList.add(item); |                 fullList.add(item); | ||||||
|             } else if (item instanceof HomeAssistantTapMenuItem) { |             } else if (item instanceof HomeAssistantTapMenuItem) { | ||||||
| @@ -183,8 +207,8 @@ class HomeAssistantView extends WatchUi.Menu2 { | |||||||
|     //! Called when this View is brought to the foreground. Restore |     //! Called when this View is brought to the foreground. Restore | ||||||
|     //! the state of this View and prepare it to be shown. This includes |     //! the state of this View and prepare it to be shown. This includes | ||||||
|     //! loading resources into memory. |     //! loading resources into memory. | ||||||
|  |     // | ||||||
|     function onShow() as Void {} |     function onShow() as Void {} | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| //! Delegate for the HomeAssistantView. | //! Delegate for the HomeAssistantView. | ||||||
| @@ -248,12 +272,17 @@ class HomeAssistantViewDelegate extends WatchUi.Menu2InputDelegate { | |||||||
|             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.callService(); | ||||||
|  |         } 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 as HomeAssistantNumericMenuItem).getPicker()); | ||||||
|  |             var mPicker         = new HomeAssistantNumericPicker(mPickerFactory,haItem); | ||||||
|  |             var mPickerDelegate = new HomeAssistantNumericPickerDelegate(mPicker); | ||||||
|  |             WatchUi.pushView(mPicker,mPickerDelegate,WatchUi.SLIDE_LEFT); | ||||||
|         } else if (item instanceof HomeAssistantGroupMenuItem) { |         } else if (item instanceof HomeAssistantGroupMenuItem) { | ||||||
|             var haMenuItem = item as HomeAssistantGroupMenuItem; |             var haMenuItem = item as HomeAssistantGroupMenuItem; | ||||||
|             // System.println("IconMenu: " + haMenuItem.getLabel() + " " + haMenuItem.getId()); |  | ||||||
|             WatchUi.pushView(haMenuItem.getMenuView(), new HomeAssistantViewDelegate(false), WatchUi.SLIDE_LEFT); |             WatchUi.pushView(haMenuItem.getMenuView(), new HomeAssistantViewDelegate(false), WatchUi.SLIDE_LEFT); | ||||||
|         // } else { |  | ||||||
|         //     System.println(item.getLabel() + " " + item.getId()); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user