Skip to content

Commit 0f9778a

Browse files
committed
Adding support for Indigo reporting of battery levels.
1 parent 4b702fb commit 0f9778a

3 files changed

Lines changed: 129 additions & 38 deletions

File tree

HomeAssistantAgent.indigoPlugin/Contents/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<plist version="1.0">
44
<dict>
55
<key>PluginVersion</key>
6-
<string>2025.1.7</string>
6+
<string>2026.0.1</string>
77
<key>ServerApiVersion</key>
88
<string>3.4</string>
99
<key>CFBundleDisplayName</key>

HomeAssistantAgent.indigoPlugin/Contents/Server Plugin/Devices.xml

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
<Label>Entity:</Label>
1616
<List class="self" method="get_entity_list" filter="climate" dynamicReload="true"/>
1717
<CallbackMethod>menuChanged</CallbackMethod>
18+
</Field>
19+
<Field id="SupportsBatteryLevel" type="checkbox" defaultValue="false">
20+
<Label>Device reports battery status:</Label>
21+
</Field>
22+
<Field id="battery_entity" type="menu" visibleBindingId="SupportsBatteryLevel" visibleBindingValue="true">
23+
<Label>Battery Entity:</Label>
24+
<List class="self" method="get_entity_list" filter="sensor" dynamicReload="true"/>
1825
</Field>
1926
</ConfigUI>
2027
<States>
@@ -85,6 +92,13 @@
8592
<List class="self" method="get_entity_list" filter="light" dynamicReload="true"/>
8693
<CallbackMethod>menuChanged</CallbackMethod>
8794
</Field>
95+
<Field id="SupportsBatteryLevel" type="checkbox" defaultValue="false">
96+
<Label>Device reports battery status:</Label>
97+
</Field>
98+
<Field id="battery_entity" type="menu" visibleBindingId="SupportsBatteryLevel" visibleBindingValue="true">
99+
<Label>Battery Entity:</Label>
100+
<List class="self" method="get_entity_list" filter="sensor" dynamicReload="true"/>
101+
</Field>
88102
</ConfigUI>
89103
<States>
90104
<State id="supported_color_modes">
@@ -113,6 +127,13 @@
113127
<Label>Entity:</Label>
114128
<List class="self" method="get_entity_list" filter="switch" dynamicReload="true"/>
115129
<CallbackMethod>menuChanged</CallbackMethod>
130+
</Field>
131+
<Field id="SupportsBatteryLevel" type="checkbox" defaultValue="false">
132+
<Label>Device reports battery status:</Label>
133+
</Field>
134+
<Field id="battery_entity" type="menu" visibleBindingId="SupportsBatteryLevel" visibleBindingValue="true">
135+
<Label>Battery Entity:</Label>
136+
<List class="self" method="get_entity_list" filter="sensor" dynamicReload="true"/>
116137
</Field>
117138
</ConfigUI>
118139
<States>
@@ -139,6 +160,13 @@
139160
<Label>Entity:</Label>
140161
<List class="self" method="get_entity_list" filter="binary_sensor" dynamicReload="true"/>
141162
<CallbackMethod>menuChanged</CallbackMethod>
163+
</Field>
164+
<Field id="SupportsBatteryLevel" type="checkbox" defaultValue="false">
165+
<Label>Device reports battery status:</Label>
166+
</Field>
167+
<Field id="battery_entity" type="menu" visibleBindingId="SupportsBatteryLevel" visibleBindingValue="true">
168+
<Label>Battery Entity:</Label>
169+
<List class="self" method="get_entity_list" filter="sensor" dynamicReload="true"/>
142170
</Field>
143171
</ConfigUI>
144172
<States>
@@ -164,6 +192,13 @@
164192
<Label>Entity:</Label>
165193
<List class="self" method="get_entity_list" filter="sensor" dynamicReload="true"/>
166194
<CallbackMethod>menuChanged</CallbackMethod>
195+
</Field>
196+
<Field id="SupportsBatteryLevel" type="checkbox" defaultValue="false">
197+
<Label>Device reports battery status:</Label>
198+
</Field>
199+
<Field id="battery_entity" type="menu" visibleBindingId="SupportsBatteryLevel" visibleBindingValue="true">
200+
<Label>Battery Entity:</Label>
201+
<List class="self" method="get_entity_list" filter="sensor" dynamicReload="true"/>
167202
</Field>
168203
</ConfigUI>
169204
<States>
@@ -195,6 +230,13 @@
195230
<List class="self" method="get_entity_list" filter="cover" dynamicReload="true"/>
196231
<CallbackMethod>menuChanged</CallbackMethod>
197232
</Field>
233+
<Field id="SupportsBatteryLevel" type="checkbox" defaultValue="false">
234+
<Label>Device reports battery status:</Label>
235+
</Field>
236+
<Field id="battery_entity" type="menu" visibleBindingId="SupportsBatteryLevel" visibleBindingValue="true">
237+
<Label>Battery Entity:</Label>
238+
<List class="self" method="get_entity_list" filter="sensor" dynamicReload="true"/>
239+
</Field>
198240
</ConfigUI>
199241
<States>
200242
<State id="lastUpdated">
@@ -218,6 +260,13 @@
218260
<Label>Entity:</Label>
219261
<List class="self" method="get_entity_list" filter="lock" dynamicReload="true"/>
220262
<CallbackMethod>menuChanged</CallbackMethod>
263+
</Field>
264+
<Field id="SupportsBatteryLevel" type="checkbox" defaultValue="false">
265+
<Label>Device reports battery status:</Label>
266+
</Field>
267+
<Field id="battery_entity" type="menu" visibleBindingId="SupportsBatteryLevel" visibleBindingValue="true">
268+
<Label>Battery Entity:</Label>
269+
<List class="self" method="get_entity_list" filter="sensor" dynamicReload="true"/>
221270
</Field>
222271
</ConfigUI>
223272
<States>
@@ -249,13 +298,20 @@
249298
<List class="self" method="get_entity_list" filter="fan" dynamicReload="true"/>
250299
<CallbackMethod>menuChanged</CallbackMethod>
251300
</Field>
301+
<Field id="SupportsBatteryLevel" type="checkbox" defaultValue="false">
302+
<Label>Device reports battery status:</Label>
303+
</Field>
304+
<Field id="battery_entity" type="menu" visibleBindingId="SupportsBatteryLevel" visibleBindingValue="true">
305+
<Label>Battery Entity:</Label>
306+
<List class="self" method="get_entity_list" filter="sensor" dynamicReload="true"/>
307+
</Field>
252308
</ConfigUI>
253309
<States>
254310
<State id="lastUpdated">
255311
<ValueType>String</ValueType>
256312
<TriggerLabel>lastUpdated</TriggerLabel>
257313
<ControlPageLabel>lastUpdated</ControlPageLabel>
258-
</State>
314+
</State>`````````````````````````````````````````````````````````````````````
259315
<State id="actual_state">
260316
<ValueType>String</ValueType>
261317
<TriggerLabel>Actual State</TriggerLabel>
@@ -272,6 +328,13 @@
272328
<Label>Entity:</Label>
273329
<List class="self" method="get_entity_list" filter="media_player" dynamicReload="true"/>
274330
<CallbackMethod>menuChanged</CallbackMethod>
331+
</Field>
332+
<Field id="SupportsBatteryLevel" type="checkbox" defaultValue="false">
333+
<Label>Device reports battery status:</Label>
334+
</Field>
335+
<Field id="battery_entity" type="menu" visibleBindingId="SupportsBatteryLevel" visibleBindingValue="true">
336+
<Label>Battery Entity:</Label>
337+
<List class="self" method="get_entity_list" filter="sensor" dynamicReload="true"/>
275338
</Field>
276339
</ConfigUI>
277340
<States>
@@ -301,6 +364,13 @@
301364
<Label>Entity:</Label>
302365
<List class="self" method="get_entity_list" filter="generic" dynamicReload="true"/>
303366
<CallbackMethod>menuChanged</CallbackMethod>
367+
</Field>
368+
<Field id="SupportsBatteryLevel" type="checkbox" defaultValue="false">
369+
<Label>Device reports battery status:</Label>
370+
</Field>
371+
<Field id="battery_entity" type="menu" visibleBindingId="SupportsBatteryLevel" visibleBindingValue="true">
372+
<Label>Battery Entity:</Label>
373+
<List class="self" method="get_entity_list" filter="sensor" dynamicReload="true"/>
304374
</Field>
305375
</ConfigUI>
306376
<States>

HomeAssistantAgent.indigoPlugin/Contents/Server Plugin/plugin.py

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs):
212212
self.last_sent_id = 0
213213
self.ha_entity_map = {}
214214
self.message_queue = Queue()
215+
self.battery_entities = {}
215216

216217
########################################
217218

@@ -264,11 +265,22 @@ def deviceStartComm(self, device):
264265

265266
self.logger.info(f"{device.name}: Starting Agent device for entity '{device.address}'")
266267
self.entity_devices[device.address] = device.id
267-
parts = device.address.split('.')
268+
269+
# check for battery support, add relationship to list
270+
271+
if device.pluginProps.get("SupportsBatteryLevel"):
272+
battery_entity = device.pluginProps.get("battery_entity")
273+
self.logger.debug(f"{device.name}: {device.address} uses battery entity {battery_entity}")
274+
self.battery_entities[device.id] = battery_entity
275+
self.logger.debug(f"Battery entity list: {self.battery_entities}")
276+
277+
# get entity info if it's already saved
278+
279+
entity_type, entity_name = device.address.split('.')
268280
try:
269-
entity = self.ha_entity_map[parts[0]][parts[1]]
281+
entity = self.ha_entity_map[entity_type][entity_name]
270282
except Exception as _err:
271-
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{parts[0]}][{parts[1]}]")
283+
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{entity_type}[{entity_name}]")
272284
return
273285

274286
new_props = device.pluginProps
@@ -418,8 +430,7 @@ def get_entity_type_list(self, filter="", valuesDict=None, typeId="", targetId=0
418430
self.logger.debug(f"get_entity_type_list: {retList = }")
419431
return retList
420432

421-
def get_entity_list(self, filter="", valuesDict=None, typeId="", targetId=0):
422-
self.logger.debug(f"get_entity_list: {filter = }, {typeId = }, {valuesDict = }, {targetId = }")
433+
def get_entity_list(self, filter="", valuesDict=None, _typeId="", _targetId=0):
423434
retList = []
424435

425436
if filter == "generic":
@@ -433,7 +444,7 @@ def get_entity_list(self, filter="", valuesDict=None, typeId="", targetId=0):
433444
else:
434445
retList.append((entity['entity_id'], entity_name))
435446
retList.sort(key=lambda tup: tup[1])
436-
self.logger.debug(f"get_entity_list for filter '{filter}': {retList = }")
447+
self.logger.threaddebug(f"get_entity_list for filter '{filter}': {retList = }")
437448
return retList
438449

439450
def menuChanged(self, valuesDict, typeId=0, devId=0):
@@ -450,20 +461,18 @@ def getDeviceStateList(self, device):
450461
return stateList
451462

452463
def entity_update(self, entity_id, entity, force_update=False):
453-
parts = entity_id.split('.')
464+
entity_type, entity_name = entity_id.split('.')
454465

455466
# check for deleted entity
456467
if entity is None:
457468
self.logger.debug(f"Removing {entity_id} from ha_entity_map")
458-
del self.ha_entity_map[parts[0]][parts[1]]
469+
del self.ha_entity_map[entity_type][entity_name]
459470
return
460471

461472
# save the entity state info in the entity map
462-
if parts[0] not in self.ha_entity_map: # create the entity_type dict if it doesn't exist
463-
self.ha_entity_map[parts[0]] = {}
464-
self.ha_entity_map[parts[0]][parts[1]] = entity # save the entity info
465-
466-
# find the matching Indigo device, update if we have one
473+
if entity_type not in self.ha_entity_map: # create the entity_type dict if it doesn't exist
474+
self.ha_entity_map[entity_type] = {}
475+
self.ha_entity_map[entity_type][entity_name] = entity # save the entity info
467476

468477
if not (device_id := self.entity_devices.get(entity_id)):
469478
return
@@ -496,6 +505,18 @@ def entity_update(self, entity_id, entity, force_update=False):
496505
else:
497506
states_list.append({'key': key, 'value': json.dumps(attributes[key])})
498507

508+
# Update battery state if needed
509+
if device.id in self.battery_entities:
510+
battery_entity_id = self.battery_entities.get(device.id)
511+
battery_entity_type, battery_entity_name= battery_entity_id.split('.')
512+
try:
513+
battery_entity = self.ha_entity_map[battery_entity_type][battery_entity_name]
514+
except KeyError:
515+
pass
516+
else:
517+
battery_value = int(battery_entity["state"])
518+
states_list.append({'key': 'batteryLevel', 'value': battery_value, 'uiValue': f'{battery_value}%'})
519+
499520
if set(old_states) != set(new_states):
500521
self.logger.threaddebug(f"{device.name}: states list changed, updating...")
501522
newProps = device.pluginProps
@@ -984,8 +1005,8 @@ def actionControlSpeedControl(self, action, device):
9841005
########################################
9851006

9861007
def log_entity(self, valuesDict, _typeId=0, _devId=0):
987-
parts = valuesDict['address'].split('.')
988-
entity = self.ha_entity_map[parts[0]][parts[1]]
1008+
entity_type, entity_name = valuesDict['address'].split('.')
1009+
entity = self.ha_entity_map[entity_type][entity_name]
9891010
self.logger.info(f"Entity info for '{valuesDict['address']}:\n{json.dumps(entity, sort_keys=True, indent=4, separators=(',', ': '))}")
9901011
return True
9911012

@@ -1021,11 +1042,11 @@ def do_climate_action(self, plugin_action, device, _callerWaitingForResult):
10211042
def hvac_mode_list(self, _filter, _values_dict, type_id, target_id):
10221043
self.logger.debug(f"hvac_mode_list: type_id = {type_id}, target_id = {target_id}")
10231044
device = indigo.devices[target_id]
1024-
parts = device.address.split('.')
1045+
entity_type, entity_name = device.address.split('.')
10251046
try:
1026-
entity = self.ha_entity_map[parts[0]][parts[1]]
1047+
entity = self.ha_entity_map[entity_type][entity_name]
10271048
except Exception as _err:
1028-
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{parts[0]}][{parts[1]}]")
1049+
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{entity_type}[{entity_name}]")
10291050
return []
10301051

10311052
if entity['attributes'].get('hvac_modes'):
@@ -1044,11 +1065,11 @@ def set_hvac_mode_action(self, plugin_action, device, _callerWaitingForResult):
10441065
def hvac_fan_mode_list(self, _filter, _values_dict, type_id, target_id):
10451066
self.logger.debug(f"fan_mode_list: type_id = {type_id}, target_id = {target_id}")
10461067
device = indigo.devices[target_id]
1047-
parts = device.address.split('.')
1068+
entity_type, entity_name = device.address.split('.')
10481069
try:
1049-
entity = self.ha_entity_map[parts[0]][parts[1]]
1070+
entity = self.ha_entity_map[entity_type][entity_name]
10501071
except Exception as _err:
1051-
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{parts[0]}][{parts[1]}]")
1072+
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{entity_type}[{entity_name}]")
10521073
return []
10531074

10541075
if entity['attributes'].get('fan_modes'):
@@ -1059,11 +1080,11 @@ def hvac_fan_mode_list(self, _filter, _values_dict, type_id, target_id):
10591080
def hvac_fan_preset_list(self, _filter, _values_dict, type_id, target_id):
10601081
self.logger.debug(f"hvac_fan_preset_list: type_id = {type_id}, target_id = {target_id}")
10611082
device = indigo.devices[target_id]
1062-
parts = device.address.split('.')
1083+
entity_type, entity_name = device.address.split('.')
10631084
try:
1064-
entity = self.ha_entity_map[parts[0]][parts[1]]
1085+
entity = self.ha_entity_map[entity_type][entity_name]
10651086
except Exception as _err:
1066-
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{parts[0]}][{parts[1]}]")
1087+
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{entity_type}[{entity_name}]")
10671088
return []
10681089

10691090
if entity['attributes'].get('preset_modes'):
@@ -1082,11 +1103,11 @@ def set_hvac_fan_mode_action(self, plugin_action, device, _callerWaitingForResul
10821103
def hvac_swing_mode_list(self, _filter, _values_dict, type_id, target_id):
10831104
self.logger.debug(f"swing_mode_list: type_id = {type_id}, target_id = {target_id}")
10841105
device = indigo.devices[target_id]
1085-
parts = device.address.split('.')
1106+
entity_type, entity_name = device.address.split('.')
10861107
try:
1087-
entity = self.ha_entity_map[parts[0]][parts[1]]
1108+
entity = self.ha_entity_map[entity_type][entity_name]
10881109
except Exception as _err:
1089-
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{parts[0]}][{parts[1]}]")
1110+
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{entity_type}[{entity_name}]")
10901111
return []
10911112

10921113
if entity['attributes'].get('swing_modes'):
@@ -1105,11 +1126,11 @@ def set_hvac_swing_mode_action(self, plugin_action, device, _callerWaitingForRes
11051126
def hvac_preset_mode_list(self, _filter, _values_dict, type_id, target_id):
11061127
self.logger.debug(f"preset_mode_list: type_id = {type_id}, target_id = {target_id}")
11071128
device = indigo.devices[target_id]
1108-
parts = device.address.split('.')
1129+
entity_type, entity_name = device.address.split('.')
11091130
try:
1110-
entity = self.ha_entity_map[parts[0]][parts[1]]
1131+
entity = self.ha_entity_map[entity_type][entity_name]
11111132
except Exception as _err:
1112-
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{parts[0]}][{parts[1]}]")
1133+
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{entity_type}[{entity_name}]")
11131134
return []
11141135

11151136
if entity['attributes'].get('preset_modes'):
@@ -1344,11 +1365,11 @@ def media_play_set_source_action(self, plugin_action, device, _callerWaitingForR
13441365
def media_player_source_list(self, _filter, _values_dict, type_id, target_id):
13451366
self.logger.debug(f"media_player_source_list: type_id = {type_id}, target_id = {target_id}")
13461367
device = indigo.devices[target_id]
1347-
parts = device.address.split('.')
1368+
entity_type, entity_name = device.address.split('.')
13481369
try:
1349-
entity = self.ha_entity_map[parts[0]][parts[1]]
1370+
entity = self.ha_entity_map[entity_type][entity_name]
13501371
except Exception as _err:
1351-
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{parts[0]}][{parts[1]}]")
1372+
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{entity_type}[{entity_name}]")
13521373
return []
13531374

13541375
if entity['attributes'].get('source_list'):
@@ -1369,11 +1390,11 @@ def media_play_set_mode_action(self, plugin_action, device, _callerWaitingForRes
13691390
def media_player_mode_list(self, _filter, _values_dict, type_id, target_id):
13701391
self.logger.debug(f"media_player_mode_list: type_id = {type_id}, target_id = {target_id}")
13711392
device = indigo.devices[target_id]
1372-
parts = device.address.split('.')
1393+
entity_type, entity_name = device.address.split('.')
13731394
try:
1374-
entity = self.ha_entity_map[parts[0]][parts[1]]
1395+
entity = self.ha_entity_map[entity_type][entity_name]
13751396
except Exception as _err:
1376-
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{parts[0]}][{parts[1]}]")
1397+
self.logger.debug(f"{device.name}: {device.address} not in ha_entity_map[{entity_type}[{entity_name}]")
13771398
return []
13781399

13791400
if entity['attributes'].get('sound_mode_list'):

0 commit comments

Comments
 (0)