Skip to content

Commit a5bf567

Browse files
committed
Merge branch 'diag'
2 parents 8fbea56 + 841d5e1 commit a5bf567

13 files changed

+470
-393
lines changed

custom_components/amshan/__init__.py

+210-11
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,40 @@
22
from __future__ import annotations
33

44
import asyncio
5+
from dataclasses import dataclass
6+
import datetime as dt
7+
from enum import Enum
58
import logging
6-
from typing import Mapping
9+
from typing import Callable, Mapping, cast
710

8-
from han import common as han_type, meter_connection
11+
from han import common as han_type, meter_connection, obis_map
912
from homeassistant import const as ha_const
13+
from homeassistant.components import sensor as ha_sensor
1014
from homeassistant.config_entries import ConfigEntry
1115
from homeassistant.core import CALLBACK_TYPE, callback
16+
from homeassistant.helpers import entity_registry
1217
from homeassistant.helpers.typing import ConfigType, EventType, HomeAssistantType
1318

14-
from .amshancfg import (
19+
from .const import (
1520
CONF_CONNECTION_CONFIG,
1621
CONF_CONNECTION_TYPE,
17-
CONFIGURATION_SCHEMA,
18-
async_migrate_config_entry,
22+
CONF_MQTT_TOPICS,
23+
CONF_TCP_HOST,
24+
DOMAIN,
1925
)
20-
from .common import ConnectionType, StopMessage
21-
from .const import DOMAIN
2226
from .metercon import async_setup_meter_mqtt_subscriptions, setup_meter_connection
2327

2428
_LOGGER: logging.Logger = logging.getLogger(__name__)
2529

2630
PLATFORM_TYPE = ha_const.Platform.SENSOR
2731

28-
CONFIG_SCHEMA = CONFIGURATION_SCHEMA
32+
33+
class ConnectionType(Enum):
34+
"""Meter connection type."""
35+
36+
SERIAL = "serial"
37+
NETWORK = "network_tcpip"
38+
MQTT = "hass_mqtt"
2939

3040

3141
class AmsHanIntegration:
@@ -80,7 +90,7 @@ async def async_close_all(self) -> None:
8090
self._tasks.clear()
8191

8292
def stop_receive(self) -> None:
83-
"""Stop receivers (serial/tcpip and/or MQTT."""
93+
"""Stop receivers (serial/tcp-ip and/or MQTT."""
8494
# signal processor to exit processing loop by sending empty bytes on the queue
8595
self.measure_queue.put_nowait(StopMessage())
8696

@@ -125,11 +135,46 @@ async def on_hass_stop(event: EventType) -> None:
125135
return True
126136

127137

128-
async def async_migrate_entry(
138+
async def async_migrate_config_entry(
129139
hass: HomeAssistantType, config_entry: ConfigEntry
130140
) -> bool:
131141
"""Migrate config when ConfigFlow version has changed."""
132-
return await async_migrate_config_entry(hass, config_entry)
142+
initial_version = config_entry.version
143+
current_data = config_entry.data
144+
_LOGGER.debug("Check for config entry migration of version %d", initial_version)
145+
146+
if config_entry.version == 1:
147+
await _async_migrate_entries(
148+
hass, config_entry.entry_id, _migrate_entity_entry_from_v1_to_v2
149+
)
150+
config_entry.version = 2
151+
current_data = {
152+
CONF_CONNECTION_TYPE: (
153+
ConnectionType.MQTT
154+
if CONF_MQTT_TOPICS in config_entry.data
155+
else ConnectionType.NETWORK
156+
if CONF_TCP_HOST in config_entry.data
157+
else ConnectionType.SERIAL
158+
).value,
159+
CONF_CONNECTION_CONFIG: {**current_data},
160+
}
161+
_LOGGER.debug("Config entry migrated to version 2")
162+
163+
if config_entry.version == 2:
164+
config_entry.version = 3
165+
await _async_migrate_entries(
166+
hass, config_entry.entry_id, _migrate_entity_entry_from_v2_to_v3
167+
)
168+
_LOGGER.debug("Config entry migrated to version 3")
169+
170+
hass.config_entries.async_update_entry(config_entry, data=current_data)
171+
_LOGGER.debug(
172+
"Config entry migration from %d to %d successfull.",
173+
initial_version,
174+
config_entry.version,
175+
)
176+
177+
return True
133178

134179

135180
async def async_unload_entry(
@@ -155,3 +200,157 @@ async def async_config_entry_changed(
155200
"""Handle config entry chnaged callback."""
156201
_LOGGER.info("Config entry has changed. Reload integration.")
157202
await hass.config_entries.async_reload(config_entry.entry_id)
203+
204+
205+
def _migrate_entity_entry_from_v1_to_v2(entity: entity_registry.RegistryEntry):
206+
def replace_ending(source, old, new):
207+
if source.endswith(old):
208+
return source[: -len(old)] + new
209+
return source
210+
211+
update = {}
212+
if entity.unique_id.endswith("_hour"):
213+
new_unique_id = replace_ending(entity.unique_id, "_hour", "_total")
214+
_LOGGER.info("Migrate unique_id from %s to %s", entity.unique_id, new_unique_id)
215+
update["new_unique_id"] = new_unique_id
216+
return update
217+
218+
219+
def _migrate_entity_entry_from_v2_to_v3(entity: entity_registry.RegistryEntry):
220+
update = {}
221+
222+
v3_migrate_fields = [
223+
obis_map.FIELD_METER_ID,
224+
obis_map.FIELD_METER_MANUFACTURER,
225+
obis_map.FIELD_METER_TYPE,
226+
obis_map.FIELD_OBIS_LIST_VER_ID,
227+
obis_map.FIELD_ACTIVE_POWER_IMPORT,
228+
obis_map.FIELD_ACTIVE_POWER_EXPORT,
229+
obis_map.FIELD_REACTIVE_POWER_IMPORT,
230+
obis_map.FIELD_REACTIVE_POWER_EXPORT,
231+
obis_map.FIELD_CURRENT_L1,
232+
obis_map.FIELD_CURRENT_L2,
233+
obis_map.FIELD_CURRENT_L3,
234+
obis_map.FIELD_VOLTAGE_L1,
235+
obis_map.FIELD_VOLTAGE_L2,
236+
obis_map.FIELD_VOLTAGE_L3,
237+
obis_map.FIELD_ACTIVE_POWER_IMPORT_TOTAL,
238+
obis_map.FIELD_ACTIVE_POWER_EXPORT_TOTAL,
239+
obis_map.FIELD_REACTIVE_POWER_IMPORT_TOTAL,
240+
obis_map.FIELD_REACTIVE_POWER_EXPORT_TOTAL,
241+
]
242+
243+
for measure_id in v3_migrate_fields:
244+
if entity.unique_id.endswith(f"-{measure_id}"):
245+
manufacturer = entity.unique_id[: entity.unique_id.find("-")]
246+
new_entity_id = f"sensor.{manufacturer}_{measure_id}".lower()
247+
if new_entity_id != entity.entity_id:
248+
update["new_entity_id"] = new_entity_id
249+
_LOGGER.info(
250+
"Migrate entity_id from %s to %s",
251+
entity.entity_id,
252+
new_entity_id,
253+
)
254+
255+
if measure_id in (
256+
obis_map.FIELD_REACTIVE_POWER_IMPORT,
257+
obis_map.FIELD_REACTIVE_POWER_EXPORT,
258+
):
259+
update["device_class"] = ha_sensor.SensorDeviceClass.REACTIVE_POWER
260+
update["unit_of_measurement"] = ha_const.POWER_VOLT_AMPERE_REACTIVE
261+
_LOGGER.info(
262+
"Migrated %s to device class %s with unit %s",
263+
entity.unique_id,
264+
ha_sensor.SensorDeviceClass.REACTIVE_POWER,
265+
ha_const.POWER_VOLT_AMPERE_REACTIVE,
266+
)
267+
268+
break
269+
270+
return update
271+
272+
273+
async def _async_migrate_entries(
274+
hass: HomeAssistantType,
275+
config_entry_id: str,
276+
entry_callback: Callable[[entity_registry.RegistryEntry], dict | None],
277+
) -> None:
278+
ent_reg = await entity_registry.async_get_registry(hass)
279+
280+
# Workaround:
281+
# entity_registry.async_migrate_entries fails with:
282+
# "RuntimeError: dictionary keys changed during iteration"
283+
# Try to get all entries from the dictionary before working on them.
284+
# The migration dows not directly change any keys of the registry. Concurrency problem in HA?
285+
286+
entries = []
287+
for entry in ent_reg.entities.values():
288+
if entry.config_entry_id == config_entry_id:
289+
entries.append(entry)
290+
291+
for entry in entries:
292+
updates = entry_callback(entry)
293+
if updates is not None:
294+
ent_reg.async_update_entity(entry.entity_id, **updates)
295+
296+
297+
@dataclass
298+
class MeterInfo:
299+
"""Info about meter."""
300+
301+
manufacturer: str | None
302+
manufacturer_id: str | None
303+
type: str | None
304+
type_id: str | None
305+
list_version_id: str
306+
meter_id: str | None
307+
308+
@property
309+
def unique_id(self) -> str | None:
310+
"""Meter unique id."""
311+
if self.meter_id:
312+
return f"{self.manufacturer}-{self.type}-{self.meter_id}".lower()
313+
return None
314+
315+
@classmethod
316+
def from_measure_data(
317+
cls, measure_data: dict[str, str | int | float | dt.datetime]
318+
) -> MeterInfo:
319+
"""Create MeterInfo from measure_data dictionary."""
320+
return cls(
321+
*[
322+
cast(str, measure_data.get(key))
323+
for key in [
324+
obis_map.FIELD_METER_MANUFACTURER,
325+
obis_map.FIELD_METER_MANUFACTURER_ID,
326+
obis_map.FIELD_METER_TYPE,
327+
obis_map.FIELD_METER_TYPE_ID,
328+
obis_map.FIELD_OBIS_LIST_VER_ID,
329+
obis_map.FIELD_METER_ID,
330+
]
331+
]
332+
)
333+
334+
335+
class StopMessage(han_type.MeterMessageBase):
336+
"""Special message top signal stop. No more messages."""
337+
338+
@property
339+
def message_type(self) -> han_type.MeterMessageType:
340+
"""Return MeterMessageType of message."""
341+
return han_type.MeterMessageType.UNKNOWN
342+
343+
@property
344+
def is_valid(self) -> bool:
345+
"""Return False for stop message."""
346+
return False
347+
348+
@property
349+
def as_bytes(self) -> bytes | None:
350+
"""Return None for stop message."""
351+
return None
352+
353+
@property
354+
def payload(self) -> bytes | None:
355+
"""Return None for stop message."""
356+
return None

0 commit comments

Comments
 (0)