"""Support for SolarEdge Monitoring API.""" from datetime import timedelta import logging import voluptuous as vol from requests.exceptions import HTTPError, ConnectTimeout from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_KEY, CONF_MONITORED_CONDITIONS, CONF_NAME, POWER_WATT, ENERGY_WATT_HOUR) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle # Config for solaredge monitoring api requests. CONF_SITE_ID = "site_id" OVERVIEW_UPDATE_DELAY = timedelta(minutes=10) DETAILS_UPDATE_DELAY = timedelta(hours=12) INVENTORY_UPDATE_DELAY = timedelta(hours=12) POWER_FLOW_UPDATE_DELAY = timedelta(minutes=10) SCAN_INTERVAL = timedelta(minutes=10) # Supported overview sensor types: # Key: ['json_key', 'name', unit, icon] SENSOR_TYPES = { 'lifetime_energy': ['lifeTimeData', "Lifetime energy", ENERGY_WATT_HOUR, 'mdi:solar-power'], 'energy_this_year': ['lastYearData', "Energy this year", ENERGY_WATT_HOUR, 'mdi:solar-power'], 'energy_this_month': ['lastMonthData', "Energy this month", ENERGY_WATT_HOUR, 'mdi:solar-power'], 'energy_today': ['lastDayData', "Energy today", ENERGY_WATT_HOUR, 'mdi:solar-power'], 'current_power': ['currentPower', "Current Power", POWER_WATT, 'mdi:solar-power'], 'site_details': [None, 'Site details', None, None], 'meters': ['meters', 'Meters', None, None], 'sensors': ['sensors', 'Sensors', None, None], 'gateways': ['gateways', 'Gateways', None, None], 'batteries': ['batteries', 'Batteries', None, None], 'inverters': ['inverters', 'Inverters', None, None], 'power_consumption': ['LOAD', 'Power Consumption', None, 'mdi:flash'], 'solar_power': ['PV', 'Solar Power', None, 'mdi:solar-power'], 'grid_power': ['GRID', 'Grid Power', None, 'mdi:power-plug'], 'storage_power': ['STORAGE', 'Storage Power', None, 'mdi:car-battery'] } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_SITE_ID): cv.string, vol.Optional(CONF_NAME, default='SolarEdge'): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=['current_power']): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]) }) _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Create the SolarEdge Monitoring API sensor.""" import solaredge api_key = config[CONF_API_KEY] site_id = config[CONF_SITE_ID] platform_name = config[CONF_NAME] # Create new SolarEdge object to retrieve data api = solaredge.Solaredge(api_key) # Check if api can be reached and site is active try: response = api.get_details(site_id) if response['details']['status'].lower() != 'active': _LOGGER.error("SolarEdge site is not active") return _LOGGER.debug("Credentials correct and site is active") except KeyError: _LOGGER.error("Missing details data in solaredge response") return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve details from SolarEdge API") return # Create sensor factory that will create sensors based on sensor_key. sensor_factory = SolarEdgeSensorFactory(platform_name, site_id, api) # Create a new sensor for each sensor type. entities = [] for sensor_key in config[CONF_MONITORED_CONDITIONS]: sensor = sensor_factory.create_sensor(sensor_key) if sensor is not None: entities.append(sensor) add_entities(entities, True) class SolarEdgeSensorFactory: """Factory which creates sensors based on the sensor_key.""" def __init__(self, platform_name, site_id, api): """Initialize the factory.""" self.platform_name = platform_name details = SolarEdgeDetailsDataService(api, site_id) overview = SolarEdgeOverviewDataService(api, site_id) inventory = SolarEdgeInventoryDataService(api, site_id) flow = SolarEdgePowerFlowDataService(api, site_id) self.services = { 'site_details': (SolarEdgeDetailsSensor, details) } for key in ['lifetime_energy', 'energy_this_year', 'energy_this_month', 'energy_today', 'current_power']: self.services[key] = (SolarEdgeOverviewSensor, overview) for key in ['meters', 'sensors', 'gateways', 'batteries', 'inverters']: self.services[key] = (SolarEdgeInventorySensor, inventory) for key in ['power_consumption', 'solar_power', 'grid_power', 'storage_power']: self.services[key] = (SolarEdgePowerFlowSensor, flow) def create_sensor(self, sensor_key): """Create and return a sensor based on the sensor_key.""" sensor_class, service = self.services[sensor_key] return sensor_class(self.platform_name, sensor_key, service) class SolarEdgeSensor(Entity): """Abstract class for a solaredge sensor.""" def __init__(self, platform_name, sensor_key, data_service): """Initialize the sensor.""" self.platform_name = platform_name self.sensor_key = sensor_key self.data_service = data_service self._state = None self._unit_of_measurement = SENSOR_TYPES[self.sensor_key][2] self._icon = SENSOR_TYPES[self.sensor_key][3] @property def name(self): """Return the name.""" return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1]) @property def unit_of_measurement(self): """Return the unit of measurement.""" return self._unit_of_measurement @property def icon(self): """Return the sensor icon.""" return self._icon @property def state(self): """Return the state of the sensor.""" return self._state class SolarEdgeOverviewSensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API overview sensor.""" def __init__(self, platform_name, sensor_key, data_service): """Initialize the overview sensor.""" super().__init__(platform_name, sensor_key, data_service) self._json_key = SENSOR_TYPES[self.sensor_key][0] def update(self): """Get the latest data from the sensor and update the state.""" self.data_service.update() self._state = self.data_service.data[self._json_key] class SolarEdgeDetailsSensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API details sensor.""" def __init__(self, platform_name, sensor_key, data_service): """Initialize the details sensor.""" super().__init__(platform_name, sensor_key, data_service) self._attributes = {} @property def device_state_attributes(self): """Return the state attributes.""" return self._attributes def update(self): """Get the latest details and update state and attributes.""" self.data_service.update() self._state = self.data_service.data self._attributes = self.data_service.attributes class SolarEdgeInventorySensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API inventory sensor.""" def __init__(self, platform_name, sensor_key, data_service): """Initialize the inventory sensor.""" super().__init__(platform_name, sensor_key, data_service) self._json_key = SENSOR_TYPES[self.sensor_key][0] self._attributes = {} @property def device_state_attributes(self): """Return the state attributes.""" return self._attributes def update(self): """Get the latest inventory data and update state and attributes.""" self.data_service.update() self._state = self.data_service.data[self._json_key] self._attributes = self.data_service.attributes[self._json_key] class SolarEdgePowerFlowSensor(SolarEdgeSensor): """Representation of an SolarEdge Monitoring API power flow sensor.""" def __init__(self, platform_name, sensor_key, data_service): """Initialize the power flow sensor.""" super().__init__(platform_name, sensor_key, data_service) self._json_key = SENSOR_TYPES[self.sensor_key][0] self._attributes = {} @property def device_state_attributes(self): """Return the state attributes.""" return self._attributes def update(self): """Get the latest inventory data and update state and attributes.""" self.data_service.update() self._state = self.data_service.data.get(self._json_key) self._attributes = self.data_service.attributes.get(self._json_key) self._unit_of_measurement = self.data_service.unit class SolarEdgeDataService: """Get and update the latest data.""" def __init__(self, api, site_id): """Initialize the data object.""" self.api = api self.site_id = site_id self.data = {} self.attributes = {} class SolarEdgeOverviewDataService(SolarEdgeDataService): """Get and update the latest overview data.""" @Throttle(OVERVIEW_UPDATE_DELAY) def update(self): """Update the data from the SolarEdge Monitoring API.""" try: data = self.api.get_overview(self.site_id) overview = data['overview'] except KeyError: _LOGGER.error("Missing overview data, skipping update") return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve data, skipping update") return self.data = {} for key, value in overview.items(): if key in ['lifeTimeData', 'lastYearData', 'lastMonthData', 'lastDayData']: data = value['energy'] elif key in ['currentPower']: data = value['power'] else: data = value self.data[key] = data _LOGGER.debug("Updated SolarEdge overview: %s", self.data) class SolarEdgeDetailsDataService(SolarEdgeDataService): """Get and update the latest details data.""" def __init__(self, api, site_id): """Initialize the details data service.""" super().__init__(api, site_id) self.data = None @Throttle(DETAILS_UPDATE_DELAY) def update(self): """Update the data from the SolarEdge Monitoring API.""" from stringcase import snakecase try: data = self.api.get_details(self.site_id) details = data['details'] except KeyError: _LOGGER.error("Missing details data, skipping update") return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve data, skipping update") return self.data = None self.attributes = {} for key, value in details.items(): key = snakecase(key) if key in ['primary_module']: for module_key, module_value in value.items(): self.attributes[snakecase(module_key)] = module_value elif key in ['peak_power', 'type', 'name', 'last_update_time', 'installation_date']: self.attributes[key] = value elif key == 'status': self.data = value _LOGGER.debug("Updated SolarEdge details: %s, %s", self.data, self.attributes) class SolarEdgeInventoryDataService(SolarEdgeDataService): """Get and update the latest inventory data.""" @Throttle(INVENTORY_UPDATE_DELAY) def update(self): """Update the data from the SolarEdge Monitoring API.""" try: data = self.api.get_inventory(self.site_id) inventory = data['Inventory'] except KeyError: _LOGGER.error("Missing inventory data, skipping update") return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve data, skipping update") return self.data = {} self.attributes = {} for key, value in inventory.items(): self.data[key] = len(value) self.attributes[key] = {key: value} _LOGGER.debug("Updated SolarEdge inventory: %s, %s", self.data, self.attributes) class SolarEdgePowerFlowDataService(SolarEdgeDataService): """Get and update the latest power flow data.""" def __init__(self, api, site_id): """Initialize the power flow data service.""" super().__init__(api, site_id) self.unit = None @Throttle(POWER_FLOW_UPDATE_DELAY) def update(self): """Update the data from the SolarEdge Monitoring API.""" try: data = self.api.get_current_power_flow(self.site_id) power_flow = data['siteCurrentPowerFlow'] except KeyError: _LOGGER.error("Missing power flow data, skipping update") return except (ConnectTimeout, HTTPError): _LOGGER.error("Could not retrieve data, skipping update") return power_from = [] power_to = [] if 'connections' not in power_flow: _LOGGER.error("Missing connections in power flow data") return for connection in power_flow['connections']: power_from.append(connection['from'].lower()) power_to.append(connection['to'].lower()) self.data = {} self.attributes = {} self.unit = power_flow['unit'] for key, value in power_flow.items(): if key in ['LOAD', 'PV', 'GRID', 'STORAGE']: self.data[key] = value['currentPower'] self.attributes[key] = {'status': value['status']} if key in ['GRID']: export = key.lower() in power_to self.data[key] *= -1 if export else 1 self.attributes[key]['flow'] = ('export' if export else 'import') if key in ['STORAGE']: charge = key.lower() in power_to self.data[key] *= -1 if charge else 1 self.attributes[key]['flow'] = ('charge' if charge else 'discharge') _LOGGER.debug("Updated SolarEdge power flow: %s, %s", self.data, self.attributes)