Skip to content

Commit 5034c99

Browse files
Fix/entity renaming (#8)
* Fix device renaming on role change, plus tidy up signals by moving them to const.py * Centralise role update message sending * Immediately register vhh solar if starting with with_solar==True * Change solar sensor emoji * Trigger solar sensor detection if from reconfigure * Clean up and simplify sensor addition and role handling --------- Co-authored-by: Jade Mattsson <github@frozenlogic.org>
1 parent 8efe500 commit 5034c99

File tree

8 files changed

+183
-146
lines changed

8 files changed

+183
-146
lines changed

custom_components/powersensor/PowersensorDiscoveryService.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
from homeassistant.helpers.dispatcher import async_dispatcher_send
77
import homeassistant.components.zeroconf
88

9-
from .const import DOMAIN
9+
from .const import (
10+
ZEROCONF_ADD_PLUG_SIGNAL,
11+
ZEROCONF_REMOVE_PLUG_SIGNAL,
12+
ZEROCONF_UPDATE_PLUG_SIGNAL,
13+
)
1014

1115
import logging
1216
_LOGGER = logging.getLogger(__name__)
@@ -30,7 +34,7 @@ def add_service(self, zc, type_, name):
3034
)
3135

3236
async def _async_service_add(self, *args):
33-
async_dispatcher_send(self._hass, f"{DOMAIN}_zeroconf_add_plug", *args)
37+
async_dispatcher_send(self._hass, ZEROCONF_ADD_PLUG_SIGNAL, *args)
3438

3539

3640
async def _async_delayed_remove(self, name):
@@ -68,7 +72,7 @@ def remove_service(self, zc, type_, name):
6872
)
6973

7074
async def _async_service_remove(self, *args):
71-
async_dispatcher_send(self._hass, f"{DOMAIN}_zeroconf_remove_plug", *args)
75+
async_dispatcher_send(self._hass, ZEROCONF_REMOVE_PLUG_SIGNAL, *args)
7276

7377
def update_service(self, zc, type_, name):
7478
self.cancel_any_pending_removal(name, "request to update")
@@ -81,7 +85,7 @@ def update_service(self, zc, type_, name):
8185

8286
async def _async_service_update(self, *args):
8387
# remove from pending tasks if update received
84-
async_dispatcher_send(self._hass, f"{DOMAIN}_zeroconf_update_plug", *args)
88+
async_dispatcher_send(self._hass, ZEROCONF_UPDATE_PLUG_SIGNAL, *args)
8589

8690
async def _async_get_service_info(self, zc, type_, name):
8791
try:
@@ -160,4 +164,4 @@ async def stop(self):
160164
self.zc = None
161165

162166
self.browser = None
163-
self.listener = None
167+
self.listener = None

custom_components/powersensor/PowersensorEntity.py

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,18 @@
1111
from homeassistant.util.dt import utcnow
1212
from .PlugMeasurements import PlugMeasurements
1313
from .SensorMeasurements import SensorMeasurements
14-
from .const import DOMAIN, POWER_SENSOR_UPDATE_SIGNAL
14+
from .const import DATA_UPDATE_SIGNAL_FMT_MAC_EVENT, DOMAIN, ROLE_UPDATE_SIGNAL
1515

1616
import logging
1717
_LOGGER = logging.getLogger(__name__)
1818

1919
class PowersensorEntity(SensorEntity):
2020
"""Powersensor Plug Class--designed to handle all measurements of the plug--perhaps less expressive"""
21-
def __init__(self, hass: HomeAssistant, mac : str,
21+
def __init__(self, hass: HomeAssistant, mac: str, role: str,
2222
input_config: dict[Union[SensorMeasurements|PlugMeasurements], dict],
2323
measurement_type: SensorMeasurements|PlugMeasurements, timeout_seconds: int = 60):
2424
"""Initialize the sensor."""
25-
self.role = None
25+
self._role = role
2626
self._has_recently_received_update_message = False
2727
self._attr_native_value = 0.0
2828
self._hass = hass
@@ -44,7 +44,7 @@ def __init__(self, hass: HomeAssistant, mac : str,
4444
self._attr_entity_category = config.get('category', None)
4545
self._attr_state_class = config.get('state_class', None)
4646

47-
self._signal = f"{POWER_SENSOR_UPDATE_SIGNAL}_{self._mac}_{config['event']}"
47+
self._signal = DATA_UPDATE_SIGNAL_FMT_MAC_EVENT % (mac, config['event'])
4848
self._message_key = config.get('message_key', None)
4949
self._message_callback = config.get('callback', None)
5050

@@ -81,6 +81,11 @@ async def async_added_to_hass(self) -> None:
8181
self._signal,
8282
self._handle_update
8383
))
84+
self.async_on_remove(async_dispatcher_connect(
85+
self._hass,
86+
ROLE_UPDATE_SIGNAL,
87+
self._handle_role_update
88+
))
8489

8590
async def async_will_remove_from_hass(self):
8691
"""Clean up."""
@@ -91,27 +96,12 @@ def _rename_based_on_role(self):
9196
return False
9297

9398
@callback
94-
def _handle_update(self, event, message):
95-
"""handle pushed data."""
96-
97-
# event is not presently used, but is passed to maintain flexibility for future development
98-
99-
name_updated = False
100-
self._has_recently_received_update_message = True
99+
def _handle_role_update(self, mac: str, role: str):
100+
if self._mac != mac or self._role == role:
101+
return
101102

102-
role = message.get('role', None)
103-
if role is not None and role != self.role:
104-
self.role = role
105-
name_updated = self._rename_based_on_role()
106-
async_dispatcher_send(self._hass, f"{DOMAIN}_update_role", self._mac, role)
107-
108-
109-
if self._message_key in message.keys():
110-
if self._message_callback:
111-
self._attr_native_value = self._message_callback( message[self._message_key])
112-
else:
113-
self._attr_native_value = message[self._message_key]
114-
self._schedule_unavailable()
103+
self._role = role
104+
name_updated = self._rename_based_on_role()
115105

116106
if name_updated:
117107
device_registry = dr.async_get(self._hass)
@@ -131,5 +121,22 @@ def _handle_update(self, event, message):
131121
self.entity_id,
132122
name = self._attr_name
133123
)
134-
self.async_write_ha_state()
135124

125+
self.async_write_ha_state()
126+
127+
@callback
128+
def _handle_update(self, event, message):
129+
"""handle pushed data."""
130+
131+
# event is not presently used, but is passed to maintain flexibility for future development
132+
133+
self._has_recently_received_update_message = True
134+
135+
if self._message_key in message.keys():
136+
if self._message_callback:
137+
self._attr_native_value = self._message_callback( message[self._message_key])
138+
else:
139+
self._attr_native_value = message[self._message_key]
140+
self._schedule_unavailable()
141+
142+
self.async_write_ha_state()

custom_components/powersensor/PowersensorMessageDispatcher.py

Lines changed: 57 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,17 @@
99
from powersensor_local import PlugApi, VirtualHousehold
1010

1111
from custom_components.powersensor.AsyncSet import AsyncSet
12-
from custom_components.powersensor.const import POWER_SENSOR_UPDATE_SIGNAL, DOMAIN
12+
from custom_components.powersensor.const import (
13+
CREATE_PLUG_SIGNAL,
14+
CREATE_SENSOR_SIGNAL,
15+
DATA_UPDATE_SIGNAL_FMT_MAC_EVENT,
16+
PLUG_ADDED_TO_HA_SIGNAL,
17+
ROLE_UPDATE_SIGNAL,
18+
SENSOR_ADDED_TO_HA_SIGNAL,
19+
ZEROCONF_ADD_PLUG_SIGNAL,
20+
ZEROCONF_REMOVE_PLUG_SIGNAL,
21+
ZEROCONF_UPDATE_PLUG_SIGNAL,
22+
)
1323

1424
_LOGGER = logging.getLogger(__name__)
1525
class PowersensorMessageDispatcher:
@@ -25,28 +35,23 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, vhh: VirtualHousehol
2535
self._pending_removals = {}
2636
self._debounce_seconds = debounce_timeout
2737
self.has_solar = False
28-
self._virtual_household_has_been_setup = False
29-
self._last_request_to_notify_about_solar = datetime.datetime(1970,1,1,0,0,0)
3038
self._solar_request_limit = datetime.timedelta(seconds = 10)
3139
self._unsubscribe_from_signals = [
3240
async_dispatcher_connect(self._hass,
33-
f"{DOMAIN}_sensor_added_to_homeassistant",
34-
self._acknowledge_sensor_added_to_homeassistant),
35-
async_dispatcher_connect(self._hass,
36-
f"{DOMAIN}_zeroconf_add_plug",
41+
ZEROCONF_ADD_PLUG_SIGNAL,
3742
self._plug_added),
3843
async_dispatcher_connect(self._hass,
39-
f"{DOMAIN}_zeroconf_update_plug",
44+
ZEROCONF_UPDATE_PLUG_SIGNAL,
4045
self._plug_updated),
4146
async_dispatcher_connect(self._hass,
42-
f"{DOMAIN}_zeroconf_remove_plug",
47+
ZEROCONF_REMOVE_PLUG_SIGNAL,
4348
self._schedule_plug_removal),
4449
async_dispatcher_connect(self._hass,
45-
f"{DOMAIN}_plug_added_to_homeassistant",
50+
PLUG_ADDED_TO_HA_SIGNAL,
4651
self._acknowledge_plug_added_to_homeassistant),
4752
async_dispatcher_connect(self._hass,
48-
f"{DOMAIN}_solar_added_to_virtual_household",
49-
self._acknowledge_solar_added_to_virtual_household),
53+
SENSOR_ADDED_TO_HA_SIGNAL,
54+
self._acknowledge_sensor_added_to_homeassistant),
5055
]
5156

5257
self._monitor_add_plug_queue = None
@@ -78,7 +83,7 @@ async def _monitor_plug_queue(self):
7883
for mac_address, host, port, name in queue_snapshot:
7984
#@todo: maybe better to query the entity registry?
8085
if not self._plug_has_been_seen(mac_address, name):
81-
async_dispatcher_send(self._hass, f"{DOMAIN}_create_plug",
86+
async_dispatcher_send(self._hass, CREATE_PLUG_SIGNAL,
8287
mac_address, host, port, name)
8388
elif mac_address in self._known_plugs and not mac_address in self.plugs:
8489
_LOGGER.info(f"Plug with mac {mac_address} is known, but API is missing."
@@ -135,62 +140,72 @@ def _create_api(self, mac_address, ip, port, name):
135140
self._known_plugs.add(mac_address)
136141
self._known_plug_names[name] = mac_address
137142
known_evs = [
138-
'exception',
143+
#'exception',
139144
'average_flow',
140145
'average_power',
141146
'average_power_components',
142147
'battery_level',
143-
'now_relaying_for',
144148
'radio_signal_quality',
145149
'summation_energy',
146150
'summation_volume',
147-
'uncalibrated_instant_reading',
151+
#'uncalibrated_instant_reading',
148152
]
149153

150154
for ev in known_evs:
151-
api.subscribe(ev, lambda event, message: self.handle_message( event, message))
155+
api.subscribe(ev, self.handle_message)
156+
api.subscribe('now_relaying_for', self.handle_relaying_for)
152157
api.connect()
153158

154-
def add_api(self, network_info):
155-
_LOGGER.debug("Manually adding API, this could cause API's and entities to get out of sync")
156-
self._create_api(mac_address=network_info['mac'], ip=network_info['host'],
157-
port=network_info['port'], name=network_info['name'])
158-
159159
def cancel_any_pending_removal(self, mac, source):
160160
task = self._pending_removals.pop(mac, None)
161161
if task:
162162
task.cancel()
163163
_LOGGER.debug(f"Cancelled pending removal for {mac} by {source}.")
164164

165+
async def handle_relaying_for(self, event: str, message: dict):
166+
"""Handle a potentially new sensor being reported."""
167+
mac = message.get('mac', None)
168+
device_type = message.get('device_type', None)
169+
if mac is None or device_type != "sensor":
170+
_LOGGER.warning(f"Ignoring relayed device with MAC \"{mac}\" and type {device_type}")
171+
return
172+
173+
persisted_role = self._entry.data.get('roles', {}).get(mac, None)
174+
role = message.get('role', None)
175+
_LOGGER.debug(f"Relayed sensor {mac} with role {role} found")
176+
177+
if mac not in self.sensors:
178+
_LOGGER.debug(f"Reporting new sensor {mac} with role {role}")
179+
self.on_start_sensor_queue[mac] = role
180+
async_dispatcher_send(self._hass, CREATE_SENSOR_SIGNAL, mac, role)
181+
if role != persisted_role:
182+
_LOGGER.debug(f"Restoring role for {mac} from {role} to {persisted_role}")
183+
async_dispatcher_send(self._hass, ROLE_UPDATE_SIGNAL, mac, persisted_role)
184+
165185
async def handle_message(self, event: str, message: dict):
166186
mac = message['mac']
167-
role = message.get('role', None)
168-
if role is None:
169-
role = self._entry.data.get('roles', {}).get(mac, None)
170-
self.cancel_any_pending_removal(mac, "new message received from plug")
187+
persisted_role = self._entry.data.get('roles', {}).get(mac, None)
188+
role = message.get('role', persisted_role)
189+
message['role'] = role
171190

172-
if mac not in self.plugs.keys():
173-
if mac not in self.sensors:
174-
if role is not None:
175-
self.on_start_sensor_queue[mac] = role
176-
async_dispatcher_send(self._hass, f"{DOMAIN}_create_sensor", mac, role)
191+
if role != persisted_role:
192+
async_dispatcher_send(self._hass, ROLE_UPDATE_SIGNAL, mac, role)
193+
194+
self.cancel_any_pending_removal(mac, "new message received from plug")
177195

178196
# Feed the household calculations
179197
if event == 'average_power':
180198
await self._vhh.process_average_power_event(message)
181199
elif event == 'summation_energy':
182200
await self._vhh.process_summation_event(message)
183-
if role == 'solar':
184-
self.has_solar = True
185-
if not self._virtual_household_has_been_setup:
186-
new_time = datetime.datetime.now()
187-
if self._last_request_to_notify_about_solar + self._solar_request_limit <new_time:
188-
self._last_request_to_notify_about_solar = new_time
189-
_LOGGER.debug("Notifying integration that solar is present.")
190-
async_dispatcher_send(self._hass, f"{DOMAIN}_solar_sensor_detected")
191-
async_dispatcher_send(self._hass, f"{POWER_SENSOR_UPDATE_SIGNAL}_{mac}_{event}", event, message)
192-
if role is not None:
193-
async_dispatcher_send(self._hass, f"{POWER_SENSOR_UPDATE_SIGNAL}_{mac}_role", 'role', { 'role': role })
201+
202+
async_dispatcher_send(self._hass,
203+
DATA_UPDATE_SIGNAL_FMT_MAC_EVENT % (mac, event), event, message)
204+
205+
# Synthesise a role type message for the role diagnostic entity
206+
async_dispatcher_send(
207+
self._hass, DATA_UPDATE_SIGNAL_FMT_MAC_EVENT % (mac, role),
208+
'role', { 'role': role })
194209

195210
async def disconnect(self):
196211
for _ in range(len(self.plugs)):
@@ -208,10 +223,6 @@ def _acknowledge_sensor_added_to_homeassistant(self,mac, role):
208223
self.sensors[mac] = role
209224

210225
@callback
211-
def _acknowledge_solar_added_to_virtual_household(self, success):
212-
_LOGGER.debug("Solar has been added to virtual household.")
213-
self._virtual_household_has_been_setup = success
214-
215226
async def _acknowledge_plug_added_to_homeassistant(self, mac_address, host, port, name):
216227
self._create_api(mac_address, host, port, name)
217228
await self._plug_added_queue.remove((mac_address, host, port, name))

custom_components/powersensor/PowersensorPlugEntity.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@
7979

8080
class PowersensorPlugEntity(PowersensorEntity):
8181
"""Powersensor Plug Class--designed to handle all measurements of the plug--perhaps less expressive"""
82-
def __init__(self, hass: HomeAssistant, mac_address: str, measurement_type: PlugMeasurements):
82+
def __init__(self, hass: HomeAssistant, mac_address: str, role: str, measurement_type: PlugMeasurements):
8383
"""Initialize the sensor."""
84-
super().__init__(hass, mac_address, _config, measurement_type)
84+
super().__init__(hass, mac_address, role, _config, measurement_type)
8585
self._model = f"PowersensorPlug"
8686
self.measurement_type = measurement_type
8787
config = _config[measurement_type]

custom_components/powersensor/PowersensorSensorEntity.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,44 +51,43 @@
5151

5252
class PowersensorSensorEntity(PowersensorEntity):
5353
"""Powersensor Plug Class--designed to handle all measurements of the plug--perhaps less expressive"""
54-
def __init__(self, hass: HomeAssistant, mac : str,
54+
def __init__(self, hass: HomeAssistant, mac: str, role: str,
5555
measurement_type: SensorMeasurements):
5656
"""Initialize the sensor."""
57-
super().__init__(hass, mac, _config, measurement_type)
57+
super().__init__(hass, mac, role, _config, measurement_type)
5858
self._model = f"PowersensorSensor"
5959
self.measurement_type = measurement_type
6060
config = _config[measurement_type]
6161
self._measurement_name = config['name']
6262
self._device_name = self._default_device_name()
6363
self._attr_name = f"{self._device_name} {self._measurement_name}"
64-
self._rename_based_on_role()
6564

6665
@property
6766
def device_info(self) -> DeviceInfo:
6867
return {
6968
'identifiers': {(DOMAIN, self._mac)},
7069
'manufacturer': "Powersensor",
7170
'model': self._model,
72-
'name': self._device_name ,
71+
'name': self._device_name,
7372
}
7473

7574
def _ensure_matching_prefix(self):
7675
if not self._attr_name.startswith(self._device_name):
7776
self._attr_name = f"{self._device_name} {self._measurement_name }"
7877

79-
def _rename_based_on_role(self):
80-
if self._device_name == self._default_device_name():
81-
if self.role =='house-net' or self.role == "water" or self.role == "solar":
82-
role2name = {
83-
"house-net": "Powersensor Mains Sensor ⚡",
84-
"solar": "Powersensor Solar Sensor ⚡",
85-
"water": "Powersensor Water Sensor 💧",
86-
}
87-
self._device_name = role2name[self.role]
88-
self._ensure_matching_prefix()
89-
return True
90-
return False
91-
92-
def _default_device_name(self):
93-
return SENSOR_NAME_FORMAT % self._mac
78+
def _rename_based_on_role(self) -> bool:
79+
expected_name = self._default_device_name()
80+
if self._device_name != expected_name:
81+
self._device_name = expected_name
82+
self._ensure_matching_prefix()
83+
return True
84+
else:
85+
return False
9486

87+
def _default_device_name(self) -> str:
88+
role2name = {
89+
"house-net": "Powersensor Mains Sensor ⚡",
90+
"solar": "Powersensor Solar Sensor ☀️",
91+
"water": "Powersensor Water Sensor 💧",
92+
}
93+
return role2name[self._role] if self._role in [ "house-net", "water", "solar" ] else SENSOR_NAME_FORMAT % self._mac

0 commit comments

Comments
 (0)