From 333e1d67890b02a250a86d792210bbc083e8d667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20M=C3=BCndler?= Date: Sat, 29 Jun 2019 05:48:53 +0200 Subject: [PATCH] Fronius (solar energy and inverter) component (#22316) * Introduced fronius component that adds ability to track Fronius devices from Home Assistant * Use device parameter for fetching inverter data * Fixed handling of default scope * Handle exceptions from yield * Fulfill PR requirements * Fixed houndci violations * Found the last hound violation * Fixed docstring (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r165776934) * Fixed import order with isort (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r165776957) * CONF_DEVICE is now CONF_DEVICEID (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r165777161) * Added docstring to class FroniusSensor (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r165777792) * Fixed docstring for state (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r165777885) * Added/fixed docstrings (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r165778108 & https://github.com/home-assistant/home-assistant/pull/11446#discussion_r165778125) * Remove redundant log entry (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r165779213) * Fixed error message if sensor update fails (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r165779435) * Fixed error log messages (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r165779751 & https://github.com/home-assistant/home-assistant/pull/11446#discussion_r165779761) * Satisfy hound * Handle exceptions explicit (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r168940902) * Removed unnecessary call of update (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r168940894) * The point makes the difference. * Removed unrelated requirements * Remove config logging (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r168968748) * Reorder and fix imports (https://github.com/home-assistant/home-assistant/pull/11446#discussion_r168968725, https://github.com/home-assistant/home-assistant/pull/11446#discussion_r168968691) * Update fronius requirement * Various small fixes * Small fixes * Formatting * Add fronius to coverage * New structure and formatting * Add manifest.json * Fix data loading * Make pylint happy * Fix issues * Fix parse_attributes * Fix docstring and platform schema * Make use of default HA-Const config values * Change configuration setup, introducing list of monitored conditions * Change the structure slightly, allowing for a list of sensors * Remove periods from logging * Formatting * Change name generation, use variable instead of string * small fixes * Update sensor.py * Incorporate correction proposals * Setting default device inside validation * Move import on top and small format * Formatting fix * Rename validation method to _device_id_validator --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/fronius/__init__.py | 1 + .../components/fronius/manifest.json | 8 + homeassistant/components/fronius/sensor.py | 197 ++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 211 insertions(+) create mode 100644 homeassistant/components/fronius/__init__.py create mode 100644 homeassistant/components/fronius/manifest.json create mode 100644 homeassistant/components/fronius/sensor.py diff --git a/.coveragerc b/.coveragerc index 33d90aa556f..7bc58a9cec1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -214,6 +214,7 @@ omit = homeassistant/components/fritzbox_callmonitor/sensor.py homeassistant/components/fritzbox_netmonitor/sensor.py homeassistant/components/fritzdect/switch.py + homeassistant/components/fronius/sensor.py homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py homeassistant/components/garadget/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index ca46cd3471f..0bf06d9945f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -91,6 +91,7 @@ homeassistant/components/flock/* @fabaff homeassistant/components/flunearyou/* @bachya homeassistant/components/foursquare/* @robbiet480 homeassistant/components/freebox/* @snoof85 +homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb diff --git a/homeassistant/components/fronius/__init__.py b/homeassistant/components/fronius/__init__.py new file mode 100644 index 00000000000..2b4d968feca --- /dev/null +++ b/homeassistant/components/fronius/__init__.py @@ -0,0 +1 @@ +"""The Fronius component.""" diff --git a/homeassistant/components/fronius/manifest.json b/homeassistant/components/fronius/manifest.json new file mode 100644 index 00000000000..8f737e2e1ff --- /dev/null +++ b/homeassistant/components/fronius/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "fronius", + "name": "Fronius", + "documentation": "https://www.home-assistant.io/components/fronius", + "requirements": ["pyfronius==0.4.6"], + "dependencies": [], + "codeowners": ["@nielstron"] +} diff --git a/homeassistant/components/fronius/sensor.py b/homeassistant/components/fronius/sensor.py new file mode 100644 index 00000000000..07d2e984f23 --- /dev/null +++ b/homeassistant/components/fronius/sensor.py @@ -0,0 +1,197 @@ +"""Support for Fronius devices.""" +import copy +import logging +import voluptuous as vol + +from pyfronius import Fronius + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import (CONF_RESOURCE, CONF_SENSOR_TYPE, CONF_DEVICE, + CONF_MONITORED_CONDITIONS) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +CONF_SCOPE = 'scope' + +TYPE_INVERTER = 'inverter' +TYPE_STORAGE = 'storage' +TYPE_METER = 'meter' +TYPE_POWER_FLOW = 'power_flow' +SCOPE_DEVICE = 'device' +SCOPE_SYSTEM = 'system' + +DEFAULT_SCOPE = SCOPE_DEVICE +DEFAULT_DEVICE = 0 +DEFAULT_INVERTER = 1 + +SENSOR_TYPES = [TYPE_INVERTER, TYPE_STORAGE, TYPE_METER, TYPE_POWER_FLOW] +SCOPE_TYPES = [SCOPE_DEVICE, SCOPE_SYSTEM] + + +def _device_id_validator(config): + """Ensure that inverters have default id 1 and other devices 0.""" + config = copy.deepcopy(config) + for cond in config[CONF_MONITORED_CONDITIONS]: + if CONF_DEVICE not in cond: + if cond[CONF_SENSOR_TYPE] == TYPE_INVERTER: + cond[CONF_DEVICE] = DEFAULT_INVERTER + else: + cond[CONF_DEVICE] = DEFAULT_DEVICE + return config + + +PLATFORM_SCHEMA = vol.Schema(vol.All(PLATFORM_SCHEMA.extend({ + vol.Required(CONF_RESOURCE): + cv.url, + vol.Required(CONF_MONITORED_CONDITIONS): + vol.All( + cv.ensure_list, + [{ + vol.Required(CONF_SENSOR_TYPE): vol.In(SENSOR_TYPES), + vol.Optional(CONF_SCOPE, default=DEFAULT_SCOPE): + vol.In(SCOPE_TYPES), + vol.Optional(CONF_DEVICE): + vol.All(vol.Coerce(int), vol.Range(min=0)) + }] + ) +}), _device_id_validator)) + + +async def async_setup_platform(hass, + config, + async_add_entities, + discovery_info=None): + """Set up of Fronius platform.""" + session = async_get_clientsession(hass) + fronius = Fronius(session, config[CONF_RESOURCE]) + + sensors = [] + for condition in config[CONF_MONITORED_CONDITIONS]: + + device = condition[CONF_DEVICE] + name = "Fronius {} {} {}".format( + condition[CONF_SENSOR_TYPE].replace('_', ' ').capitalize(), + device, + config[CONF_RESOURCE], + ) + sensor_type = condition[CONF_SENSOR_TYPE] + scope = condition[CONF_SCOPE] + if sensor_type == TYPE_INVERTER: + if scope == SCOPE_SYSTEM: + sensor_cls = FroniusInverterSystem + else: + sensor_cls = FroniusInverterDevice + elif sensor_type == TYPE_METER: + if scope == SCOPE_SYSTEM: + sensor_cls = FroniusMeterSystem + else: + sensor_cls = FroniusMeterDevice + elif sensor_type == TYPE_POWER_FLOW: + sensor_cls = FroniusPowerFlow + else: + sensor_cls = FroniusStorage + + sensors.append(sensor_cls(fronius, name, device)) + + async_add_entities(sensors, True) + + +class FroniusSensor(Entity): + """The Fronius sensor implementation.""" + + def __init__(self, data, name, device): + """Initialize the sensor.""" + self.data = data + self._name = name + self._device = device + self._state = None + self._attributes = {} + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the current state.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + async def async_update(self): + """Retrieve and update latest state.""" + values = {} + try: + values = await self._update() + except ConnectionError: + _LOGGER.error("Failed to update: connection error") + except ValueError: + _LOGGER.error("Failed to update: invalid response returned." + "Maybe the configured device is not supported") + + if values: + self._state = values['status']['Code'] + attributes = {} + for key in values: + if 'value' in values[key]: + attributes[key] = values[key].get('value', 0) + self._attributes = attributes + + async def _update(self): + """Return values of interest.""" + pass + + +class FroniusInverterSystem(FroniusSensor): + """Sensor for the fronius inverter with system scope.""" + + async def _update(self): + """Get the values for the current state.""" + return await self.data.current_system_inverter_data() + + +class FroniusInverterDevice(FroniusSensor): + """Sensor for the fronius inverter with device scope.""" + + async def _update(self): + """Get the values for the current state.""" + return await self.data.current_inverter_data(self._device) + + +class FroniusStorage(FroniusSensor): + """Sensor for the fronius battery storage.""" + + async def _update(self): + """Get the values for the current state.""" + return await self.data.current_storage_data(self._device) + + +class FroniusMeterSystem(FroniusSensor): + """Sensor for the fronius meter with system scope.""" + + async def _update(self): + """Get the values for the current state.""" + return await self.data.current_system_meter_data() + + +class FroniusMeterDevice(FroniusSensor): + """Sensor for the fronius meter with device scope.""" + + async def _update(self): + """Get the values for the current state.""" + return await self.data.current_meter_data(self._device) + + +class FroniusPowerFlow(FroniusSensor): + """Sensor for the fronius power flow.""" + + async def _update(self): + """Get the values for the current state.""" + return await self.data.current_power_flow() diff --git a/requirements_all.txt b/requirements_all.txt index 06fa352d43b..1f1e380c8a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1126,6 +1126,9 @@ pyfnip==0.2 # homeassistant.components.fritzbox pyfritzhome==0.4.0 +# homeassistant.components.fronius +pyfronius==0.4.6 + # homeassistant.components.ifttt pyfttt==0.3