From 82f0b28e89a4a2c0af01588a556b68923a407d5a Mon Sep 17 00:00:00 2001 From: yangbo Date: Thu, 14 Dec 2023 17:01:29 +0800 Subject: [PATCH] Bump iammeter to 0.2.1 (#95885) * Bump iammeter to 0.2.1 * Refactor sensor. * Add const.py to .coveragerc. * Add id migration. * Modify translation file. * Fix ruff test error * update asyncio.timeout import. * Delete homeassistant/components/iammeter/translations directory * Add strings.json --- .coveragerc | 1 + homeassistant/components/iammeter/__init__.py | 2 +- homeassistant/components/iammeter/const.py | 11 + .../components/iammeter/manifest.json | 2 +- homeassistant/components/iammeter/sensor.py | 452 ++++++++++++++++-- .../components/iammeter/strings.json | 69 +++ requirements_all.txt | 2 +- 7 files changed, 486 insertions(+), 53 deletions(-) create mode 100644 homeassistant/components/iammeter/const.py create mode 100644 homeassistant/components/iammeter/strings.json diff --git a/.coveragerc b/.coveragerc index 7c74ed57505..3a0cfb4a70c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -539,6 +539,7 @@ omit = homeassistant/components/hvv_departures/binary_sensor.py homeassistant/components/hvv_departures/sensor.py homeassistant/components/ialarm/alarm_control_panel.py + homeassistant/components/iammeter/const.py homeassistant/components/iammeter/sensor.py homeassistant/components/iaqualink/binary_sensor.py homeassistant/components/iaqualink/climate.py diff --git a/homeassistant/components/iammeter/__init__.py b/homeassistant/components/iammeter/__init__.py index b53cc35197c..46b8aaca3e7 100644 --- a/homeassistant/components/iammeter/__init__.py +++ b/homeassistant/components/iammeter/__init__.py @@ -1 +1 @@ -"""Support for IamMeter Devices.""" +"""Iammeter integration.""" diff --git a/homeassistant/components/iammeter/const.py b/homeassistant/components/iammeter/const.py new file mode 100644 index 00000000000..c2d122c9e32 --- /dev/null +++ b/homeassistant/components/iammeter/const.py @@ -0,0 +1,11 @@ +"""Constants for the Iammeter integration.""" +from __future__ import annotations + +DOMAIN = "iammeter" + +# Default config for iammeter. +DEFAULT_IP = "192.168.2.15" +DEFAULT_NAME = "IamMeter" +DEVICE_3080 = "WEM3080" +DEVICE_3080T = "WEM3080T" +DEVICE_TYPES = [DEVICE_3080, DEVICE_3080T] diff --git a/homeassistant/components/iammeter/manifest.json b/homeassistant/components/iammeter/manifest.json index 191dbdedb98..f1ebecab00d 100644 --- a/homeassistant/components/iammeter/manifest.json +++ b/homeassistant/components/iammeter/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/iammeter", "iot_class": "local_polling", "loggers": ["iammeter"], - "requirements": ["iammeter==0.1.7"] + "requirements": ["iammeter==0.2.1"] } diff --git a/homeassistant/components/iammeter/sensor.py b/homeassistant/components/iammeter/sensor.py index ca468200370..f36eca93f28 100644 --- a/homeassistant/components/iammeter/sensor.py +++ b/homeassistant/components/iammeter/sensor.py @@ -2,26 +2,44 @@ from __future__ import annotations import asyncio -from datetime import timedelta +from asyncio import timeout +from collections.abc import Callable +from dataclasses import dataclass +from datetime import datetime, timedelta import logging -from iammeter import real_time_api -from iammeter.power_meter import IamMeterError +from iammeter.client import IamMeter import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PORT, + PERCENTAGE, + Platform, + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfFrequency, + UnitOfPower, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers import debounce +from homeassistant.helpers import debounce, entity_registry as er, update_coordinator import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEVICE_3080, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -40,6 +58,51 @@ SCAN_INTERVAL = timedelta(seconds=30) PLATFORM_TIMEOUT = 8 +def _migrate_to_new_unique_id( + hass: HomeAssistant, model: str, serial_number: str +) -> None: + """Migrate old unique ids to new unique ids.""" + ent_reg = er.async_get(hass) + name_list = [ + "Voltage", + "Current", + "Power", + "ImportEnergy", + "ExportGrid", + "Frequency", + "PF", + ] + phase_list = ["A", "B", "C", "NET"] + id_phase_range = 1 if model == DEVICE_3080 else 4 + id_name_range = 5 if model == DEVICE_3080 else 7 + for row in range(0, id_phase_range): + for idx in range(0, id_name_range): + old_unique_id = f"{serial_number}-{row}-{idx}" + new_unique_id = ( + f"{serial_number}_{name_list[idx]}" + if model == DEVICE_3080 + else f"{serial_number}_{name_list[idx]}_{phase_list[row]}" + ) + entity_id = ent_reg.async_get_entity_id( + Platform.SENSOR, DOMAIN, old_unique_id + ) + if entity_id is not None: + try: + ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id) + except ValueError: + _LOGGER.warning( + "Skip migration of id [%s] to [%s] because it already exists", + old_unique_id, + new_unique_id, + ) + else: + _LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + old_unique_id, + new_unique_id, + ) + + async def async_setup_platform( hass: HomeAssistant, config: ConfigType, @@ -51,23 +114,24 @@ async def async_setup_platform( config_port = config[CONF_PORT] config_name = config[CONF_NAME] try: - async with asyncio.timeout(PLATFORM_TIMEOUT): - api = await real_time_api(config_host, config_port) - except (IamMeterError, asyncio.TimeoutError) as err: + api = await hass.async_add_executor_job( + IamMeter, config_host, config_port, config_name + ) + except asyncio.TimeoutError as err: _LOGGER.error("Device is not ready") raise PlatformNotReady from err async def async_update_data(): try: - async with asyncio.timeout(PLATFORM_TIMEOUT): - return await api.get_data() - except (IamMeterError, asyncio.TimeoutError) as err: + async with timeout(PLATFORM_TIMEOUT): + return await hass.async_add_executor_job(api.client.get_data) + except asyncio.TimeoutError as err: raise UpdateFailed from err coordinator = DataUpdateCoordinator( hass, _LOGGER, - name=DEFAULT_DEVICE_NAME, + name=config_name, update_method=async_update_data, update_interval=SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( @@ -75,46 +139,334 @@ async def async_setup_platform( ), ) await coordinator.async_refresh() - entities = [] - for sensor_name, (row, idx, unit) in api.iammeter.sensor_map().items(): - serial_number = api.iammeter.serial_number - uid = f"{serial_number}-{row}-{idx}" - entities.append(IamMeter(coordinator, uid, sensor_name, unit, config_name)) - async_add_entities(entities) + model = coordinator.data["Model"] + serial_number = coordinator.data["sn"] + _migrate_to_new_unique_id(hass, model, serial_number) + if model == DEVICE_3080: + async_add_entities( + IammeterSensor(coordinator, description) + for description in SENSOR_TYPES_3080 + ) + else: # DEVICE_3080T: + async_add_entities( + IammeterSensor(coordinator, description) + for description in SENSOR_TYPES_3080T + ) -class IamMeter(CoordinatorEntity, SensorEntity): - """Class for a sensor.""" +class IammeterSensor(update_coordinator.CoordinatorEntity, SensorEntity): + """Representation of a Sensor.""" - def __init__(self, coordinator, uid, sensor_name, unit, dev_name): - """Initialize an iammeter sensor.""" + entity_description: IammeterSensorEntityDescription + _attr_has_entity_name = True + _attr_name = None + + def __init__( + self, + coordinator: DataUpdateCoordinator, + description: IammeterSensorEntityDescription, + ) -> None: + """Initialize the sensor.""" super().__init__(coordinator) - self.uid = uid - self.sensor_name = sensor_name - self.unit = unit - self.dev_name = dev_name + self.entity_description = description + self._attr_unique_id = f"{coordinator.data['sn']}_{description.key}" + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, coordinator.data["sn"])}, + manufacturer="IamMeter", + name=coordinator.name, + ) @property def native_value(self): - """Return the state of the sensor.""" - return self.coordinator.data.data[self.sensor_name] + """Return the native sensor value.""" + raw_attr = self.coordinator.data.get(self.entity_description.key, None) + if self.entity_description.value: + return self.entity_description.value(raw_attr) + return raw_attr - @property - def unique_id(self): - """Return unique id.""" - return self.uid - @property - def name(self): - """Name of this iammeter attribute.""" - return f"{self.dev_name} {self.sensor_name}" +@dataclass +class IammeterSensorEntityDescription(SensorEntityDescription): + """Describes Iammeter sensor entity.""" - @property - def icon(self): - """Icon for each sensor.""" - return "mdi:flash" + value: Callable[[float | int], float] | Callable[[datetime], datetime] | None = None - @property - def native_unit_of_measurement(self): - """Return the unit of measurement.""" - return self.unit + +SENSOR_TYPES_3080: tuple[IammeterSensorEntityDescription, ...] = ( + IammeterSensorEntityDescription( + key="Voltage", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Current", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Power", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + IammeterSensorEntityDescription( + key="ImportEnergy", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IammeterSensorEntityDescription( + key="ExportGrid", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), +) +SENSOR_TYPES_3080T: tuple[IammeterSensorEntityDescription, ...] = ( + IammeterSensorEntityDescription( + key="Voltage_A", + translation_key="voltage_a", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Current_A", + translation_key="current_a", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Power_A", + translation_key="power_a", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + IammeterSensorEntityDescription( + key="ImportEnergy_A", + translation_key="import_energy_a", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IammeterSensorEntityDescription( + key="ExportGrid_A", + translation_key="export_grid_a", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IammeterSensorEntityDescription( + key="Frequency_A", + translation_key="frequency_a", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="PF_A", + translation_key="pf_a", + icon="mdi:solar-power", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + value=lambda value: value * 100, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Voltage_B", + translation_key="voltage_b", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Current_B", + translation_key="current_b", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Power_B", + translation_key="power_b", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + IammeterSensorEntityDescription( + key="ImportEnergy_B", + translation_key="import_energy_b", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IammeterSensorEntityDescription( + key="ExportGrid_B", + translation_key="export_grid_b", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IammeterSensorEntityDescription( + key="Frequency_B", + translation_key="frequency_b", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="PF_B", + translation_key="pf_b", + icon="mdi:solar-power", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + value=lambda value: value * 100, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Voltage_C", + translation_key="voltage_c", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Current_C", + translation_key="current_c", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.CURRENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Power_C", + translation_key="power_c", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + IammeterSensorEntityDescription( + key="ImportEnergy_C", + translation_key="import_energy_c", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IammeterSensorEntityDescription( + key="ExportGrid_C", + translation_key="export_grid_c", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), + IammeterSensorEntityDescription( + key="Frequency_C", + translation_key="frequency_c", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="PF_C", + translation_key="pf_c", + icon="mdi:solar-power", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + value=lambda value: value * 100, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Voltage_Net", + translation_key="voltage_net", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Power_Net", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="ImportEnergy_Net", + translation_key="import_energy_net", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="ExportGrid_Net", + translation_key="export_grid_net", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="Frequency_Net", + translation_key="frequency_net", + icon="mdi:solar-power", + native_unit_of_measurement=UnitOfFrequency.HERTZ, + device_class=SensorDeviceClass.FREQUENCY, + state_class=SensorStateClass.MEASUREMENT, + entity_registry_enabled_default=False, + ), + IammeterSensorEntityDescription( + key="PF_Net", + translation_key="pf_net", + icon="mdi:solar-power", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + device_class=SensorDeviceClass.POWER_FACTOR, + value=lambda value: value * 100, + entity_registry_enabled_default=False, + ), +) diff --git a/homeassistant/components/iammeter/strings.json b/homeassistant/components/iammeter/strings.json new file mode 100644 index 00000000000..6d0c3797dfc --- /dev/null +++ b/homeassistant/components/iammeter/strings.json @@ -0,0 +1,69 @@ +{ + "entity": { + "sensor": { + "voltage_a": { + "name": "Voltage A" + }, + "voltage_b": { + "name": "Voltage B" + }, + "voltage_c": { + "name": "Voltage C" + }, + "current_a": { + "name": "Current A" + }, + "current_b": { + "name": "Current B" + }, + "current_c": { + "name": "Current C" + }, + "power_a": { + "name": "Power A" + }, + "power_b": { + "name": "Power B" + }, + "power_c": { + "name": "Power C" + }, + "import_energy_a": { + "name": "ImportEnergy A" + }, + "import_energy_b": { + "name": "ImportEnergy B" + }, + "import_energy_c": { + "name": "ImportEnergy C" + }, + "export_grid_a": { + "name": "ExportGrid A" + }, + "export_grid_b": { + "name": "ExportGrid B" + }, + "export_grid_c": { + "name": "ExportGrid C" + }, + "frequency_a": { + "name": "Frequency A" + }, + "frequency_b": { + "name": "Frequency B" + }, + "frequency_c": { + "name": "Frequency C" + }, + "pf_a": { + "name": "PF A" + }, + "pf_b": { + "name": "PF B" + }, + "pf_c": { + "name": "PF C" + } + } + } +} diff --git a/requirements_all.txt b/requirements_all.txt index b7ed653568f..8fb23783083 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1051,7 +1051,7 @@ huawei-lte-api==1.7.3 hyperion-py==0.7.5 # homeassistant.components.iammeter -iammeter==0.1.7 +iammeter==0.2.1 # homeassistant.components.iaqualink iaqualink==0.5.0