"""Support for monitoring a Sense energy sensor.""" from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, SensorEntity, ) from homeassistant.const import ( ATTR_ATTRIBUTION, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, ELECTRIC_POTENTIAL_VOLT, ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( ACTIVE_NAME, ACTIVE_TYPE, ATTRIBUTION, CONSUMPTION_ID, CONSUMPTION_NAME, DOMAIN, FROM_GRID_ID, FROM_GRID_NAME, ICON, MDI_ICONS, NET_PRODUCTION_ID, NET_PRODUCTION_NAME, PRODUCTION_ID, PRODUCTION_NAME, PRODUCTION_PCT_ID, PRODUCTION_PCT_NAME, SENSE_DATA, SENSE_DEVICE_UPDATE, SENSE_DEVICES_DATA, SENSE_DISCOVERED_DEVICES_DATA, SENSE_TRENDS_COORDINATOR, SOLAR_POWERED_ID, SOLAR_POWERED_NAME, TO_GRID_ID, TO_GRID_NAME, ) class SensorConfig: """Data structure holding sensor configuration.""" def __init__(self, name, sensor_type): """Sensor name and type to pass to API.""" self.name = name self.sensor_type = sensor_type # Sensor types/ranges ACTIVE_SENSOR_TYPE = SensorConfig(ACTIVE_NAME, ACTIVE_TYPE) # Sensor types/ranges TRENDS_SENSOR_TYPES = { "daily": SensorConfig("Daily", "DAY"), "weekly": SensorConfig("Weekly", "WEEK"), "monthly": SensorConfig("Monthly", "MONTH"), "yearly": SensorConfig("Yearly", "YEAR"), } # Production/consumption variants SENSOR_VARIANTS = [(PRODUCTION_ID, PRODUCTION_NAME), (CONSUMPTION_ID, CONSUMPTION_NAME)] # Trend production/consumption variants TREND_SENSOR_VARIANTS = SENSOR_VARIANTS + [ (PRODUCTION_PCT_ID, PRODUCTION_PCT_NAME), (NET_PRODUCTION_ID, NET_PRODUCTION_NAME), (FROM_GRID_ID, FROM_GRID_NAME), (TO_GRID_ID, TO_GRID_NAME), (SOLAR_POWERED_ID, SOLAR_POWERED_NAME), ] def sense_to_mdi(sense_icon): """Convert sense icon to mdi icon.""" return "mdi:{}".format(MDI_ICONS.get(sense_icon, "power-plug")) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Sense sensor.""" data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DATA] sense_devices_data = hass.data[DOMAIN][config_entry.entry_id][SENSE_DEVICES_DATA] trends_coordinator = hass.data[DOMAIN][config_entry.entry_id][ SENSE_TRENDS_COORDINATOR ] # Request only in case it takes longer # than 60s await trends_coordinator.async_request_refresh() sense_monitor_id = data.sense_monitor_id sense_devices = hass.data[DOMAIN][config_entry.entry_id][ SENSE_DISCOVERED_DEVICES_DATA ] devices = [ SenseEnergyDevice(sense_devices_data, device, sense_monitor_id) for device in sense_devices if device["tags"]["DeviceListAllowed"] == "true" ] for variant_id, variant_name in SENSOR_VARIANTS: name = ACTIVE_SENSOR_TYPE.name sensor_type = ACTIVE_SENSOR_TYPE.sensor_type unique_id = f"{sense_monitor_id}-active-{variant_id}" devices.append( SenseActiveSensor( data, name, sensor_type, sense_monitor_id, variant_id, variant_name, unique_id, ) ) for i in range(len(data.active_voltage)): devices.append(SenseVoltageSensor(data, i, sense_monitor_id)) for type_id, typ in TRENDS_SENSOR_TYPES.items(): for variant_id, variant_name in TREND_SENSOR_VARIANTS: name = typ.name sensor_type = typ.sensor_type unique_id = f"{sense_monitor_id}-{type_id}-{variant_id}" devices.append( SenseTrendsSensor( data, name, sensor_type, variant_id, variant_name, trends_coordinator, unique_id, ) ) async_add_entities(devices) class SenseActiveSensor(SensorEntity): """Implementation of a Sense energy sensor.""" _attr_icon = ICON _attr_native_unit_of_measurement = POWER_WATT _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_should_poll = False _attr_available = False _attr_state_class = STATE_CLASS_MEASUREMENT def __init__( self, data, name, sensor_type, sense_monitor_id, variant_id, variant_name, unique_id, ): """Initialize the Sense sensor.""" self._attr_name = f"{name} {variant_name}" self._attr_unique_id = unique_id self._data = data self._sense_monitor_id = sense_monitor_id self._sensor_type = sensor_type self._variant_id = variant_id self._variant_name = variant_name async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( self.hass, f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}", self._async_update_from_data, ) ) @callback def _async_update_from_data(self): """Update the sensor from the data. Must not do I/O.""" new_state = round( self._data.active_solar_power if self._variant_id == PRODUCTION_ID 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): """Implementation of a Sense energy voltage sensor.""" _attr_native_unit_of_measurement = ELECTRIC_POTENTIAL_VOLT _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON _attr_should_poll = False _attr_available = False def __init__( self, data, index, sense_monitor_id, ): """Initialize the Sense sensor.""" line_num = index + 1 self._attr_name = f"L{line_num} Voltage" self._attr_unique_id = f"{sense_monitor_id}-L{line_num}" self._data = data self._sense_monitor_id = sense_monitor_id self._voltage_index = index async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( self.hass, f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}", self._async_update_from_data, ) ) @callback def _async_update_from_data(self): """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(SensorEntity): """Implementation of a Sense energy sensor.""" _attr_device_class = DEVICE_CLASS_ENERGY _attr_state_class = STATE_CLASS_TOTAL_INCREASING _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_icon = ICON _attr_should_poll = False def __init__( self, data, name, sensor_type, variant_id, variant_name, trends_coordinator, unique_id, ): """Initialize the Sense sensor.""" self._attr_name = f"{name} {variant_name}" self._attr_unique_id = unique_id self._data = data self._sensor_type = sensor_type self._coordinator = trends_coordinator self._variant_id = variant_id self._had_any_update = False if variant_id in [PRODUCTION_PCT_ID, SOLAR_POWERED_ID]: self._attr_native_unit_of_measurement = PERCENTAGE self._attr_entity_registry_enabled_default = False self._attr_state_class = None self._attr_device_class = None @property def native_value(self): """Return the state of the sensor.""" return round(self._data.get_trend(self._sensor_type, self._variant_id), 1) @property def available(self): """Return if entity is available.""" return self._had_any_update and self._coordinator.last_update_success @callback def _async_update(self): """Track if we had an update so we do not report zero data.""" self._had_any_update = True self.async_write_ha_state() async def async_update(self): """Update the entity. Only used by the generic entity update service. """ await self._coordinator.async_request_refresh() async def async_added_to_hass(self): """When entity is added to hass.""" self.async_on_remove(self._coordinator.async_add_listener(self._async_update)) class SenseEnergyDevice(SensorEntity): """Implementation of a Sense energy device.""" _attr_available = False _attr_state_class = STATE_CLASS_MEASUREMENT _attr_native_unit_of_measurement = POWER_WATT _attr_extra_state_attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} _attr_device_class = DEVICE_CLASS_POWER _attr_should_poll = False def __init__(self, sense_devices_data, device, sense_monitor_id): """Initialize the Sense binary sensor.""" self._attr_name = f"{device['name']} {CONSUMPTION_NAME}" 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._sense_devices_data = sense_devices_data async def async_added_to_hass(self): """Register callbacks.""" self.async_on_remove( async_dispatcher_connect( self.hass, f"{SENSE_DEVICE_UPDATE}-{self._sense_monitor_id}", self._async_update_from_data, ) ) @callback def _async_update_from_data(self): """Get the latest data, update state. Must not do I/O.""" device_data = self._sense_devices_data.get_device_by_id(self._id) if not device_data or "w" not in device_data: new_state = 0 else: new_state = int(device_data["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()