diff --git a/custom_components/dabblerdk_powermeterreader/__init__.py b/custom_components/dabblerdk_powermeterreader/__init__.py index 379a81d..d116e16 100644 --- a/custom_components/dabblerdk_powermeterreader/__init__.py +++ b/custom_components/dabblerdk_powermeterreader/__init__.py @@ -1,18 +1,19 @@ """Support for dabblerdk_powermeterreader.""" -import logging import asyncio from datetime import timedelta +import logging from homeassistant import config_entries, core +from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.helpers.dispatcher import async_dispatcher_send # , dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.const import CONF_SCAN_INTERVAL + from .const import DOMAIN from .meter import MeterReader _LOGGER = logging.getLogger(__name__) -PLATFORMS = ["sensor", "binary_sensor"] +PLATFORMS = ["binary_sensor", "sensor"] async def async_setup_entry( @@ -61,7 +62,8 @@ async def signal_refresh(event_time=None): # pylint: disable=unused-argument async def async_setup( - hass: core.HomeAssistant, config: dict # pylint: disable=unused-argument + hass: core.HomeAssistant, + config: dict, # pylint: disable=unused-argument ) -> bool: """Set up the GitHub Custom component from yaml configuration.""" hass.data.setdefault(DOMAIN, {}) @@ -87,13 +89,20 @@ async def async_unload_entry( _LOGGER.debug("Remove timer") data["timer_remove"]() - unloaded = [] - for component in PLATFORMS: - unloaded.append( - await asyncio.gather( - *[hass.config_entries.async_forward_entry_unload(entry, component)] - ) + unloaded = [ + await asyncio.gather( + *[hass.config_entries.async_forward_entry_unload(entry, component)] ) + for component in PLATFORMS + ] + # unloaded = [] + # for component in PLATFORMS: + # unloaded.append( + # await asyncio.gather( + # *[hass.config_entries.async_forward_entry_unload(entry, component)] + # ) + # ) + unload_ok = all(unloaded) # await asyncio.gather( # *[hass.config_entries.async_forward_entry_unload(entry, component)] diff --git a/custom_components/dabblerdk_powermeterreader/binary_sensor.py b/custom_components/dabblerdk_powermeterreader/binary_sensor.py index e5d2667..23e412a 100644 --- a/custom_components/dabblerdk_powermeterreader/binary_sensor.py +++ b/custom_components/dabblerdk_powermeterreader/binary_sensor.py @@ -1,18 +1,19 @@ """Support for dabblerdk_powermeterreader.""" +from enum import IntEnum import logging import traceback -from enum import IntEnum from homeassistant import config_entries, core -from homeassistant.core import callback from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, - BinarySensorDeviceClass, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect + from .const import DOMAIN from .meter import MeterReader @@ -87,7 +88,8 @@ def __init__( meterclient, description: BinarySensorEntityDescription, meter_sn, - ): + ) -> None: + """Initialize the binarysensor.""" self.entity_description = description self._meterName = meterName if is_mep is False else f"{meterName} MEP" self._itemName = self.entity_description.name @@ -114,6 +116,7 @@ def __init__( @property def device_info(self): + """Device properties.""" identifier = self._meter_sn if not self._is_mep else f"{self._meter_sn}_MEP" return { "identifiers": { @@ -130,6 +133,7 @@ def device_info(self): @property def available(self): + """Device availability.""" return self._attr_is_on is not None async def async_update(self): @@ -229,7 +233,7 @@ async def async_update(self): @property def should_poll(self): - """Should Home Assistant check with the entity for an updated state?""" + """Should Home Assistant check with the entity for an updated state?.""" return False async def async_added_to_hass(self): diff --git a/custom_components/dabblerdk_powermeterreader/config_flow.py b/custom_components/dabblerdk_powermeterreader/config_flow.py index 3164678..5a33020 100644 --- a/custom_components/dabblerdk_powermeterreader/config_flow.py +++ b/custom_components/dabblerdk_powermeterreader/config_flow.py @@ -1,18 +1,15 @@ """Support for dabblerdk_powermeterreader.""" import logging -from typing import Any, Dict, Optional import re +from typing import Any, Optional # , Dict + +import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ( - CONF_NAME, - CONF_URL, - CONF_SCAN_INTERVAL, -) -from homeassistant.core import callback +from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL, CONF_URL +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -import voluptuous as vol from .const import DOMAIN from .meter import MeterReader @@ -30,11 +27,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """dabblerdk_powermeterreader config flow.""" - data: Optional[Dict[str, Any]] + data: Optional[dict[str, Any]] - async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None): - """Invoked when a user initiates a flow via the user interface.""" - errors: Dict[str, str] = {} + async def async_step_user(self, user_input: Optional[dict[str, Any]] = None): + """User initiates a flow via the user interface.""" + errors: dict[str, str] = {} if user_input is not None: if user_input[CONF_NAME] == "": errors[CONF_NAME] = "empty_name" @@ -68,24 +65,24 @@ async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None): @staticmethod @callback def async_get_options_flow(config_entry): + """Initiate options flow.""" return OptionsFlowHandler(config_entry) class OptionsFlowHandler(config_entries.OptionsFlow): """dabblerdk_powermeterreader options flow.""" - def __init__(self, config_entry): + def __init__(self, config_entry) -> None: """Initialize options flow.""" self.config_entry = config_entry async def async_step_init( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: + self, user_input: dict[str, Any] = None + ) -> dict[str, Any]: """Manage the options.""" - errors: Dict[str, str] = {} + errors: dict[str, str] = {} if user_input is not None: - # Check url await fnCheckUrl(user_input[CONF_URL], self.hass, errors) @@ -125,7 +122,7 @@ async def async_step_init( ) -async def fnCheckUrl(url, hass, errors: Dict[str, str]): +async def fnCheckUrl(url, hass: HomeAssistant, errors: dict[str, str]): """Check if url is working.""" if ( re.search(r"^http(s){0,1}:\/\/[a-zA-Z0-9_\-\.]+(:\d+){0,1}\/{0,1}$", url) @@ -162,7 +159,7 @@ async def fnCheckUrl(url, hass, errors: Dict[str, str]): except Exception as err: # pylint: disable=broad-except # _LOGGER.debug(err) - print(err) + # print(err) if str(err) == "empty_response": errors[CONF_URL] = "empty_response" # elif str(err).startswith("Requesting meter values failed:"): diff --git a/custom_components/dabblerdk_powermeterreader/manifest.json b/custom_components/dabblerdk_powermeterreader/manifest.json index 0b79fa2..3d14f87 100644 --- a/custom_components/dabblerdk_powermeterreader/manifest.json +++ b/custom_components/dabblerdk_powermeterreader/manifest.json @@ -1,6 +1,6 @@ { "domain": "dabblerdk_powermeterreader", - "name": "Dabbler.dk reader for Echelon/NES smart power meter", + "name": "Dabbler.dk reader for Echelon/NES smart power meter", "codeowners": ["@jnxxx"], "config_flow": true, "dependencies": ["zeroconf"], @@ -11,6 +11,6 @@ "issue_tracker": "https://github.com/jnxxx/homeassistant-dabblerdk_powermeterreader/issues", "requirements": [], "ssdp": [], - "version": "1.0.4", + "version": "1.0.5", "zeroconf": [] } diff --git a/custom_components/dabblerdk_powermeterreader/meter/__init__.py b/custom_components/dabblerdk_powermeterreader/meter/__init__.py index 0081da8..4f151f0 100644 --- a/custom_components/dabblerdk_powermeterreader/meter/__init__.py +++ b/custom_components/dabblerdk_powermeterreader/meter/__init__.py @@ -2,4 +2,4 @@ from .meter import MeterReader -__version__ = '0.1.0' +__version__ = "0.1.0" diff --git a/custom_components/dabblerdk_powermeterreader/meter/meter.py b/custom_components/dabblerdk_powermeterreader/meter/meter.py index 0cd6aa1..284f65a 100644 --- a/custom_components/dabblerdk_powermeterreader/meter/meter.py +++ b/custom_components/dabblerdk_powermeterreader/meter/meter.py @@ -1,30 +1,31 @@ """Wrapper for dabbler.dk MEP module.""" -import logging -from datetime import datetime -from datetime import timedelta +import asyncio +from datetime import UTC, datetime, timedelta import json +import logging from urllib.parse import urlparse -import asyncio + import aiohttp -from homeassistant.components import zeroconf from zeroconf.asyncio import AsyncServiceInfo +from homeassistant.components import zeroconf +from homeassistant.core import HomeAssistant + _LOGGER = logging.getLogger(__name__) class MeterReader: - """ - Primary exported interface for dabbler.dk MEP module wrapper. - """ + """Primary exported interface for dabbler.dk MEP module wrapper.""" - def __init__(self, target_url, hass): + def __init__(self, target_url, hass: HomeAssistant) -> None: + """Initialize.""" self._base_url = target_url.strip("/") self._data = None self._data_expires = None self._lockUpdate = asyncio.Lock() # self._fail_count = 0 - self._succeed_timestamp = datetime.utcnow() + self._succeed_timestamp = datetime.now(tz=UTC) # datetime.utcnow() self._connected = False self._sticking_with_prev_value = False self._hass = hass @@ -67,7 +68,7 @@ async def is_connected(self): return ret async def is_stuck_with_prev_value(self): - """Get bool indicating if stuck with cahed data or freshly read""" + """Get bool indicating if stuck with cahed data or freshly read.""" ret = None async with self._lockUpdate: ret = self._sticking_with_prev_value @@ -83,7 +84,7 @@ async def get_meter_data(self): if ( self._data_expires is None or self._data is None - or datetime.utcnow() > self._data_expires + or datetime.now(tz=UTC) > self._data_expires ): self._data_expires = None # self._data = None @@ -162,7 +163,8 @@ async def get_meter_data(self): 60 if (self._succeed_timestamp is None) else ( - datetime.utcnow() - self._succeed_timestamp + datetime.now(tz=UTC) + - self._succeed_timestamp ).total_seconds() ) if elapsed_time < 60: @@ -190,7 +192,9 @@ async def get_meter_data(self): and total_power >= sum_power - 3 ): self._data = temp - self._succeed_timestamp = datetime.utcnow() + self._succeed_timestamp = datetime.now( + tz=UTC + ) if stuck_with_prev_value: _LOGGER.warning( "Resume-read meter data: %s", @@ -203,7 +207,7 @@ async def get_meter_data(self): total_power, ) _LOGGER.warning( - "data: %s", json.dumps(temp) + "Data: %s", json.dumps(temp) ) self._sticking_with_prev_value = True else: @@ -216,19 +220,19 @@ async def get_meter_data(self): else: self._data = temp - self._succeed_timestamp = datetime.utcnow() + self._succeed_timestamp = datetime.now(tz=UTC) _LOGGER.warning( "First meter data: %s", json.dumps(temp) ) - self._data_expires = datetime.utcnow() + timedelta( + self._data_expires = datetime.now(tz=UTC) + timedelta( seconds=2 ) else: - raise Exception("empty_response") + raise Exception("empty_response") # pylint: disable=broad-exception-raised except aiohttp.ClientError as client_error: _LOGGER.warning("Requesting meter values failed: %s", client_error) - raise Exception( + raise Exception( # pylint: disable=broad-exception-raised f"Requesting meter values failed: {client_error}" ) from client_error diff --git a/custom_components/dabblerdk_powermeterreader/sensor.py b/custom_components/dabblerdk_powermeterreader/sensor.py index b96a2c8..f665117 100644 --- a/custom_components/dabblerdk_powermeterreader/sensor.py +++ b/custom_components/dabblerdk_powermeterreader/sensor.py @@ -1,31 +1,27 @@ """Support for dabblerdk_powermeterreader.""" +from enum import IntEnum import logging - -# from datetime import datetime, timedelta -# from typing import Any, Callable, Dict, Optional import traceback -from enum import IntEnum +from homeassistant import config_entries, core from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) -from homeassistant import config_entries, core -from homeassistant.core import callback from homeassistant.const import ( + UnitOfElectricCurrent, + UnitOfElectricPotential, UnitOfEnergy, - ELECTRIC_POTENTIAL_VOLT, - ELECTRIC_CURRENT_AMPERE, - POWER_WATT, - FREQUENCY_HERTZ, + UnitOfFrequency, + UnitOfPower, ) -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect -# from homeassistant.helpers import device_registry as dr from .const import DOMAIN from .meter import MeterReader @@ -70,7 +66,7 @@ class EchelonSensorType(IntEnum): key=EchelonSensorType.VOLTAGE, device_class=SensorDeviceClass.VOLTAGE, entity_category=None, - native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, entity_registry_enabled_default=False, icon="mdi:lightning-bolt", name="voltage", @@ -79,7 +75,7 @@ class EchelonSensorType(IntEnum): key=EchelonSensorType.CURRENT, device_class=SensorDeviceClass.CURRENT, entity_category=None, - native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, entity_registry_enabled_default=False, icon="mdi:current-ac", name="current", @@ -88,7 +84,7 @@ class EchelonSensorType(IntEnum): key=EchelonSensorType.POWER, device_class=SensorDeviceClass.POWER, entity_category=None, - native_unit_of_measurement=POWER_WATT, + native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=True, icon="mdi:flash", @@ -98,7 +94,7 @@ class EchelonSensorType(IntEnum): key=EchelonSensorType.POWER_PHASE, device_class=SensorDeviceClass.POWER, entity_category=None, - native_unit_of_measurement=POWER_WATT, + native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, icon="mdi:flash", @@ -108,7 +104,7 @@ class EchelonSensorType(IntEnum): key=EchelonSensorType.POWER_REV, device_class=SensorDeviceClass.POWER, entity_category=None, - native_unit_of_measurement=POWER_WATT, + native_unit_of_measurement=UnitOfPower.WATT, state_class=SensorStateClass.MEASUREMENT, entity_registry_enabled_default=False, icon="mdi:flash", @@ -118,7 +114,7 @@ class EchelonSensorType(IntEnum): key=EchelonSensorType.FREQUENCY, device_class=SensorDeviceClass.FREQUENCY, entity_category=None, - native_unit_of_measurement=FREQUENCY_HERTZ, + native_unit_of_measurement=UnitOfFrequency.HERTZ, entity_registry_enabled_default=False, icon="mdi:sine-wave", name="frequency", @@ -201,7 +197,7 @@ def __init__( meterclient, description: SensorEntityDescription, meter_sn, - ): + ) -> None: """Initialize the sensor.""" self.entity_description = description self._config_entry_id = config_entry_id @@ -230,6 +226,7 @@ def __init__( @property def device_info(self): + """Device properties.""" return { "identifiers": { # Serial numbers are unique identifiers within a specific domain @@ -245,6 +242,7 @@ def device_info(self): @property def available(self): + """Device availability.""" return self._attr_native_value is not None async def async_update(self): @@ -281,8 +279,8 @@ async def async_update(self): current = await self._meterclient.get_value([self._phase + "_RMS_A"]) if current is not None: self._attr_native_value = int(current) / 1000 - if self._itemName == "power" or self._itemName == "power returned": - prop = "Fwd_W" if self._returned == False else "Rev_W" + if self._itemName in ("power", "power returned"): + prop = "Fwd_W" if not self._returned else "Rev_W" if self._phase != "": prop = f"{self._phase}_{prop}" power = await self._meterclient.get_value([prop]) @@ -317,7 +315,7 @@ async def async_update(self): @property def should_poll(self): - """Should Home Assistant check with the entity for an updated state?""" + """Should Home Assistant check with the entity for an updated state?.""" return False async def async_added_to_hass(self): diff --git a/hacs.json b/hacs.json index 72227fe..ef67a8f 100644 --- a/hacs.json +++ b/hacs.json @@ -1,7 +1,7 @@ { "name": "Dabbler.dk reader for Echelon/NES smart power meter", "render_readme": true, - "homeassistant": "2022.11.0", + "homeassistant": "2024.01.0", "zip_release": true, "filename": "dabblerdk_powermeterreader.zip" }