""" Support for Smappee energy monitor. For more details about this component, please refer to the documentation at https://home-assistant.io/components/smappee/ """ import logging from datetime import datetime, timedelta import re import voluptuous as vol from requests.exceptions import RequestException from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, CONF_HOST ) from homeassistant.util import Throttle from homeassistant.helpers.discovery import load_platform import homeassistant.helpers.config_validation as cv REQUIREMENTS = ['smappy==0.2.16'] _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'Smappee' DEFAULT_HOST_PASSWORD = 'admin' CONF_CLIENT_ID = 'client_id' CONF_CLIENT_SECRET = 'client_secret' CONF_HOST_PASSWORD = 'host_password' DOMAIN = 'smappee' DATA_SMAPPEE = 'SMAPPEE' _SENSOR_REGEX = re.compile( r'(?P([A-Za-z]+))\=' + r'(?P([0-9\.]+))') CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Inclusive(CONF_CLIENT_ID, 'Server credentials'): cv.string, vol.Inclusive(CONF_CLIENT_SECRET, 'Server credentials'): cv.string, vol.Inclusive(CONF_USERNAME, 'Server credentials'): cv.string, vol.Inclusive(CONF_PASSWORD, 'Server credentials'): cv.string, vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_HOST_PASSWORD, default=DEFAULT_HOST_PASSWORD): cv.string }), }, extra=vol.ALLOW_EXTRA) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) def setup(hass, config): """Set up the Smapee component.""" client_id = config.get(DOMAIN).get(CONF_CLIENT_ID) client_secret = config.get(DOMAIN).get(CONF_CLIENT_SECRET) username = config.get(DOMAIN).get(CONF_USERNAME) password = config.get(DOMAIN).get(CONF_PASSWORD) host = config.get(DOMAIN).get(CONF_HOST) host_password = config.get(DOMAIN).get(CONF_HOST_PASSWORD) smappee = Smappee(client_id, client_secret, username, password, host, host_password) if not smappee.is_local_active and not smappee.is_remote_active: _LOGGER.error("Neither Smappee server or local component enabled.") return False hass.data[DATA_SMAPPEE] = smappee load_platform(hass, 'switch', DOMAIN, {}, config) load_platform(hass, 'sensor', DOMAIN, {}, config) return True class Smappee: """Stores data retrieved from Smappee sensor.""" def __init__(self, client_id, client_secret, username, password, host, host_password): """Initialize the data.""" import smappy self._remote_active = False self._local_active = False if client_id is not None: try: self._smappy = smappy.Smappee(client_id, client_secret) self._smappy.authenticate(username, password) self._remote_active = True except RequestException as error: self._smappy = None _LOGGER.exception( "Smappee server authentication failed (%s)", error) else: _LOGGER.warning("Smappee server component init skipped.") if host is not None: try: self._localsmappy = smappy.LocalSmappee(host) self._localsmappy.logon(host_password) self._local_active = True except RequestException as error: self._localsmappy = None _LOGGER.exception( "Local Smappee device authentication failed (%s)", error) else: _LOGGER.warning("Smappee local component init skipped.") self.locations = {} self.info = {} self.consumption = {} self.sensor_consumption = {} self.instantaneous = {} if self._remote_active or self._local_active: self.update() @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update data from Smappee API.""" if self.is_remote_active: service_locations = self._smappy.get_service_locations() \ .get('serviceLocations') for location in service_locations: location_id = location.get('serviceLocationId') if location_id is not None: self.sensor_consumption[location_id] = {} self.locations[location_id] = location.get('name') self.info[location_id] = self._smappy \ .get_service_location_info(location_id) _LOGGER.debug("Remote info %s %s", self.locations, self.info[location_id]) for sensors in self.info[location_id].get('sensors'): sensor_id = sensors.get('id') self.sensor_consumption[location_id]\ .update({sensor_id: self.get_sensor_consumption( location_id, sensor_id, aggregation=3, delta=1440)}) _LOGGER.debug("Remote sensors %s %s", self.locations, self.sensor_consumption[location_id]) self.consumption[location_id] = self.get_consumption( location_id, aggregation=3, delta=1440) _LOGGER.debug("Remote consumption %s %s", self.locations, self.consumption[location_id]) if self.is_local_active: self.local_devices = self.get_switches() _LOGGER.debug("Local switches %s", self.local_devices) self.instantaneous = self.load_instantaneous() _LOGGER.debug("Local values %s", self.instantaneous) @property def is_remote_active(self): """Return true if Smappe server is configured and working.""" return self._remote_active @property def is_local_active(self): """Return true if Smappe local device is configured and working.""" return self._local_active def get_switches(self): """Get switches from local Smappee.""" if not self.is_local_active: return try: return self._localsmappy.load_command_control_config() except RequestException as error: _LOGGER.error( "Error getting switches from local Smappee. (%s)", error) def get_consumption(self, location_id, aggregation, delta): """Update data from Smappee.""" # Start & End accept epoch (in milliseconds), # datetime and pandas timestamps # Aggregation: # 1 = 5 min values (only available for the last 14 days), # 2 = hourly values, # 3 = daily values, # 4 = monthly values, # 5 = quarterly values if not self.is_remote_active: return end = datetime.utcnow() start = end - timedelta(minutes=delta) try: return self._smappy.get_consumption(location_id, start, end, aggregation) except RequestException as error: _LOGGER.error( "Error getting comsumption from Smappee cloud. (%s)", error) def get_sensor_consumption(self, location_id, sensor_id, aggregation, delta): """Update data from Smappee.""" # Start & End accept epoch (in milliseconds), # datetime and pandas timestamps # Aggregation: # 1 = 5 min values (only available for the last 14 days), # 2 = hourly values, # 3 = daily values, # 4 = monthly values, # 5 = quarterly values if not self.is_remote_active: return end = datetime.utcnow() start = end - timedelta(minutes=delta) try: return self._smappy.get_sensor_consumption(location_id, sensor_id, start, end, aggregation) except RequestException as error: _LOGGER.error( "Error getting comsumption from Smappee cloud. (%s)", error) def actuator_on(self, location_id, actuator_id, is_remote_switch, duration=None): """Turn on actuator.""" # Duration = 300,900,1800,3600 # or any other value for an undetermined period of time. # # The comport plugs have a tendency to ignore the on/off signal. # And because you can't read the status of a plug, it's more # reliable to execute the command twice. try: if is_remote_switch: self._smappy.actuator_on(location_id, actuator_id, duration) self._smappy.actuator_on(location_id, actuator_id, duration) else: self._localsmappy.on_command_control(actuator_id) self._localsmappy.on_command_control(actuator_id) except RequestException as error: _LOGGER.error( "Error turning actuator on. (%s)", error) return False return True def actuator_off(self, location_id, actuator_id, is_remote_switch, duration=None): """Turn off actuator.""" # Duration = 300,900,1800,3600 # or any other value for an undetermined period of time. # # The comport plugs have a tendency to ignore the on/off signal. # And because you can't read the status of a plug, it's more # reliable to execute the command twice. try: if is_remote_switch: self._smappy.actuator_off(location_id, actuator_id, duration) self._smappy.actuator_off(location_id, actuator_id, duration) else: self._localsmappy.off_command_control(actuator_id) self._localsmappy.off_command_control(actuator_id) except RequestException as error: _LOGGER.error( "Error turning actuator on. (%s)", error) return False return True def active_power(self): """Get sum of all instantaneous active power values from local hub.""" if not self.is_local_active: return try: return self._localsmappy.active_power() except RequestException as error: _LOGGER.error( "Error getting data from Local Smappee unit. (%s)", error) def active_cosfi(self): """Get the average of all instantaneous cosfi values.""" if not self.is_local_active: return try: return self._localsmappy.active_cosfi() except RequestException as error: _LOGGER.error( "Error getting data from Local Smappee unit. (%s)", error) def instantaneous_values(self): """ReportInstantaneousValues.""" if not self.is_local_active: return report_instantaneous_values = \ self._localsmappy.report_instantaneous_values() report_result = \ report_instantaneous_values['report'].split('
') properties = {} for lines in report_result: lines_result = lines.split(',') for prop in lines_result: match = _SENSOR_REGEX.search(prop) if match: properties[match.group('key')] = \ match.group('value') _LOGGER.debug(properties) return properties def active_current(self): """Get current active Amps.""" if not self.is_local_active: return properties = self.instantaneous_values() return float(properties['current']) def active_voltage(self): """Get current active Voltage.""" if not self.is_local_active: return properties = self.instantaneous_values() return float(properties['voltage']) def load_instantaneous(self): """LoadInstantaneous.""" if not self.is_local_active: return try: return self._localsmappy.load_instantaneous() except RequestException as error: _LOGGER.error( "Error getting data from Local Smappee unit. (%s)", error)