diff --git a/.coveragerc b/.coveragerc index 557567e7aaf..e0f797c4d04 100644 --- a/.coveragerc +++ b/.coveragerc @@ -655,6 +655,7 @@ omit = homeassistant/components/wirelesstag/* homeassistant/components/xiaomi_aqara/* homeassistant/components/xiaomi_miio/* + homeassistant/components/xs1/* homeassistant/components/zabbix/* homeassistant/components/zeroconf/* homeassistant/components/zha/__init__.py diff --git a/homeassistant/components/xs1/__init__.py b/homeassistant/components/xs1/__init__.py new file mode 100644 index 00000000000..14656737f5c --- /dev/null +++ b/homeassistant/components/xs1/__init__.py @@ -0,0 +1,119 @@ +""" +Support for the EZcontrol XS1 gateway. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/xs1/ +""" + +import asyncio +from functools import partial +import logging + +import voluptuous as vol + +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_USERNAME) +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +REQUIREMENTS = ['xs1-api-client==2.3.5'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'xs1' +ACTUATORS = 'actuators' +SENSORS = 'sensors' + +# define configuration parameters +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=80): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string + }), +}, extra=vol.ALLOW_EXTRA) + +XS1_COMPONENTS = [ + 'switch', + 'sensor', + 'climate' +] + +# Lock used to limit the amount of concurrent update requests +# as the XS1 Gateway can only handle a very +# small amount of concurrent requests +UPDATE_LOCK = asyncio.Lock() + + +def _create_controller_api(host, port, ssl, user, password): + """Create an api instance to use for communication.""" + import xs1_api_client + + try: + return xs1_api_client.XS1( + host=host, + port=port, + ssl=ssl, + user=user, + password=password) + except ConnectionError as error: + _LOGGER.error("Failed to create XS1 api client " + "because of a connection error: %s", error) + return None + + +async def async_setup(hass, config): + """Set up XS1 Component.""" + _LOGGER.debug("Initializing XS1") + + host = config[DOMAIN][CONF_HOST] + port = config[DOMAIN][CONF_PORT] + ssl = config[DOMAIN][CONF_SSL] + user = config[DOMAIN].get(CONF_USERNAME) + password = config[DOMAIN].get(CONF_PASSWORD) + + # initialize XS1 API + xs1 = await hass.async_add_executor_job( + partial(_create_controller_api, + host, port, ssl, user, password)) + if xs1 is None: + return False + + _LOGGER.debug( + "Establishing connection to XS1 gateway and retrieving data...") + + hass.data[DOMAIN] = {} + + actuators = await hass.async_add_executor_job( + partial(xs1.get_all_actuators, enabled=True)) + sensors = await hass.async_add_executor_job( + partial(xs1.get_all_sensors, enabled=True)) + + hass.data[DOMAIN][ACTUATORS] = actuators + hass.data[DOMAIN][SENSORS] = sensors + + _LOGGER.debug("Loading components for XS1 platform...") + # load components for supported devices + for component in XS1_COMPONENTS: + hass.async_create_task( + discovery.async_load_platform( + hass, component, DOMAIN, {}, config)) + + return True + + +class XS1DeviceEntity(Entity): + """Representation of a base XS1 device.""" + + def __init__(self, device): + """Initialize the XS1 device.""" + self.device = device + + async def async_update(self): + """Retrieve latest device state.""" + async with UPDATE_LOCK: + await self.hass.async_add_executor_job( + partial(self.device.update)) diff --git a/homeassistant/components/xs1/climate.py b/homeassistant/components/xs1/climate.py new file mode 100644 index 00000000000..0417d3bcde0 --- /dev/null +++ b/homeassistant/components/xs1/climate.py @@ -0,0 +1,109 @@ +""" +Support for XS1 climate devices. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/climate.xs1/ +""" +from functools import partial +import logging + +from homeassistant.components.climate import ( + ATTR_TEMPERATURE, ClimateDevice, SUPPORT_TARGET_TEMPERATURE) +from homeassistant.components.xs1 import ( + ACTUATORS, DOMAIN as COMPONENT_DOMAIN, SENSORS, XS1DeviceEntity) + +DEPENDENCIES = ['xs1'] +_LOGGER = logging.getLogger(__name__) + +MIN_TEMP = 8 +MAX_TEMP = 25 + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the XS1 thermostat platform.""" + from xs1_api_client.api_constants import ActuatorType + + actuators = hass.data[COMPONENT_DOMAIN][ACTUATORS] + sensors = hass.data[COMPONENT_DOMAIN][SENSORS] + + thermostat_entities = [] + for actuator in actuators: + if actuator.type() == ActuatorType.TEMPERATURE: + # Search for a matching sensor (by name) + actuator_name = actuator.name() + + matching_sensor = None + for sensor in sensors: + if actuator_name in sensor.name(): + matching_sensor = sensor + + break + + thermostat_entities.append( + XS1ThermostatEntity(actuator, matching_sensor)) + + async_add_entities(thermostat_entities) + + +class XS1ThermostatEntity(XS1DeviceEntity, ClimateDevice): + """Representation of a XS1 thermostat.""" + + def __init__(self, device, sensor): + """Initialize the actuator.""" + super().__init__(device) + self.sensor = sensor + + @property + def name(self): + """Return the name of the device if any.""" + return self.device.name() + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_TARGET_TEMPERATURE + + @property + def current_temperature(self): + """Return the current temperature.""" + if self.sensor is None: + return None + + return self.sensor.value() + + @property + def temperature_unit(self): + """Return the unit of measurement used by the platform.""" + return self.device.unit() + + @property + def target_temperature(self): + """Return the current target temperature.""" + return self.device.new_value() + + @property + def min_temp(self): + """Return the minimum temperature.""" + return MIN_TEMP + + @property + def max_temp(self): + """Return the maximum temperature.""" + return MAX_TEMP + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + temp = kwargs.get(ATTR_TEMPERATURE) + + self.device.set_value(temp) + + if self.sensor is not None: + self.schedule_update_ha_state() + + async def async_update(self): + """Also update the sensor when available.""" + await super().async_update() + if self.sensor is not None: + await self.hass.async_add_executor_job( + partial(self.sensor.update)) diff --git a/homeassistant/components/xs1/sensor.py b/homeassistant/components/xs1/sensor.py new file mode 100644 index 00000000000..b4d9bfe5ff9 --- /dev/null +++ b/homeassistant/components/xs1/sensor.py @@ -0,0 +1,57 @@ +""" +Support for XS1 sensors. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.xs1/ +""" + +import logging + +from homeassistant.components.xs1 import ( + ACTUATORS, DOMAIN as COMPONENT_DOMAIN, SENSORS, XS1DeviceEntity) +from homeassistant.helpers.entity import Entity + +DEPENDENCIES = ['xs1'] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the XS1 sensor platform.""" + from xs1_api_client.api_constants import ActuatorType + + sensors = hass.data[COMPONENT_DOMAIN][SENSORS] + actuators = hass.data[COMPONENT_DOMAIN][ACTUATORS] + + sensor_entities = [] + for sensor in sensors: + belongs_to_climate_actuator = False + for actuator in actuators: + if actuator.type() == ActuatorType.TEMPERATURE and \ + actuator.name() in sensor.name(): + belongs_to_climate_actuator = True + break + + if not belongs_to_climate_actuator: + sensor_entities.append(XS1Sensor(sensor)) + + async_add_entities(sensor_entities) + + +class XS1Sensor(XS1DeviceEntity, Entity): + """Representation of a Sensor.""" + + @property + def name(self): + """Return the name of the sensor.""" + return self.device.name() + + @property + def state(self): + """Return the state of the sensor.""" + return self.device.value() + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self.device.unit() diff --git a/homeassistant/components/xs1/switch.py b/homeassistant/components/xs1/switch.py new file mode 100644 index 00000000000..e6855865845 --- /dev/null +++ b/homeassistant/components/xs1/switch.py @@ -0,0 +1,52 @@ +""" +Support for XS1 switches. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.xs1/ +""" +import logging + +from homeassistant.components.xs1 import ( + ACTUATORS, DOMAIN as COMPONENT_DOMAIN, XS1DeviceEntity) +from homeassistant.helpers.entity import ToggleEntity + +DEPENDENCIES = ['xs1'] +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, + discovery_info=None): + """Set up the XS1 switch platform.""" + from xs1_api_client.api_constants import ActuatorType + + actuators = hass.data[COMPONENT_DOMAIN][ACTUATORS] + + switch_entities = [] + for actuator in actuators: + if (actuator.type() == ActuatorType.SWITCH) or \ + (actuator.type() == ActuatorType.DIMMER): + switch_entities.append(XS1SwitchEntity(actuator)) + + async_add_entities(switch_entities) + + +class XS1SwitchEntity(XS1DeviceEntity, ToggleEntity): + """Representation of a XS1 switch actuator.""" + + @property + def name(self): + """Return the name of the device if any.""" + return self.device.name() + + @property + def is_on(self): + """Return true if switch is on.""" + return self.device.value() == 100 + + def turn_on(self, **kwargs): + """Turn the device on.""" + self.device.turn_on() + + def turn_off(self, **kwargs): + """Turn the device off.""" + self.device.turn_off() diff --git a/requirements_all.txt b/requirements_all.txt index 7f618e0920a..452275bfcdc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1754,6 +1754,9 @@ xknx==0.9.3 # homeassistant.components.sensor.zestimate xmltodict==0.11.0 +# homeassistant.components.xs1 +xs1-api-client==2.3.5 + # homeassistant.components.sensor.yweather # homeassistant.components.weather.yweather yahooweather==0.10