From 2945c79c5aacbb00950a9f658f0b2400e52a3b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Wed, 12 May 2021 20:07:44 +0200 Subject: [PATCH] Tibber sensors (#50418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tibber, split attribute to sensors Signed-off-by: Daniel Hjelseth Høyer * Tibber, split attribute to sensors Signed-off-by: Daniel Hjelseth Høyer * Tibber, split attribute to sensors Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * fix review comments Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * fix review comments Signed-off-by: Daniel Hjelseth Høyer * migrate to new device ids Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * Migrate entity id Signed-off-by: Daniel Hjelseth Høyer * Migrate entity id Signed-off-by: Daniel Hjelseth Høyer * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * Update homeassistant/components/tibber/sensor.py Co-authored-by: Martin Hjelmare * move registers out of looå Signed-off-by: Daniel Hjelseth Høyer Co-authored-by: Martin Hjelmare --- homeassistant/components/tibber/manifest.json | 2 +- homeassistant/components/tibber/sensor.py | 235 ++++++++++++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 194 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 01a20011bef..57b329765a9 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.16.2"], + "requirements": ["pyTibber==0.17.0"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 5ab85013a25..4a706a6a517 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -6,9 +6,29 @@ from random import randrange import aiohttp -from homeassistant.components.sensor import DEVICE_CLASS_POWER, SensorEntity -from homeassistant.const import POWER_WATT +from homeassistant.components.sensor import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_VOLTAGE, + SensorEntity, +) +from homeassistant.const import ( + ELECTRICAL_CURRENT_AMPERE, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + SIGNAL_STRENGTH_DECIBELS, + VOLT, +) +from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.device_registry import async_get as async_get_dev_reg +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity_registry import async_get as async_get_entity_reg from homeassistant.util import Throttle, dt as dt_util from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER @@ -19,6 +39,56 @@ ICON = "mdi:currency-usd" SCAN_INTERVAL = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) PARALLEL_UPDATES = 0 +SIGNAL_UPDATE_ENTITY = "tibber_rt_update_{}" + +RT_SENSOR_MAP = { + "averagePower": ["average power", DEVICE_CLASS_POWER, POWER_WATT], + "power": ["power", DEVICE_CLASS_POWER, POWER_WATT], + "minPower": ["min power", DEVICE_CLASS_POWER, POWER_WATT], + "maxPower": ["max power", DEVICE_CLASS_POWER, POWER_WATT], + "accumulatedConsumption": [ + "accumulated consumption", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "accumulatedConsumptionLastHour": [ + "accumulated consumption last hour", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "accumulatedProduction": [ + "accumulated production", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "accumulatedProductionLastHour": [ + "accumulated production last hour", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "lastMeterConsumption": [ + "last meter consumption", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "lastMeterProduction": [ + "last meter production", + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ], + "voltagePhase1": ["voltage phase1", DEVICE_CLASS_VOLTAGE, VOLT], + "voltagePhase2": ["voltage phase2", DEVICE_CLASS_VOLTAGE, VOLT], + "voltagePhase3": ["voltage phase3", DEVICE_CLASS_VOLTAGE, VOLT], + "currentL1": ["current L1", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE], + "currentL2": ["current L2", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE], + "currentL3": ["current L3", DEVICE_CLASS_CURRENT, ELECTRICAL_CURRENT_AMPERE], + "signalStrength": [ + "signal strength", + DEVICE_CLASS_SIGNAL_STRENGTH, + SIGNAL_STRENGTH_DECIBELS, + ], + "accumulatedCost": ["accumulated cost", None, None], +} async def async_setup_entry(hass, entry, async_add_entities): @@ -26,7 +96,10 @@ async def async_setup_entry(hass, entry, async_add_entities): tibber_connection = hass.data.get(TIBBER_DOMAIN) - dev = [] + entity_registry = async_get_entity_reg(hass) + device_registry = async_get_dev_reg(hass) + + entities = [] for home in tibber_connection.get_homes(only_active=False): try: await home.update_info() @@ -36,12 +109,36 @@ async def async_setup_entry(hass, entry, async_add_entities): except aiohttp.ClientError as err: _LOGGER.error("Error connecting to Tibber home: %s ", err) raise PlatformNotReady() from err - if home.has_active_subscription: - dev.append(TibberSensorElPrice(home)) - if home.has_real_time_consumption: - dev.append(TibberSensorRT(home)) - async_add_entities(dev, True) + if home.has_active_subscription: + entities.append(TibberSensorElPrice(home)) + if home.has_real_time_consumption: + await home.rt_subscribe( + TibberRtDataHandler(async_add_entities, home, hass).async_callback + ) + + # migrate + old_id = home.info["viewer"]["home"]["meteringPointData"]["consumptionEan"] + if old_id is None: + continue + + # migrate to new device ids + old_entity_id = entity_registry.async_get_entity_id( + "sensor", TIBBER_DOMAIN, old_id + ) + if old_entity_id is not None: + entity_registry.async_update_entity( + old_entity_id, new_unique_id=home.home_id + ) + + # migrate to new device ids + device_entry = device_registry.async_get_device({(TIBBER_DOMAIN, old_id)}) + if device_entry and entry.entry_id in device_entry.config_entries: + device_registry.async_update_device( + device_entry.id, new_identifiers={(TIBBER_DOMAIN, home.home_id)} + ) + + async_add_entities(entities, True) class TibberSensor(SensorEntity): @@ -50,21 +147,13 @@ class TibberSensor(SensorEntity): def __init__(self, tibber_home): """Initialize the sensor.""" self._tibber_home = tibber_home - self._last_updated = None self._state = None - self._is_available = False - self._extra_state_attributes = {} + self._name = tibber_home.info["viewer"]["home"]["appNickname"] if self._name is None: self._name = tibber_home.info["viewer"]["home"]["address"].get( "address1", "" ) - self._spread_load_constant = randrange(3600) - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return self._extra_state_attributes @property def model(self): @@ -79,8 +168,7 @@ class TibberSensor(SensorEntity): @property def device_id(self): """Return the ID of the physical device this sensor is part of.""" - home = self._tibber_home.info["viewer"]["home"] - return home["meteringPointData"]["consumptionEan"] + return self._tibber_home.home_id @property def device_info(self): @@ -98,6 +186,19 @@ class TibberSensor(SensorEntity): class TibberSensorElPrice(TibberSensor): """Representation of a Tibber sensor for el price.""" + def __init__(self, tibber_home): + """Initialize the sensor.""" + super().__init__(tibber_home) + self._last_updated = None + self._is_available = False + self._extra_state_attributes = {} + self._spread_load_constant = randrange(5000) + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return self._extra_state_attributes + async def async_update(self): """Get the latest data and updates the states.""" now = dt_util.now() @@ -176,29 +277,23 @@ class TibberSensorElPrice(TibberSensor): class TibberSensorRT(TibberSensor): """Representation of a Tibber sensor for real time consumption.""" + def __init__(self, tibber_home, sensor_name, device_class, unit, initial_state): + """Initialize the sensor.""" + super().__init__(tibber_home) + self._sensor_name = sensor_name + self._device_class = device_class + self._unit = unit + self._state = initial_state + async def async_added_to_hass(self): """Start listen for real time data.""" - await self._tibber_home.rt_subscribe(self.hass.loop, self._async_callback) - - async def _async_callback(self, payload): - """Handle received data.""" - errors = payload.get("errors") - if errors: - _LOGGER.error(errors[0]) - return - data = payload.get("data") - if data is None: - return - live_measurement = data.get("liveMeasurement") - if live_measurement is None: - return - self._state = live_measurement.pop("power", None) - for key, value in live_measurement.items(): - if value is None: - continue - self._extra_state_attributes[key] = value - - self.async_write_ha_state() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_UPDATE_ENTITY.format(self._sensor_name), + self._set_state, + ) + ) @property def available(self): @@ -213,7 +308,13 @@ class TibberSensorRT(TibberSensor): @property def name(self): """Return the name of the sensor.""" - return f"Real time consumption {self._name}" + return f"{self._sensor_name} {self._name}" + + @callback + def _set_state(self, state): + """Set sensor state.""" + self._state = state + self.async_write_ha_state() @property def should_poll(self): @@ -223,14 +324,60 @@ class TibberSensorRT(TibberSensor): @property def unit_of_measurement(self): """Return the unit of measurement of this entity.""" - return POWER_WATT + return self._unit @property def unique_id(self): """Return a unique ID.""" - return f"{self.device_id}_rt_consumption" + return f"{self.device_id}_rt_{self._sensor_name}" @property def device_class(self): """Return the device class of the sensor.""" - return DEVICE_CLASS_POWER + return self._device_class + + +class TibberRtDataHandler: + """Handle Tibber realtime data.""" + + def __init__(self, async_add_entities, tibber_home, hass): + """Initialize the data handler.""" + self._async_add_entities = async_add_entities + self._tibber_home = tibber_home + self.hass = hass + self._entities = set() + + async def async_callback(self, payload): + """Handle received data.""" + errors = payload.get("errors") + if errors: + _LOGGER.error(errors[0]) + return + data = payload.get("data") + if data is None: + return + live_measurement = data.get("liveMeasurement") + if live_measurement is None: + return + + new_entities = [] + for sensor_type, state in live_measurement.items(): + if state is None or sensor_type not in RT_SENSOR_MAP: + continue + if sensor_type in self._entities: + async_dispatcher_send( + self.hass, + SIGNAL_UPDATE_ENTITY.format(RT_SENSOR_MAP[sensor_type][0]), + state, + ) + else: + sensor_name, device_class, unit = RT_SENSOR_MAP[sensor_type] + if sensor_type == "accumulatedCost": + unit = self._tibber_home.currency + entity = TibberSensorRT( + self._tibber_home, sensor_name, device_class, unit, state + ) + new_entities.append(entity) + self._entities.add(sensor_type) + if new_entities: + self._async_add_entities(new_entities) diff --git a/requirements_all.txt b/requirements_all.txt index f2ff61a58d2..4490c2e2a46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1250,7 +1250,7 @@ pyRFXtrx==0.26.1 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.16.2 +pyTibber==0.17.0 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 15721c5599f..c53334f9688 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -687,7 +687,7 @@ pyMetno==0.8.3 pyRFXtrx==0.26.1 # homeassistant.components.tibber -pyTibber==0.16.2 +pyTibber==0.17.0 # homeassistant.components.nextbus py_nextbusnext==0.1.4