Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert solarlog to coordinator #55345

Merged
merged 4 commits into from
Aug 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions homeassistant/components/solarlog/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,102 @@
"""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


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:
Copy link
Contributor

@Ernst79 Ernst79 Aug 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the pypi package seems to either send all data, or nothing {}, so I think we can do

        if api:
            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)

            if api.time.year == 1999:
                raise update_coordinator.UpdateFailed(
                    "Invalid data returned (can happen after Solarlog restart)"
                )
        else:
            raise update_coordinator.UpdateFailed(
                "Solarlog response returns no data"
            )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, just posted the above comment while you added your last commit. Modified it.

Thanks for your help.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that will work because api is a SolarLog object

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the current code works I suggest we go with that and you can clean it up in a future PR. Since I don't have a device to test it with, I don't want to do more than necessary.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'll do a final test.

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)
balloob marked this conversation as resolved.
Show resolved Hide resolved
return data
6 changes: 1 addition & 5 deletions homeassistant/components/solarlog/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from __future__ import annotations

from dataclasses import dataclass
from datetime import timedelta

from homeassistant.components.sensor import (
STATE_CLASS_MEASUREMENT,
Expand All @@ -23,13 +22,10 @@

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:
Expand Down
133 changes: 21 additions & 112 deletions homeassistant/components/solarlog/sensor.py
Original file line number Diff line number Diff line change
@@ -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 .const import DOMAIN, SCAN_INTERVAL, SENSOR_TYPES, SolarLogSensorEntityDescription
from homeassistant.helpers import update_coordinator
from homeassistant.helpers.entity import StateType

_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


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")
coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
SolarlogSensor(coordinator, description) for description in SENSOR_TYPES
)


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]
Copy link
Member

@MartinHjelmare MartinHjelmare Aug 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note for the future: If we rename the keys in the data returned by the coordinator to the same names as the entity description keys, we can drop the json_key from the entity description and use the entity description key instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, But that will cause a change in the unique_id. I don’t think that is desired.

self._attr_unique_id = f"{coordinator.unique_id}_{description.key}"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we won't change the description key attribute just remove the json_key attribute.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I need the JSON key to get the correct value from the sunwatcher pypi package. This package is having partly capitalized works as keys in its output

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use the json_key to get the data from the pypi library. We use it to get the data from the coordinator, which is data we define in this integration. We can define that data dict however we want.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, sorry, you are totally right. I’ll add that in a future PR