diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py index b3cfebe9abc..e32f1d85564 100644 --- a/homeassistant/components/solarlog/__init__.py +++ b/homeassistant/components/solarlog/__init__.py @@ -1,12 +1,28 @@ """Solar-Log integration.""" +from datetime import timedelta +import logging +from urllib.parse import ParseResult, urlparse + +from requests.exceptions import HTTPError, Timeout +from sunwatcher.solarlog.solarlog import SolarLog + from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant +from homeassistant.helpers import update_coordinator + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) PLATFORMS = ["sensor"] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry for solarlog.""" + coordinator = SolarlogData(hass, entry) + await coordinator.async_config_entry_first_refresh() + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator hass.config_entries.async_setup_platforms(entry, PLATFORMS) return True @@ -14,3 +30,73 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass, entry): """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +class SolarlogData(update_coordinator.DataUpdateCoordinator): + """Get and update the latest data.""" + + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + """Initialize the data object.""" + super().__init__( + hass, _LOGGER, name="SolarLog", update_interval=timedelta(seconds=60) + ) + + host_entry = entry.data[CONF_HOST] + + url = urlparse(host_entry, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + self.unique_id = entry.entry_id + self.name = entry.title + self.host = url.geturl() + + async def _async_update_data(self): + """Update the data from the SolarLog device.""" + try: + api = await self.hass.async_add_executor_job(SolarLog, self.host) + except (OSError, Timeout, HTTPError) as err: + raise update_coordinator.UpdateFailed(err) + + if api.time.year == 1999: + raise update_coordinator.UpdateFailed( + "Invalid data returned (can happen after Solarlog restart)." + ) + + self.logger.debug( + "Connection to Solarlog successful. Retrieving latest Solarlog update of %s", + api.time, + ) + + data = {} + + try: + data["TIME"] = api.time + data["powerAC"] = api.power_ac + data["powerDC"] = api.power_dc + data["voltageAC"] = api.voltage_ac + data["voltageDC"] = api.voltage_dc + data["yieldDAY"] = api.yield_day / 1000 + data["yieldYESTERDAY"] = api.yield_yesterday / 1000 + data["yieldMONTH"] = api.yield_month / 1000 + data["yieldYEAR"] = api.yield_year / 1000 + data["yieldTOTAL"] = api.yield_total / 1000 + data["consumptionAC"] = api.consumption_ac + data["consumptionDAY"] = api.consumption_day / 1000 + data["consumptionYESTERDAY"] = api.consumption_yesterday / 1000 + data["consumptionMONTH"] = api.consumption_month / 1000 + data["consumptionYEAR"] = api.consumption_year / 1000 + data["consumptionTOTAL"] = api.consumption_total / 1000 + data["totalPOWER"] = api.total_power + data["alternatorLOSS"] = api.alternator_loss + data["CAPACITY"] = round(api.capacity * 100, 0) + data["EFFICIENCY"] = round(api.efficiency * 100, 0) + data["powerAVAILABLE"] = api.power_available + data["USAGE"] = round(api.usage * 100, 0) + except AttributeError as err: + raise update_coordinator.UpdateFailed( + f"Missing details data in Solarlog response: {err}" + ) from err + + _LOGGER.debug("Updated Solarlog overview data: %s", data) + return data diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py index e4e10b3a7e6..eecf73b6a09 100644 --- a/homeassistant/components/solarlog/const.py +++ b/homeassistant/components/solarlog/const.py @@ -2,7 +2,6 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import timedelta from homeassistant.components.sensor import ( STATE_CLASS_MEASUREMENT, @@ -23,13 +22,10 @@ from homeassistant.const import ( DOMAIN = "solarlog" -"""Default config for solarlog.""" +# Default config for solarlog. DEFAULT_HOST = "http://solar-log" DEFAULT_NAME = "solarlog" -"""Fixed constants.""" -SCAN_INTERVAL = timedelta(seconds=60) - @dataclass class SolarlogRequiredKeysMixin: diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py index e87977f64e5..ee7425cf2d7 100644 --- a/homeassistant/components/solarlog/sensor.py +++ b/homeassistant/components/solarlog/sensor.py @@ -1,133 +1,42 @@ """Platform for solarlog sensors.""" -import logging -from urllib.parse import ParseResult, urlparse - -from requests.exceptions import HTTPError, Timeout -from sunwatcher.solarlog.solarlog import SolarLog - from homeassistant.components.sensor import SensorEntity -from homeassistant.const import CONF_HOST -from homeassistant.util import Throttle +from homeassistant.helpers import update_coordinator +from homeassistant.helpers.entity import StateType -from .const import DOMAIN, SCAN_INTERVAL, SENSOR_TYPES, SolarLogSensorEntityDescription - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the solarlog platform.""" - _LOGGER.warning( - "Configuration of the solarlog platform in configuration.yaml is deprecated " - "in Home Assistant 0.119. Please remove entry from your configuration" - ) +from . import SolarlogData +from .const import DOMAIN, SENSOR_TYPES, SolarLogSensorEntityDescription async def async_setup_entry(hass, entry, async_add_entities): """Add solarlog entry.""" - host_entry = entry.data[CONF_HOST] - device_name = entry.title - - url = urlparse(host_entry, "http") - netloc = url.netloc or url.path - path = url.path if url.netloc else "" - url = ParseResult("http", netloc, path, *url[3:]) - host = url.geturl() - - try: - api = await hass.async_add_executor_job(SolarLog, host) - _LOGGER.debug("Connected to Solar-Log device, setting up entries") - except (OSError, HTTPError, Timeout): - _LOGGER.error( - "Could not connect to Solar-Log device at %s, check host ip address", host - ) - return - - # Create solarlog data service which will retrieve and update the data. - data = await hass.async_add_executor_job(SolarlogData, hass, api, host) - - # Create a new sensor for each sensor type. - entities = [ - SolarlogSensor(entry.entry_id, device_name, data, description) - for description in SENSOR_TYPES - ] - async_add_entities(entities, True) - return True + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SolarlogSensor(coordinator, description) for description in SENSOR_TYPES + ) -class SolarlogData: - """Get and update the latest data.""" - - def __init__(self, hass, api, host): - """Initialize the data object.""" - self.api = api - self.hass = hass - self.host = host - self.update = Throttle(SCAN_INTERVAL)(self._update) - self.data = {} - - def _update(self): - """Update the data from the SolarLog device.""" - try: - self.api = SolarLog(self.host) - response = self.api.time - _LOGGER.debug( - "Connection to Solarlog successful. Retrieving latest Solarlog update of %s", - response, - ) - except (OSError, Timeout, HTTPError): - _LOGGER.error("Connection error, Could not retrieve data, skipping update") - return - - try: - self.data["TIME"] = self.api.time - self.data["powerAC"] = self.api.power_ac - self.data["powerDC"] = self.api.power_dc - self.data["voltageAC"] = self.api.voltage_ac - self.data["voltageDC"] = self.api.voltage_dc - self.data["yieldDAY"] = self.api.yield_day / 1000 - self.data["yieldYESTERDAY"] = self.api.yield_yesterday / 1000 - self.data["yieldMONTH"] = self.api.yield_month / 1000 - self.data["yieldYEAR"] = self.api.yield_year / 1000 - self.data["yieldTOTAL"] = self.api.yield_total / 1000 - self.data["consumptionAC"] = self.api.consumption_ac - self.data["consumptionDAY"] = self.api.consumption_day / 1000 - self.data["consumptionYESTERDAY"] = self.api.consumption_yesterday / 1000 - self.data["consumptionMONTH"] = self.api.consumption_month / 1000 - self.data["consumptionYEAR"] = self.api.consumption_year / 1000 - self.data["consumptionTOTAL"] = self.api.consumption_total / 1000 - self.data["totalPOWER"] = self.api.total_power - self.data["alternatorLOSS"] = self.api.alternator_loss - self.data["CAPACITY"] = round(self.api.capacity * 100, 0) - self.data["EFFICIENCY"] = round(self.api.efficiency * 100, 0) - self.data["powerAVAILABLE"] = self.api.power_available - self.data["USAGE"] = round(self.api.usage * 100, 0) - _LOGGER.debug("Updated Solarlog overview data: %s", self.data) - except AttributeError: - _LOGGER.error("Missing details data in Solarlog response") - - -class SolarlogSensor(SensorEntity): +class SolarlogSensor(update_coordinator.CoordinatorEntity, SensorEntity): """Representation of a Sensor.""" + entity_description: SolarLogSensorEntityDescription + def __init__( self, - entry_id: str, - device_name: str, - data: SolarlogData, + coordinator: SolarlogData, description: SolarLogSensorEntityDescription, ) -> None: """Initialize the sensor.""" + super().__init__(coordinator) self.entity_description = description - self.data = data - self._attr_name = f"{device_name} {description.name}" - self._attr_unique_id = f"{entry_id}_{description.key}" + self._attr_name = f"{coordinator.name} {description.name}" + self._attr_unique_id = f"{coordinator.unique_id}_{description.key}" self._attr_device_info = { - "identifiers": {(DOMAIN, entry_id)}, - "name": device_name, + "identifiers": {(DOMAIN, coordinator.unique_id)}, + "name": coordinator.name, "manufacturer": "Solar-Log", } - def update(self): - """Get the latest data from the sensor and update the state.""" - self.data.update() - self._attr_native_value = self.data.data[self.entity_description.json_key] + @property + def native_value(self) -> StateType: + """Return the native sensor value.""" + return self.coordinator.data[self.entity_description.json_key]