Add coordinators to Sense (#129171)

pull/129185/head
Keilin Bickar 2024-10-25 14:45:55 -04:00 committed by GitHub
parent bb36dd3893
commit 68284bed74
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 194 additions and 242 deletions

View File

@ -1,10 +1,8 @@
"""Support for monitoring a Sense energy sensor.""" """Support for monitoring a Sense energy sensor."""
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta
from functools import partial from functools import partial
import logging import logging
from typing import Any
from sense_energy import ( from sense_energy import (
ASyncSenseable, ASyncSenseable,
@ -13,26 +11,18 @@ from sense_energy import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import CONF_TIMEOUT, Platform
CONF_EMAIL, from homeassistant.core import HomeAssistant
CONF_TIMEOUT,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import ( from .const import (
ACTIVE_UPDATE_RATE, ACTIVE_UPDATE_RATE,
SENSE_CONNECT_EXCEPTIONS, SENSE_CONNECT_EXCEPTIONS,
SENSE_DEVICE_UPDATE,
SENSE_TIMEOUT_EXCEPTIONS, SENSE_TIMEOUT_EXCEPTIONS,
SENSE_WEBSOCKET_EXCEPTIONS, SENSE_WEBSOCKET_EXCEPTIONS,
) )
from .coordinator import SenseRealtimeCoordinator, SenseTrendCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -45,14 +35,14 @@ class SenseData:
"""Sense data type.""" """Sense data type."""
data: ASyncSenseable data: ASyncSenseable
trends: DataUpdateCoordinator[Any] trends: SenseTrendCoordinator
rt: SenseRealtimeCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> bool:
"""Set up Sense from a config entry.""" """Set up Sense from a config entry."""
entry_data = entry.data entry_data = entry.data
email = entry_data[CONF_EMAIL]
timeout = entry_data[CONF_TIMEOUT] timeout = entry_data[CONF_TIMEOUT]
access_token = entry_data.get("access_token", "") access_token = entry_data.get("access_token", "")
@ -99,26 +89,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> boo
except SENSE_WEBSOCKET_EXCEPTIONS as err: except SENSE_WEBSOCKET_EXCEPTIONS as err:
raise ConfigEntryNotReady(str(err) or "Error during realtime update") from err raise ConfigEntryNotReady(str(err) or "Error during realtime update") from err
async def _async_update_trend() -> None: trends_coordinator = SenseTrendCoordinator(hass, gateway)
"""Update the trend data.""" realtime_coordinator = SenseRealtimeCoordinator(hass, gateway)
try:
await gateway.update_trend_data()
except (SenseAuthenticationException, SenseMFARequiredException) as err:
_LOGGER.warning("Sense authentication expired")
raise ConfigEntryAuthFailed(err) from err
except SENSE_CONNECT_EXCEPTIONS as err:
raise UpdateFailed(err) from err
trends_coordinator: DataUpdateCoordinator[None] = DataUpdateCoordinator(
hass,
_LOGGER,
name=f"Sense Trends {email}",
update_method=_async_update_trend,
update_interval=timedelta(seconds=300),
)
# Start out as unavailable so we do not report 0 data
# until the update happens
trends_coordinator.last_update_success = False
# This can take longer than 60s and we already know # This can take longer than 60s and we already know
# sense is online since get_discovered_device_data was # sense is online since get_discovered_device_data was
@ -128,40 +100,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: SenseConfigEntry) -> boo
trends_coordinator.async_request_refresh(), trends_coordinator.async_request_refresh(),
"sense.trends-coordinator-refresh", "sense.trends-coordinator-refresh",
) )
entry.async_create_background_task(
hass,
realtime_coordinator.async_request_refresh(),
"sense.realtime-coordinator-refresh",
)
entry.runtime_data = SenseData( entry.runtime_data = SenseData(
data=gateway, data=gateway,
trends=trends_coordinator, trends=trends_coordinator,
rt=realtime_coordinator,
) )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def async_sense_update(_) -> None:
"""Retrieve latest state."""
try:
await gateway.update_realtime()
except SENSE_TIMEOUT_EXCEPTIONS as ex:
_LOGGER.error("Timeout retrieving data: %s", ex)
except SENSE_WEBSOCKET_EXCEPTIONS as ex:
_LOGGER.error("Failed to update data: %s", ex)
async_dispatcher_send(hass, f"{SENSE_DEVICE_UPDATE}-{gateway.sense_monitor_id}")
remove_update_callback = async_track_time_interval(
hass, async_sense_update, timedelta(seconds=ACTIVE_UPDATE_RATE)
)
@callback
def _remove_update_callback_at_stop(event) -> None:
remove_update_callback()
entry.async_on_unload(remove_update_callback)
entry.async_on_unload(
hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, _remove_update_callback_at_stop
)
)
return True return True

View File

@ -8,13 +8,14 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import SenseConfigEntry from . import SenseConfigEntry
from .const import ATTRIBUTION, DOMAIN, MDI_ICONS, SENSE_DEVICE_UPDATE from .const import ATTRIBUTION, DOMAIN, MDI_ICONS
from .coordinator import SenseRealtimeCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -26,8 +27,10 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the Sense binary sensor.""" """Set up the Sense binary sensor."""
sense_monitor_id = config_entry.runtime_data.data.sense_monitor_id sense_monitor_id = config_entry.runtime_data.data.sense_monitor_id
realtime_coordinator = config_entry.runtime_data.rt
devices = [ devices = [
SenseBinarySensor(device, sense_monitor_id) SenseBinarySensor(device, sense_monitor_id, realtime_coordinator)
for device in config_entry.runtime_data.data.devices for device in config_entry.runtime_data.data.devices
] ]
@ -41,19 +44,25 @@ def sense_to_mdi(sense_icon: str) -> str:
return f"mdi:{MDI_ICONS.get(sense_icon, "power-plug")}" return f"mdi:{MDI_ICONS.get(sense_icon, "power-plug")}"
class SenseBinarySensor(BinarySensorEntity): class SenseBinarySensor(
CoordinatorEntity[SenseRealtimeCoordinator], BinarySensorEntity
):
"""Implementation of a Sense energy device binary sensor.""" """Implementation of a Sense energy device binary sensor."""
_attr_attribution = ATTRIBUTION _attr_attribution = ATTRIBUTION
_attr_should_poll = False _attr_should_poll = False
_attr_available = False
_attr_device_class = BinarySensorDeviceClass.POWER _attr_device_class = BinarySensorDeviceClass.POWER
def __init__(self, device: SenseDevice, sense_monitor_id: str) -> None: def __init__(
self,
device: SenseDevice,
sense_monitor_id: str,
coordinator: SenseRealtimeCoordinator,
) -> None:
"""Initialize the Sense binary sensor.""" """Initialize the Sense binary sensor."""
super().__init__(coordinator)
self._attr_name = device.name self._attr_name = device.name
self._id = device.id self._id = device.id
self._sense_monitor_id = sense_monitor_id
self._attr_unique_id = f"{sense_monitor_id}-{self._id}" self._attr_unique_id = f"{sense_monitor_id}-{self._id}"
self._attr_icon = sense_to_mdi(device.icon) self._attr_icon = sense_to_mdi(device.icon)
self._device = device self._device = device
@ -63,25 +72,10 @@ class SenseBinarySensor(BinarySensorEntity):
"""Return the old not so unique id of the binary sensor.""" """Return the old not so unique id of the binary sensor."""
return self._id return self._id
async def async_added_to_hass(self) -> None: @property
"""Register callbacks.""" def is_on(self) -> bool:
self.async_on_remove( """Return the state of the sensor."""
async_dispatcher_connect( return self._device.is_on
self.hass,
f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
self._async_update_from_data,
)
)
@callback
def _async_update_from_data(self) -> None:
"""Get the latest data, update state. Must not do I/O."""
new_state = self._device.is_on
if self._attr_available and self._attr_is_on == new_state:
return
self._attr_available = True
self._attr_is_on = new_state
self.async_write_ha_state()
async def _migrate_old_unique_ids( async def _migrate_old_unique_ids(

View File

@ -11,6 +11,7 @@ from sense_energy import (
DOMAIN = "sense" DOMAIN = "sense"
DEFAULT_TIMEOUT = 30 DEFAULT_TIMEOUT = 30
ACTIVE_UPDATE_RATE = 60 ACTIVE_UPDATE_RATE = 60
TREND_UPDATE_RATE = 300
DEFAULT_NAME = "Sense" DEFAULT_NAME = "Sense"
SENSE_DEVICE_UPDATE = "sense_devices_update" SENSE_DEVICE_UPDATE = "sense_devices_update"

View File

@ -0,0 +1,76 @@
"""Sense Coordinators."""
from datetime import timedelta
import logging
from sense_energy import (
ASyncSenseable,
SenseAuthenticationException,
SenseMFARequiredException,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
ACTIVE_UPDATE_RATE,
SENSE_CONNECT_EXCEPTIONS,
SENSE_TIMEOUT_EXCEPTIONS,
SENSE_WEBSOCKET_EXCEPTIONS,
TREND_UPDATE_RATE,
)
_LOGGER = logging.getLogger(__name__)
class SenseCoordinator(DataUpdateCoordinator[None]):
"""Sense Trend Coordinator."""
def __init__(
self, hass: HomeAssistant, gateway: ASyncSenseable, name: str, update: int
) -> None:
"""Initialize."""
super().__init__(
hass,
logger=_LOGGER,
name=f"Sense {name} {gateway.sense_monitor_id}",
update_interval=timedelta(seconds=update),
)
self._gateway = gateway
self.last_update_success = False
class SenseTrendCoordinator(SenseCoordinator):
"""Sense Trend Coordinator."""
def __init__(self, hass: HomeAssistant, gateway: ASyncSenseable) -> None:
"""Initialize."""
super().__init__(hass, gateway, "Trends", TREND_UPDATE_RATE)
async def _async_update_data(self) -> None:
"""Update the trend data."""
try:
await self._gateway.update_trend_data()
except (SenseAuthenticationException, SenseMFARequiredException) as err:
_LOGGER.warning("Sense authentication expired")
raise ConfigEntryAuthFailed(err) from err
except SENSE_CONNECT_EXCEPTIONS as err:
raise UpdateFailed(err) from err
class SenseRealtimeCoordinator(SenseCoordinator):
"""Sense Realtime Coordinator."""
def __init__(self, hass: HomeAssistant, gateway: ASyncSenseable) -> None:
"""Initialize."""
super().__init__(hass, gateway, "Realtime", ACTIVE_UPDATE_RATE)
async def _async_update_data(self) -> None:
"""Retrieve latest state."""
try:
await self._gateway.update_realtime()
except SENSE_TIMEOUT_EXCEPTIONS as ex:
_LOGGER.error("Timeout retrieving data: %s", ex)
except SENSE_WEBSOCKET_EXCEPTIONS as ex:
_LOGGER.error("Failed to update data: %s", ex)

View File

@ -1,7 +1,6 @@
"""Support for monitoring a Sense energy sensor.""" """Support for monitoring a Sense energy sensor."""
from datetime import datetime from datetime import datetime
from typing import Any
from sense_energy import ASyncSenseable, Scale from sense_energy import ASyncSenseable, Scale
from sense_energy.sense_api import SenseDevice from sense_energy.sense_api import SenseDevice
@ -17,14 +16,10 @@ from homeassistant.const import (
UnitOfEnergy, UnitOfEnergy,
UnitOfPower, UnitOfPower,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import CoordinatorEntity
CoordinatorEntity,
DataUpdateCoordinator,
)
from . import SenseConfigEntry from . import SenseConfigEntry
from .const import ( from .const import (
@ -43,12 +38,16 @@ from .const import (
PRODUCTION_NAME, PRODUCTION_NAME,
PRODUCTION_PCT_ID, PRODUCTION_PCT_ID,
PRODUCTION_PCT_NAME, PRODUCTION_PCT_NAME,
SENSE_DEVICE_UPDATE,
SOLAR_POWERED_ID, SOLAR_POWERED_ID,
SOLAR_POWERED_NAME, SOLAR_POWERED_NAME,
TO_GRID_ID, TO_GRID_ID,
TO_GRID_NAME, TO_GRID_NAME,
) )
from .coordinator import (
SenseCoordinator,
SenseRealtimeCoordinator,
SenseTrendCoordinator,
)
# Sensor types/ranges # Sensor types/ranges
TRENDS_SENSOR_TYPES = { TRENDS_SENSOR_TYPES = {
@ -86,6 +85,7 @@ async def async_setup_entry(
"""Set up the Sense sensor.""" """Set up the Sense sensor."""
data = config_entry.runtime_data.data data = config_entry.runtime_data.data
trends_coordinator = config_entry.runtime_data.trends trends_coordinator = config_entry.runtime_data.trends
realtime_coordinator = config_entry.runtime_data.rt
# Request only in case it takes longer # Request only in case it takes longer
# than 60s # than 60s
@ -94,22 +94,19 @@ async def async_setup_entry(
sense_monitor_id = data.sense_monitor_id sense_monitor_id = data.sense_monitor_id
entities: list[SensorEntity] = [ entities: list[SensorEntity] = [
SenseDevicePowerSensor(device, sense_monitor_id) SenseDevicePowerSensor(device, sense_monitor_id, realtime_coordinator)
for device in config_entry.runtime_data.data.devices for device in config_entry.runtime_data.data.devices
] ]
for variant_id, variant_name in SENSOR_VARIANTS: for variant_id, variant_name in SENSOR_VARIANTS:
entities.append( entities.append(
SensePowerSensor( SensePowerSensor(
data, data, sense_monitor_id, variant_id, variant_name, realtime_coordinator
sense_monitor_id,
variant_id,
variant_name,
) )
) )
entities.extend( entities.extend(
SenseVoltageSensor(data, i, sense_monitor_id) SenseVoltageSensor(data, i, sense_monitor_id, realtime_coordinator)
for i in range(len(data.active_voltage)) for i in range(len(data.active_voltage))
) )
@ -129,14 +126,28 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
class SensePowerSensor(SensorEntity): class SenseBaseSensor(CoordinatorEntity[SenseCoordinator], SensorEntity):
"""Base implementation of a Sense sensor."""
_attr_attribution = ATTRIBUTION
_attr_should_poll = False
def __init__(
self,
coordinator: SenseCoordinator,
sense_monitor_id: str,
unique_id: str,
) -> None:
"""Initialize the Sense sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{sense_monitor_id}-{unique_id}"
class SensePowerSensor(SenseBaseSensor):
"""Implementation of a Sense energy sensor.""" """Implementation of a Sense energy sensor."""
_attr_device_class = SensorDeviceClass.POWER _attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = UnitOfPower.WATT _attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_attribution = ATTRIBUTION
_attr_should_poll = False
_attr_available = False
_attr_state_class = SensorStateClass.MEASUREMENT _attr_state_class = SensorStateClass.MEASUREMENT
def __init__( def __init__(
@ -145,106 +156,71 @@ class SensePowerSensor(SensorEntity):
sense_monitor_id: str, sense_monitor_id: str,
variant_id: str, variant_id: str,
variant_name: str, variant_name: str,
realtime_coordinator: SenseRealtimeCoordinator,
) -> None: ) -> None:
"""Initialize the Sense sensor.""" """Initialize the Sense sensor."""
self._attr_name = f"{ACTIVE_NAME} {variant_name}" super().__init__(
self._attr_unique_id = f"{sense_monitor_id}-{ACTIVE_TYPE}-{variant_id}" realtime_coordinator, sense_monitor_id, f"{ACTIVE_TYPE}-{variant_id}"
self._data = data
self._sense_monitor_id = sense_monitor_id
self._variant_id = variant_id
self._variant_name = variant_name
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
self._async_update_from_data,
)
) )
self._attr_name = f"{ACTIVE_NAME} {variant_name}"
self._data = data
self._variant_id = variant_id
@callback @property
def _async_update_from_data(self) -> None: def native_value(self) -> float:
"""Update the sensor from the data. Must not do I/O.""" """Return the state of the sensor."""
new_state = round( return round(
self._data.active_solar_power self._data.active_solar_power
if self._variant_id == PRODUCTION_ID if self._variant_id == PRODUCTION_ID
else self._data.active_power else self._data.active_power
) )
if self._attr_available and self._attr_native_value == new_state:
return
self._attr_native_value = new_state
self._attr_available = True
self.async_write_ha_state()
class SenseVoltageSensor(SensorEntity): class SenseVoltageSensor(SenseBaseSensor):
"""Implementation of a Sense energy voltage sensor.""" """Implementation of a Sense energy voltage sensor."""
_attr_device_class = SensorDeviceClass.VOLTAGE _attr_device_class = SensorDeviceClass.VOLTAGE
_attr_state_class = SensorStateClass.MEASUREMENT _attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT _attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
_attr_attribution = ATTRIBUTION
_attr_should_poll = False
_attr_available = False
def __init__( def __init__(
self, self,
data: ASyncSenseable, data: ASyncSenseable,
index: int, index: int,
sense_monitor_id: str, sense_monitor_id: str,
realtime_coordinator: SenseRealtimeCoordinator,
) -> None: ) -> None:
"""Initialize the Sense sensor.""" """Initialize the Sense sensor."""
line_num = index + 1 super().__init__(realtime_coordinator, sense_monitor_id, f"L{index + 1}")
self._attr_name = f"L{line_num} Voltage" self._attr_name = f"L{index + 1} Voltage"
self._attr_unique_id = f"{sense_monitor_id}-L{line_num}"
self._data = data self._data = data
self._sense_monitor_id = sense_monitor_id
self._voltage_index = index self._voltage_index = index
async def async_added_to_hass(self) -> None: @property
"""Register callbacks.""" def native_value(self) -> float:
self.async_on_remove( """Return the state of the sensor."""
async_dispatcher_connect( return round(self._data.active_voltage[self._voltage_index], 1)
self.hass,
f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
self._async_update_from_data,
)
)
@callback
def _async_update_from_data(self) -> None:
"""Update the sensor from the data. Must not do I/O."""
new_state = round(self._data.active_voltage[self._voltage_index], 1)
if self._attr_available and self._attr_native_value == new_state:
return
self._attr_available = True
self._attr_native_value = new_state
self.async_write_ha_state()
class SenseTrendsSensor(CoordinatorEntity, SensorEntity): class SenseTrendsSensor(SenseBaseSensor):
"""Implementation of a Sense energy sensor.""" """Implementation of a Sense energy sensor."""
_attr_attribution = ATTRIBUTION
_attr_should_poll = False
def __init__( def __init__(
self, self,
data: ASyncSenseable, data: ASyncSenseable,
scale: Scale, scale: Scale,
variant_id: str, variant_id: str,
variant_name: str, variant_name: str,
trends_coordinator: DataUpdateCoordinator[Any], trends_coordinator: SenseTrendCoordinator,
sense_monitor_id: str, sense_monitor_id: str,
) -> None: ) -> None:
"""Initialize the Sense sensor.""" """Initialize the Sense sensor."""
super().__init__(trends_coordinator) super().__init__(
self._attr_name = f"{TRENDS_SENSOR_TYPES[scale]} {variant_name}" trends_coordinator,
self._attr_unique_id = ( sense_monitor_id,
f"{sense_monitor_id}-{TRENDS_SENSOR_TYPES[scale].lower()}-{variant_id}" f"{TRENDS_SENSOR_TYPES[scale].lower()}-{variant_id}",
) )
self._attr_name = f"{TRENDS_SENSOR_TYPES[scale]} {variant_name}"
self._data = data self._data = data
self._scale = scale self._scale = scale
self._variant_id = variant_id self._variant_id = variant_id
@ -279,41 +255,29 @@ class SenseTrendsSensor(CoordinatorEntity, SensorEntity):
return None return None
class SenseDevicePowerSensor(SensorEntity): class SenseDevicePowerSensor(SenseBaseSensor):
"""Implementation of a Sense energy device.""" """Implementation of a Sense energy device."""
_attr_available = False
_attr_state_class = SensorStateClass.MEASUREMENT _attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = UnitOfPower.WATT _attr_native_unit_of_measurement = UnitOfPower.WATT
_attr_attribution = ATTRIBUTION
_attr_device_class = SensorDeviceClass.POWER _attr_device_class = SensorDeviceClass.POWER
_attr_should_poll = False
def __init__(self, device: SenseDevice, sense_monitor_id: str) -> None: def __init__(
self,
device: SenseDevice,
sense_monitor_id: str,
realtime_coordinator: SenseRealtimeCoordinator,
) -> None:
"""Initialize the Sense binary sensor.""" """Initialize the Sense binary sensor."""
super().__init__(
realtime_coordinator, sense_monitor_id, f"{device.id}-{CONSUMPTION_ID}"
)
self._attr_name = f"{device.name} {CONSUMPTION_NAME}" self._attr_name = f"{device.name} {CONSUMPTION_NAME}"
self._id = device.id self._id = device.id
self._sense_monitor_id = sense_monitor_id
self._attr_unique_id = f"{sense_monitor_id}-{self._id}-{CONSUMPTION_ID}"
self._attr_icon = sense_to_mdi(device.icon) self._attr_icon = sense_to_mdi(device.icon)
self._device = device self._device = device
async def async_added_to_hass(self) -> None: @property
"""Register callbacks.""" def native_value(self) -> float:
self.async_on_remove( """Return the state of the sensor."""
async_dispatcher_connect( return self._device.power_w
self.hass,
f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}",
self._async_update_from_data,
)
)
@callback
def _async_update_from_data(self) -> None:
"""Get the latest data, update state. Must not do I/O."""
new_state = self._device.power_w
if self._attr_available and self._attr_native_value == new_state:
return
self._attr_native_value = new_state
self._attr_available = True
self.async_write_ha_state()

View File

@ -45,7 +45,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unavailable', 'state': 'off',
}) })
# --- # ---
# name: test_binary_sensors[binary_sensor.oven-entry] # name: test_binary_sensors[binary_sensor.oven-entry]
@ -94,6 +94,6 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unavailable', 'state': 'off',
}) })
# --- # ---

View File

@ -410,7 +410,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unavailable', 'state': '100.0',
}) })
# --- # ---
# name: test_sensors[sensor.daily_from_grid-entry] # name: test_sensors[sensor.daily_from_grid-entry]
@ -823,7 +823,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unavailable', 'state': '500',
}) })
# --- # ---
# name: test_sensors[sensor.energy_usage-entry] # name: test_sensors[sensor.energy_usage-entry]
@ -875,7 +875,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unavailable', 'state': '100',
}) })
# --- # ---
# name: test_sensors[sensor.l1_voltage-entry] # name: test_sensors[sensor.l1_voltage-entry]
@ -927,7 +927,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unavailable', 'state': '120',
}) })
# --- # ---
# name: test_sensors[sensor.l2_voltage-entry] # name: test_sensors[sensor.l2_voltage-entry]
@ -979,7 +979,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unavailable', 'state': '240',
}) })
# --- # ---
# name: test_sensors[sensor.monthly_from_grid-entry] # name: test_sensors[sensor.monthly_from_grid-entry]
@ -1393,7 +1393,7 @@
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,
'state': 'unavailable', 'state': '50.0',
}) })
# --- # ---
# name: test_sensors[sensor.weekly_from_grid-entry] # name: test_sensors[sensor.weekly_from_grid-entry]

View File

@ -7,7 +7,7 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.sense.const import ACTIVE_UPDATE_RATE from homeassistant.components.sense.const import ACTIVE_UPDATE_RATE
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
@ -40,15 +40,6 @@ async def test_on_off_sensors(
await setup_platform(hass, config_entry, BINARY_SENSOR_DOMAIN) await setup_platform(hass, config_entry, BINARY_SENSOR_DOMAIN)
device_1, device_2 = mock_sense.devices device_1, device_2 = mock_sense.devices
state = hass.states.get(f"binary_sensor.{DEVICE_1_NAME.lower()}")
assert state.state == STATE_UNAVAILABLE
state = hass.states.get(f"binary_sensor.{DEVICE_2_NAME.lower()}")
assert state.state == STATE_UNAVAILABLE
async_fire_time_changed(hass, utcnow() + timedelta(seconds=ACTIVE_UPDATE_RATE))
await hass.async_block_till_done()
state = hass.states.get(f"binary_sensor.{DEVICE_1_NAME.lower()}") state = hass.states.get(f"binary_sensor.{DEVICE_1_NAME.lower()}")
assert state.state == STATE_OFF assert state.state == STATE_OFF

View File

@ -9,7 +9,7 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components.sense.const import ACTIVE_UPDATE_RATE, CONSUMPTION_ID from homeassistant.components.sense.const import ACTIVE_UPDATE_RATE, CONSUMPTION_ID
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import STATE_UNAVAILABLE, Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
@ -40,19 +40,11 @@ async def test_device_power_sensors(
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Test the Sense device power sensors.""" """Test the Sense device power sensors."""
await setup_platform(hass, config_entry, SENSOR_DOMAIN)
device_1, device_2 = mock_sense.devices device_1, device_2 = mock_sense.devices
state = hass.states.get(f"sensor.{DEVICE_1_NAME.lower()}_{CONSUMPTION_ID}")
assert state.state == STATE_UNAVAILABLE
state = hass.states.get(f"sensor.{DEVICE_2_NAME.lower()}_{CONSUMPTION_ID}")
assert state.state == STATE_UNAVAILABLE
device_1.power_w = 0 device_1.power_w = 0
device_2.power_w = 0 device_2.power_w = 0
async_fire_time_changed(hass, utcnow() + timedelta(seconds=ACTIVE_UPDATE_RATE)) await setup_platform(hass, config_entry, SENSOR_DOMAIN)
await hass.async_block_till_done() device_1, device_2 = mock_sense.devices
state = hass.states.get(f"sensor.{DEVICE_1_NAME.lower()}_{CONSUMPTION_ID}") state = hass.states.get(f"sensor.{DEVICE_1_NAME.lower()}_{CONSUMPTION_ID}")
assert state.state == "0" assert state.state == "0"
@ -90,20 +82,10 @@ async def test_voltage_sensors(
) -> None: ) -> None:
"""Test the Sense voltage sensors.""" """Test the Sense voltage sensors."""
type(mock_sense).active_voltage = PropertyMock(return_value=[0, 0]) type(mock_sense).active_voltage = PropertyMock(return_value=[120, 121])
await setup_platform(hass, config_entry, SENSOR_DOMAIN) await setup_platform(hass, config_entry, SENSOR_DOMAIN)
state = hass.states.get("sensor.l1_voltage")
assert state.state == STATE_UNAVAILABLE
state = hass.states.get("sensor.l2_voltage")
assert state.state == STATE_UNAVAILABLE
type(mock_sense).active_voltage = PropertyMock(return_value=[120, 121])
async_fire_time_changed(hass, utcnow() + timedelta(seconds=ACTIVE_UPDATE_RATE))
await hass.async_block_till_done()
state = hass.states.get("sensor.l1_voltage") state = hass.states.get("sensor.l1_voltage")
assert state.state == "120" assert state.state == "120"
@ -129,18 +111,10 @@ async def test_active_power_sensors(
) -> None: ) -> None:
"""Test the Sense power sensors.""" """Test the Sense power sensors."""
await setup_platform(hass, config_entry, SENSOR_DOMAIN)
state = hass.states.get("sensor.energy_usage")
assert state.state == STATE_UNAVAILABLE
state = hass.states.get("sensor.energy_production")
assert state.state == STATE_UNAVAILABLE
type(mock_sense).active_power = PropertyMock(return_value=400) type(mock_sense).active_power = PropertyMock(return_value=400)
type(mock_sense).active_solar_power = PropertyMock(return_value=500) type(mock_sense).active_solar_power = PropertyMock(return_value=500)
async_fire_time_changed(hass, utcnow() + timedelta(seconds=ACTIVE_UPDATE_RATE))
await hass.async_block_till_done() await setup_platform(hass, config_entry, SENSOR_DOMAIN)
state = hass.states.get("sensor.energy_usage") state = hass.states.get("sensor.energy_usage")
assert state.state == "400" assert state.state == "400"