-
-
Notifications
You must be signed in to change notification settings - Fork 30.2k
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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: | ||
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 |
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] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, we won't change the description There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't use the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
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 doThere was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 aSolarLog
objectThere was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.