diff --git a/.coveragerc b/.coveragerc index a2055681f18..e16b8d96600 100644 --- a/.coveragerc +++ b/.coveragerc @@ -190,6 +190,9 @@ omit = homeassistant/components/*/thinkingcleaner.py + homeassistant/components/toon.py + homeassistant/components/*/toon.py + homeassistant/components/tradfri.py homeassistant/components/*/tradfri.py diff --git a/homeassistant/components/climate/toon.py b/homeassistant/components/climate/toon.py new file mode 100644 index 00000000000..c4021a97c91 --- /dev/null +++ b/homeassistant/components/climate/toon.py @@ -0,0 +1,95 @@ +""" +Toon van Eneco Thermostat Support. + +This provides a component for the rebranded Quby thermostat as provided by +Eneco. +""" + +from homeassistant.components.climate import (ClimateDevice, + ATTR_TEMPERATURE, + STATE_PERFORMANCE, + STATE_HEAT, + STATE_ECO, + STATE_COOL) +from homeassistant.const import TEMP_CELSIUS + +import homeassistant.components.toon as toon_main + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup thermostat.""" + # Add toon + add_devices((ThermostatDevice(hass), ), True) + + +class ThermostatDevice(ClimateDevice): + """Interface class for the toon module and HA.""" + + def __init__(self, hass): + """Initialize the device.""" + self._name = 'Toon van Eneco' + self.hass = hass + self.thermos = hass.data[toon_main.TOON_HANDLE] + + # set up internal state vars + self._state = None + self._temperature = None + self._setpoint = None + self._operation_list = [STATE_PERFORMANCE, + STATE_HEAT, + STATE_ECO, + STATE_COOL] + + @property + def name(self): + """Name of this Thermostat.""" + return self._name + + @property + def should_poll(self): + """Polling is required.""" + return True + + @property + def temperature_unit(self): + """The unit of measurement used by the platform.""" + return TEMP_CELSIUS + + @property + def current_operation(self): + """Return current operation i.e. comfort, home, away.""" + state = self.thermos.get_data('state') + return state + + @property + def operation_list(self): + """List of available operation modes.""" + return self._operation_list + + @property + def current_temperature(self): + """Return the current temperature.""" + return self.thermos.get_data('temp') + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self.thermos.get_data('setpoint') + + def set_temperature(self, **kwargs): + """Change the setpoint of the thermostat.""" + temp = kwargs.get(ATTR_TEMPERATURE) + self.thermos.set_temp(temp) + + def set_operation_mode(self, operation_mode): + """Set new operation mode as toonlib requires it.""" + toonlib_values = {STATE_PERFORMANCE: 'Comfort', + STATE_HEAT: 'Home', + STATE_ECO: 'Away', + STATE_COOL: 'Sleep'} + + self.thermos.set_state(toonlib_values[operation_mode]) + + def update(self): + """Update local state.""" + self.thermos.update() diff --git a/homeassistant/components/sensor/toon.py b/homeassistant/components/sensor/toon.py new file mode 100644 index 00000000000..ee5ae9ca51e --- /dev/null +++ b/homeassistant/components/sensor/toon.py @@ -0,0 +1,256 @@ +""" +Toon van Eneco Utility Gages. + +This provides a component for the rebranded Quby thermostat as provided by +Eneco. +""" +import logging +import datetime as datetime + +from homeassistant.helpers.entity import Entity +import homeassistant.components.toon as toon_main + +_LOGGER = logging.getLogger(__name__) + +STATE_ATTR_DEVICE_TYPE = "device_type" +STATE_ATTR_LAST_CONNECTED_CHANGE = "last_connected_change" + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Setup sensors.""" + _toon_main = hass.data[toon_main.TOON_HANDLE] + + sensor_items = [] + sensor_items.extend([ToonSensor(hass, + 'Power_current', + 'power-plug', + 'Watt'), + ToonSensor(hass, + 'Power_today', + 'power-plug', + 'kWh')]) + + if _toon_main.gas: + sensor_items.extend([ToonSensor(hass, + 'Gas_current', + 'gas-cylinder', + 'CM3'), + ToonSensor(hass, + 'Gas_today', + 'gas-cylinder', + 'M3')]) + + for plug in _toon_main.toon.smartplugs: + sensor_items.extend([ + FibaroSensor(hass, + '{}_current_power'.format(plug.name), + plug.name, + 'power-socket-eu', + 'Watt'), + FibaroSensor(hass, + '{}_today_energy'.format(plug.name), + plug.name, + 'power-socket-eu', + 'kWh')]) + + if _toon_main.toon.solar.produced or _toon_main.solar: + sensor_items.extend([ + SolarSensor(hass, 'Solar_maximum', 'kWh'), + SolarSensor(hass, 'Solar_produced', 'kWh'), + SolarSensor(hass, 'Solar_value', 'Watt'), + SolarSensor(hass, 'Solar_average_produced', 'kWh'), + SolarSensor(hass, 'Solar_meter_reading_low_produced', 'kWh'), + SolarSensor(hass, 'Solar_meter_reading_produced', 'kWh'), + SolarSensor(hass, 'Solar_daily_cost_produced', 'Euro') + ]) + + for smokedetector in _toon_main.toon.smokedetectors: + sensor_items.append( + FibaroSmokeDetector(hass, + '{}_smoke_detector'.format(smokedetector.name), + smokedetector.device_uuid, + 'alarm-bell', + '%')) + + add_devices(sensor_items) + + +class ToonSensor(Entity): + """Representation of a sensor.""" + + def __init__(self, hass, name, icon, unit_of_measurement): + """Initialize the sensor.""" + self._name = name + self._state = None + self._icon = "mdi:" + icon + self._unit_of_measurement = unit_of_measurement + self.thermos = hass.data[toon_main.TOON_HANDLE] + + @property + def should_poll(self): + """Polling required.""" + return True + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the mdi icon of the sensor.""" + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + return self.thermos.get_data(self.name.lower()) + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + def update(self): + """Get the latest data from the sensor.""" + self.thermos.update() + + +class FibaroSensor(Entity): + """Representation of a sensor.""" + + def __init__(self, hass, name, plug_name, icon, unit_of_measurement): + """Initialize the sensor.""" + self._name = name + self._plug_name = plug_name + self._state = None + self._icon = "mdi:" + icon + self._unit_of_measurement = unit_of_measurement + self.toon = hass.data[toon_main.TOON_HANDLE] + + @property + def should_poll(self): + """Polling required.""" + return True + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the mdi icon of the sensor.""" + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + value = '_'.join(self.name.lower().split('_')[1:]) + return self.toon.get_data(value, self._plug_name) + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + def update(self): + """Get the latest data from the sensor.""" + self.toon.update() + + +class SolarSensor(Entity): + """Representation of a sensor.""" + + def __init__(self, hass, name, unit_of_measurement): + """Initialize the sensor.""" + self._name = name + self._state = None + self._icon = "mdi:weather-sunny" + self._unit_of_measurement = unit_of_measurement + self.toon = hass.data[toon_main.TOON_HANDLE] + + @property + def should_poll(self): + """Polling required.""" + return True + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the mdi icon of the sensor.""" + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + return self.toon.get_data(self.name.lower()) + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + def update(self): + """Get the latest data from the sensor.""" + self.toon.update() + + +class FibaroSmokeDetector(Entity): + """Representation of a smoke detector.""" + + def __init__(self, hass, name, uid, icon, unit_of_measurement): + """Initialize the sensor.""" + self._name = name + self._uid = uid + self._state = None + self._icon = "mdi:" + icon + self._unit_of_measurement = unit_of_measurement + self.toon = hass.data[toon_main.TOON_HANDLE] + + @property + def should_poll(self): + """Polling required.""" + return True + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the mdi icon of the sensor.""" + return self._icon + + @property + def state_attributes(self): + """Return the state attributes of the smoke detectors.""" + value = datetime.datetime.fromtimestamp( + int(self.toon.get_data('last_connected_change', self.name)) + ).strftime('%Y-%m-%d %H:%M:%S') + + return { + STATE_ATTR_DEVICE_TYPE: self.toon.get_data('device_type', + self.name), + STATE_ATTR_LAST_CONNECTED_CHANGE: value + } + + @property + def state(self): + """Return the state of the sensor.""" + value = self.name.lower().split('_', 1)[1] + return self.toon.get_data(value, self.name) + + @property + def unit_of_measurement(self): + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + def update(self): + """Get the latest data from the sensor.""" + self.toon.update() diff --git a/homeassistant/components/switch/toon.py b/homeassistant/components/switch/toon.py new file mode 100644 index 00000000000..656d175ff3a --- /dev/null +++ b/homeassistant/components/switch/toon.py @@ -0,0 +1,77 @@ +""" +Support for Eneco Slimmer stekkers (Smart Plugs). + +This provides controlls for the z-wave smart plugs Toon can control. +""" +import logging + +from homeassistant.components.switch import SwitchDevice +import homeassistant.components.toon as toon_main + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices_callback, discovery_info=None): + """Setup discovered Smart Plugs.""" + _toon_main = hass.data[toon_main.TOON_HANDLE] + switch_items = [] + for plug in _toon_main.toon.smartplugs: + switch_items.append(EnecoSmartPlug(hass, plug)) + + add_devices_callback(switch_items) + + +class EnecoSmartPlug(SwitchDevice): + """Representation of a Smart Plug.""" + + def __init__(self, hass, plug): + """Initialize the Smart Plug.""" + self.smartplug = plug + self.toon_data_store = hass.data[toon_main.TOON_HANDLE] + + @property + def should_poll(self): + """No polling needed with subscriptions.""" + return True + + @property + def unique_id(self): + """Return the ID of this switch.""" + return self.smartplug.device_uuid + + @property + def name(self): + """Return the name of the switch if any.""" + return self.smartplug.name + + @property + def current_power_w(self): + """Current power usage in W.""" + return self.toon_data_store.get_data('current_power', self.name) + + @property + def today_energy_kwh(self): + """Today total energy usage in kWh.""" + return self.toon_data_store.get_data('today_energy', self.name) + + @property + def is_on(self): + """Return true if switch is on. Standby is on.""" + return self.toon_data_store.get_data('current_state', self.name) + + @property + def available(self): + """True if switch is available.""" + return self.smartplug.can_toggle + + def turn_on(self, **kwargs): + """Turn the switch on.""" + return self.smartplug.turn_on() + + def turn_off(self): + """Turn the switch off.""" + return self.smartplug.turn_off() + + def update(self): + """Update state.""" + self.toon_data_store.update() diff --git a/homeassistant/components/toon.py b/homeassistant/components/toon.py new file mode 100644 index 00000000000..d873c42e815 --- /dev/null +++ b/homeassistant/components/toon.py @@ -0,0 +1,149 @@ +""" +Toon van Eneco Support. + +This provides a component for the rebranded Quby thermostat as provided by +Eneco. +""" +import logging +from datetime import datetime, timedelta +import voluptuous as vol + +# Import the device class from the component that you want to support +from homeassistant.const import (CONF_USERNAME, CONF_PASSWORD) +from homeassistant.helpers.discovery import load_platform +import homeassistant.helpers.config_validation as cv +from homeassistant.util import Throttle + +# Home Assistant depends on 3rd party packages for API specific code. +REQUIREMENTS = ['toonlib==1.0.2'] + +_LOGGER = logging.getLogger(__name__) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=5) + +DOMAIN = 'toon' +TOON_HANDLE = 'toon_handle' +CONF_GAS = 'gas' +DEFAULT_GAS = True +CONF_SOLAR = 'solar' +DEFAULT_SOLAR = False + +# Validation of the user's configuration +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_GAS, default=DEFAULT_GAS): cv.boolean, + vol.Optional(CONF_SOLAR, default=DEFAULT_SOLAR): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Setup toon.""" + from toonlib import InvalidCredentials + gas = config['toon']['gas'] + solar = config['toon']['solar'] + + try: + hass.data[TOON_HANDLE] = ToonDataStore(config['toon']['username'], + config['toon']['password'], + gas, + solar) + except InvalidCredentials: + return False + + # Load all platforms + for platform in ('climate', 'sensor', 'switch'): + load_platform(hass, platform, DOMAIN, {}, config) + + # Initialization successfull + return True + + +class ToonDataStore: + """An object to store the toon data.""" + + def __init__(self, username, password, gas=DEFAULT_GAS, + solar=DEFAULT_SOLAR): + """Initialize toon.""" + from toonlib import Toon + + # Creating the class + + toon = Toon(username, password) + + self.toon = toon + self.gas = gas + self.solar = solar + self.data = {} + + self.last_update = datetime.min + self.update() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update toon data.""" + self.last_update = datetime.now() + + self.data['power_current'] = self.toon.power.value + self.data['power_today'] = round( + (float(self.toon.power.daily_usage) + + float(self.toon.power.daily_usage_low)) / 1000, 2) + self.data['temp'] = self.toon.temperature + + if self.toon.thermostat_state: + self.data['state'] = self.toon.thermostat_state.name + else: + self.data['state'] = 'Manual' + + self.data['setpoint'] = float( + self.toon.thermostat_info.current_set_point) / 100 + self.data['gas_current'] = self.toon.gas.value + self.data['gas_today'] = round(float(self.toon.gas.daily_usage) / + 1000, 2) + + for plug in self.toon.smartplugs: + self.data[plug.name] = {'current_power': plug.current_usage, + 'today_energy': round( + float(plug.daily_usage) / 1000, 2), + 'current_state': plug.current_state, + 'is_connected': plug.is_connected} + + self.data['solar_maximum'] = self.toon.solar.maximum + self.data['solar_produced'] = self.toon.solar.produced + self.data['solar_value'] = self.toon.solar.value + self.data['solar_average_produced'] = self.toon.solar.average_produced + self.data['solar_meter_reading_low_produced'] = \ + self.toon.solar.meter_reading_low_produced + self.data['solar_meter_reading_produced'] = \ + self.toon.solar.meter_reading_produced + self.data['solar_daily_cost_produced'] = \ + self.toon.solar.daily_cost_produced + + for detector in self.toon.smokedetectors: + value = '{}_smoke_detector'.format(detector.name) + self.data[value] = {'smoke_detector': detector.battery_level, + 'device_type': detector.device_type, + 'is_connected': detector.is_connected, + 'last_connected_change': + detector.last_connected_change} + + def set_state(self, state): + """Push a new state to the Toon unit.""" + self.toon.thermostat_state = state + + def set_temp(self, temp): + """Push a new temperature to the Toon unit.""" + self.toon.thermostat = temp + + def get_data(self, data_id, plug_name=None): + """Get the cached data.""" + data = {'error': 'no data'} + if plug_name: + if data_id in self.data[plug_name]: + data = self.data[plug_name][data_id] + else: + if data_id in self.data: + data = self.data[data_id] + return data diff --git a/requirements_all.txt b/requirements_all.txt index 8e8a05f834c..98abf7b57e8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1014,6 +1014,9 @@ tikteck==0.4 # homeassistant.components.calendar.todoist todoist-python==7.0.17 +# homeassistant.components.toon +toonlib==1.0.2 + # homeassistant.components.alarm_control_panel.totalconnect total_connect_client==0.11