2
2
from __future__ import annotations
3
3
4
4
import asyncio
5
+ from dataclasses import dataclass
6
+ import datetime as dt
7
+ from enum import Enum
5
8
import logging
6
- from typing import Mapping
9
+ from typing import Callable , Mapping , cast
7
10
8
- from han import common as han_type , meter_connection
11
+ from han import common as han_type , meter_connection , obis_map
9
12
from homeassistant import const as ha_const
13
+ from homeassistant .components import sensor as ha_sensor
10
14
from homeassistant .config_entries import ConfigEntry
11
15
from homeassistant .core import CALLBACK_TYPE , callback
16
+ from homeassistant .helpers import entity_registry
12
17
from homeassistant .helpers .typing import ConfigType , EventType , HomeAssistantType
13
18
14
- from .amshancfg import (
19
+ from .const import (
15
20
CONF_CONNECTION_CONFIG ,
16
21
CONF_CONNECTION_TYPE ,
17
- CONFIGURATION_SCHEMA ,
18
- async_migrate_config_entry ,
22
+ CONF_MQTT_TOPICS ,
23
+ CONF_TCP_HOST ,
24
+ DOMAIN ,
19
25
)
20
- from .common import ConnectionType , StopMessage
21
- from .const import DOMAIN
22
26
from .metercon import async_setup_meter_mqtt_subscriptions , setup_meter_connection
23
27
24
28
_LOGGER : logging .Logger = logging .getLogger (__name__ )
25
29
26
30
PLATFORM_TYPE = ha_const .Platform .SENSOR
27
31
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"
29
39
30
40
31
41
class AmsHanIntegration :
@@ -80,7 +90,7 @@ async def async_close_all(self) -> None:
80
90
self ._tasks .clear ()
81
91
82
92
def stop_receive (self ) -> None :
83
- """Stop receivers (serial/tcpip and/or MQTT."""
93
+ """Stop receivers (serial/tcp-ip and/or MQTT."""
84
94
# signal processor to exit processing loop by sending empty bytes on the queue
85
95
self .measure_queue .put_nowait (StopMessage ())
86
96
@@ -125,11 +135,46 @@ async def on_hass_stop(event: EventType) -> None:
125
135
return True
126
136
127
137
128
- async def async_migrate_entry (
138
+ async def async_migrate_config_entry (
129
139
hass : HomeAssistantType , config_entry : ConfigEntry
130
140
) -> bool :
131
141
"""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
133
178
134
179
135
180
async def async_unload_entry (
@@ -155,3 +200,157 @@ async def async_config_entry_changed(
155
200
"""Handle config entry chnaged callback."""
156
201
_LOGGER .info ("Config entry has changed. Reload integration." )
157
202
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