Files
Mike Marvin 92647f6835 Initial Support for Re-Auth and Reconfiguration (#350)
- Updates to config flows to support re-auth when password changes and
reconfiguration to change the configuration normally set during
initialization.
- Add config option to invert solar data, defaults to true. This should
allow the user to choose if they want it inverted, since the energy
dashboard requires positive data but emporia usually provides negative
for generation.
- A bunch of typing and similar style updates
- Update some translation strings
- Add support for using the API simulator without dev code
2025-02-02 17:26:32 -05:00

174 lines
6.4 KiB
Python

"""Platform for sensor integration."""
from datetime import datetime
import logging
from pyemvue.device import VueDevice, VueDeviceChannel
from pyemvue.enums import Scale
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
_LOGGER: logging.Logger = logging.getLogger(__name__)
# def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
coordinator_1min = hass.data[DOMAIN][config_entry.entry_id]["coordinator_1min"]
coordinator_1mon = hass.data[DOMAIN][config_entry.entry_id]["coordinator_1mon"]
coordinator_day_sensor = hass.data[DOMAIN][config_entry.entry_id][
"coordinator_day_sensor"
]
_LOGGER.info(hass.data[DOMAIN][config_entry.entry_id])
if coordinator_1min:
async_add_entities(
CurrentVuePowerSensor(coordinator_1min, identifier)
for _, identifier in enumerate(coordinator_1min.data)
)
if coordinator_1mon:
async_add_entities(
CurrentVuePowerSensor(coordinator_1mon, identifier)
for _, identifier in enumerate(coordinator_1mon.data)
)
if coordinator_day_sensor:
async_add_entities(
CurrentVuePowerSensor(coordinator_day_sensor, identifier)
for _, identifier in enumerate(coordinator_day_sensor.data)
)
class CurrentVuePowerSensor(CoordinatorEntity, SensorEntity): # type: ignore
"""Representation of a Vue Sensor's current power."""
def __init__(self, coordinator, identifier) -> None:
"""Pass coordinator to CoordinatorEntity."""
super().__init__(coordinator)
self._id = identifier
self._scale: str = coordinator.data[identifier]["scale"]
device_gid: int = coordinator.data[identifier]["device_gid"]
channel_num: str = coordinator.data[identifier]["channel_num"]
self._device: VueDevice = coordinator.data[identifier]["info"]
final_channel: VueDeviceChannel | None = None
if self._device is not None:
for channel in self._device.channels:
if channel.channel_num == channel_num:
final_channel = channel
break
if final_channel is None:
_LOGGER.warning(
"No channel found for device_gid %s and channel_num %s",
device_gid,
channel_num,
)
raise RuntimeError(
f"No channel found for device_gid {device_gid} and channel_num {channel_num}"
)
self._channel: VueDeviceChannel = final_channel
self._iskwh = self.scale_is_energy()
self._attr_has_entity_name = True
if self._iskwh:
self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
self._attr_device_class = SensorDeviceClass.ENERGY
self._attr_state_class = SensorStateClass.TOTAL
self._attr_suggested_display_precision = 3
self._attr_name = f"Energy {self.scale_readable()}"
else:
self._attr_native_unit_of_measurement = UnitOfPower.WATT
self._attr_device_class = SensorDeviceClass.POWER
self._attr_state_class = SensorStateClass.MEASUREMENT
self._attr_suggested_display_precision = 1
self._attr_name = f"Power {self.scale_readable()}"
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
device_name = self._channel.name or self._device.device_name
return DeviceInfo(
identifiers={
(DOMAIN, f"{self._device.device_gid}-{self._channel.channel_num}")
},
name=device_name,
model=self._device.model,
sw_version=self._device.firmware,
manufacturer="Emporia",
)
@property
def last_reset(self) -> datetime | None:
"""Reset time of the daily/monthly sensor. Midnight local time."""
if self._id in self.coordinator.data:
return self.coordinator.data[self._id]["reset"]
return None
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
if self._id in self.coordinator.data:
usage = self.coordinator.data[self._id]["usage"]
return self.scale_usage(usage) if usage is not None else None
return None
@property
def unique_id(self) -> str:
"""Return the Unique ID for the sensor."""
if self._scale == Scale.MINUTE.value:
return (
"sensor.emporia_vue.instant."
f"{self._channel.device_gid}-{self._channel.channel_num}"
)
return (
f"sensor.emporia_vue.{self._scale}."
f"{self._channel.device_gid}-{self._channel.channel_num}"
)
def scale_usage(self, usage):
"""Scales the usage to the correct timescale and magnitude."""
if self._scale == Scale.MINUTE.value:
usage = 60 * 1000 * usage # convert from kwh to w rate
elif self._scale == Scale.SECOND.value:
usage = 3600 * 1000 * usage # convert to rate
elif self._scale == Scale.MINUTES_15.value:
usage = (
4 * 1000 * usage
) # this might never be used but for safety, convert to rate
return usage
def scale_is_energy(self):
"""Return True if the scale is an energy unit instead of power."""
return self._scale not in (
Scale.MINUTE.value,
Scale.SECOND.value,
Scale.MINUTES_15.value,
)
def scale_readable(self):
"""Return a human readable scale."""
if self._scale == Scale.MINUTE.value:
return "Minute Average"
if self._scale == Scale.DAY.value:
return "Today"
if self._scale == Scale.MONTH.value:
return "This Month"
return self._scale