diff --git a/.coveragerc b/.coveragerc index 1838b1cb3f2..72451dab531 100644 --- a/.coveragerc +++ b/.coveragerc @@ -247,7 +247,7 @@ omit = homeassistant/components/opencv.py homeassistant/components/*/opencv.py - homeassistant/components/opentherm_gw.py + homeassistant/components/opentherm_gw/* homeassistant/components/*/opentherm_gw.py homeassistant/components/openuv/__init__.py diff --git a/homeassistant/components/opentherm_gw.py b/homeassistant/components/opentherm_gw/__init__.py similarity index 50% rename from homeassistant/components/opentherm_gw.py rename to homeassistant/components/opentherm_gw/__init__.py index 7152a58afcc..3cf66c72a3a 100644 --- a/homeassistant/components/opentherm_gw.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -5,14 +5,16 @@ For more details about this component, please refer to the documentation at http://home-assistant.io/components/opentherm_gw/ """ import logging +from datetime import datetime, date import voluptuous as vol from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR from homeassistant.components.sensor import DOMAIN as COMP_SENSOR -from homeassistant.const import (CONF_DEVICE, CONF_MONITORED_VARIABLES, - CONF_NAME, PRECISION_HALVES, PRECISION_TENTHS, - PRECISION_WHOLE) +from homeassistant.const import ( + ATTR_DATE, ATTR_ID, ATTR_TEMPERATURE, ATTR_TIME, CONF_DEVICE, + CONF_MONITORED_VARIABLES, CONF_NAME, EVENT_HOMEASSISTANT_STOP, + PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE) from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -20,16 +22,72 @@ import homeassistant.helpers.config_validation as cv DOMAIN = 'opentherm_gw' +ATTR_MODE = 'mode' +ATTR_LEVEL = 'level' + CONF_CLIMATE = 'climate' CONF_FLOOR_TEMP = 'floor_temperature' CONF_PRECISION = 'precision' DATA_DEVICE = 'device' DATA_GW_VARS = 'gw_vars' +DATA_LATEST_STATUS = 'latest_status' DATA_OPENTHERM_GW = 'opentherm_gw' SIGNAL_OPENTHERM_GW_UPDATE = 'opentherm_gw_update' +SERVICE_RESET_GATEWAY = 'reset_gateway' + +SERVICE_SET_CLOCK = 'set_clock' +SERVICE_SET_CLOCK_SCHEMA = vol.Schema({ + vol.Optional(ATTR_DATE, default=date.today()): cv.date, + vol.Optional(ATTR_TIME, default=datetime.now().time()): cv.time, +}) + +SERVICE_SET_CONTROL_SETPOINT = 'set_control_setpoint' +SERVICE_SET_CONTROL_SETPOINT_SCHEMA = vol.Schema({ + vol.Required(ATTR_TEMPERATURE): vol.All(vol.Coerce(float), + vol.Range(min=0, max=90)), +}) + +SERVICE_SET_GPIO_MODE = 'set_gpio_mode' +SERVICE_SET_GPIO_MODE_SCHEMA = vol.Schema(vol.Any( + vol.Schema({ + vol.Required(ATTR_ID): vol.Equal('A'), + vol.Required(ATTR_MODE): vol.All(vol.Coerce(int), + vol.Range(min=0, max=6)), + }), + vol.Schema({ + vol.Required(ATTR_ID): vol.Equal('B'), + vol.Required(ATTR_MODE): vol.All(vol.Coerce(int), + vol.Range(min=0, max=7)), + }), +)) + +SERVICE_SET_LED_MODE = 'set_led_mode' +SERVICE_SET_LED_MODE_SCHEMA = vol.Schema({ + vol.Required(ATTR_ID): vol.In('ABCDEF'), + vol.Required(ATTR_MODE): vol.In('RXTBOFHWCEMP'), +}) + +SERVICE_SET_MAX_MOD = 'set_max_modulation' +SERVICE_SET_MAX_MOD_SCHEMA = vol.Schema({ + vol.Required(ATTR_LEVEL): vol.All(vol.Coerce(int), + vol.Range(min=-1, max=100)) +}) + +SERVICE_SET_OAT = 'set_outside_temperature' +SERVICE_SET_OAT_SCHEMA = vol.Schema({ + vol.Required(ATTR_TEMPERATURE): vol.All(vol.Coerce(float), + vol.Range(min=-40, max=99)), +}) + +SERVICE_SET_SB_TEMP = 'set_setback_temperature' +SERVICE_SET_SB_TEMP_SCHEMA = vol.Schema({ + vol.Required(ATTR_TEMPERATURE): vol.All(vol.Coerce(float), + vol.Range(min=0, max=30)), +}) + CLIMATE_SCHEMA = vol.Schema({ vol.Optional(CONF_NAME, default="OpenTherm Gateway"): cv.string, vol.Optional(CONF_PRECISION): vol.In([PRECISION_TENTHS, PRECISION_HALVES, @@ -46,7 +104,7 @@ CONFIG_SCHEMA = vol.Schema({ }), }, extra=vol.ALLOW_EXTRA) -REQUIREMENTS = ['pyotgw==0.2b1'] +REQUIREMENTS = ['pyotgw==0.3b0'] _LOGGER = logging.getLogger(__name__) @@ -60,9 +118,11 @@ async def async_setup(hass, config): hass.data[DATA_OPENTHERM_GW] = { DATA_DEVICE: gateway, DATA_GW_VARS: pyotgw.vars, + DATA_LATEST_STATUS: {} } hass.async_create_task(connect_and_subscribe( hass, conf[CONF_DEVICE], gateway)) + hass.async_create_task(register_services(hass, gateway)) hass.async_create_task(async_load_platform( hass, 'climate', DOMAIN, conf.get(CONF_CLIMATE), config)) if monitored_vars: @@ -76,13 +136,110 @@ async def connect_and_subscribe(hass, device_path, gateway): await gateway.connect(hass.loop, device_path) _LOGGER.debug("Connected to OpenTherm Gateway at %s", device_path) + async def cleanup(event): + """Reset overrides on the gateway.""" + await gateway.set_control_setpoint(0) + await gateway.set_max_relative_mod('-') + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, cleanup) + async def handle_report(status): """Handle reports from the OpenTherm Gateway.""" _LOGGER.debug("Received report: %s", status) + hass.data[DATA_OPENTHERM_GW][DATA_LATEST_STATUS] = status async_dispatcher_send(hass, SIGNAL_OPENTHERM_GW_UPDATE, status) gateway.subscribe(handle_report) +async def register_services(hass, gateway): + """Register services for the component.""" + gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] + + async def reset_gateway(call): + """Reset the OpenTherm Gateway.""" + mode_rst = gw_vars.OTGW_MODE_RESET + status = await gateway.set_mode(mode_rst) + hass.data[DATA_OPENTHERM_GW][DATA_LATEST_STATUS] = status + async_dispatcher_send(hass, SIGNAL_OPENTHERM_GW_UPDATE, status) + hass.services.async_register(DOMAIN, SERVICE_RESET_GATEWAY, reset_gateway) + + async def set_control_setpoint(call): + """Set the control setpoint on the OpenTherm Gateway.""" + gw_var = gw_vars.DATA_CONTROL_SETPOINT + value = await gateway.set_control_setpoint(call.data[ATTR_TEMPERATURE]) + status = hass.data[DATA_OPENTHERM_GW][DATA_LATEST_STATUS] + status.update({gw_var: value}) + async_dispatcher_send(hass, SIGNAL_OPENTHERM_GW_UPDATE, status) + hass.services.async_register(DOMAIN, SERVICE_SET_CONTROL_SETPOINT, + set_control_setpoint, + SERVICE_SET_CONTROL_SETPOINT_SCHEMA) + + async def set_device_clock(call): + """Set the clock on the OpenTherm Gateway.""" + attr_date = call.data[ATTR_DATE] + attr_time = call.data[ATTR_TIME] + await gateway.set_clock(datetime.combine(attr_date, attr_time)) + hass.services.async_register(DOMAIN, SERVICE_SET_CLOCK, set_device_clock, + SERVICE_SET_CLOCK_SCHEMA) + + async def set_gpio_mode(call): + """Set the OpenTherm Gateway GPIO modes.""" + gpio_id = call.data[ATTR_ID] + gpio_mode = call.data[ATTR_MODE] + mode = await gateway.set_gpio_mode(gpio_id, gpio_mode) + gpio_var = getattr(gw_vars, 'OTGW_GPIO_{}'.format(gpio_id)) + status = hass.data[DATA_OPENTHERM_GW][DATA_LATEST_STATUS] + status.update({gpio_var: mode}) + async_dispatcher_send(hass, SIGNAL_OPENTHERM_GW_UPDATE, status) + hass.services.async_register(DOMAIN, SERVICE_SET_GPIO_MODE, set_gpio_mode, + SERVICE_SET_GPIO_MODE_SCHEMA) + + async def set_led_mode(call): + """Set the OpenTherm Gateway LED modes.""" + led_id = call.data[ATTR_ID] + led_mode = call.data[ATTR_MODE] + mode = await gateway.set_led_mode(led_id, led_mode) + led_var = getattr(gw_vars, 'OTGW_LED_{}'.format(led_id)) + status = hass.data[DATA_OPENTHERM_GW][DATA_LATEST_STATUS] + status.update({led_var: mode}) + async_dispatcher_send(hass, SIGNAL_OPENTHERM_GW_UPDATE, status) + hass.services.async_register(DOMAIN, SERVICE_SET_LED_MODE, set_led_mode, + SERVICE_SET_LED_MODE_SCHEMA) + + async def set_max_mod(call): + """Set the max modulation level.""" + gw_var = gw_vars.DATA_SLAVE_MAX_RELATIVE_MOD + level = call.data[ATTR_LEVEL] + if level == -1: + # Backend only clears setting on non-numeric values. + level = '-' + value = await gateway.set_max_relative_mod(level) + status = hass.data[DATA_OPENTHERM_GW][DATA_LATEST_STATUS] + status.update({gw_var: value}) + async_dispatcher_send(hass, SIGNAL_OPENTHERM_GW_UPDATE, status) + hass.services.async_register(DOMAIN, SERVICE_SET_MAX_MOD, set_max_mod, + SERVICE_SET_MAX_MOD_SCHEMA) + + async def set_outside_temp(call): + """Provide the outside temperature to the OpenTherm Gateway.""" + gw_var = gw_vars.DATA_OUTSIDE_TEMP + value = await gateway.set_outside_temp(call.data[ATTR_TEMPERATURE]) + status = hass.data[DATA_OPENTHERM_GW][DATA_LATEST_STATUS] + status.update({gw_var: value}) + async_dispatcher_send(hass, SIGNAL_OPENTHERM_GW_UPDATE, status) + hass.services.async_register(DOMAIN, SERVICE_SET_OAT, set_outside_temp, + SERVICE_SET_OAT_SCHEMA) + + async def set_setback_temp(call): + """Set the OpenTherm Gateway SetBack temperature.""" + gw_var = gw_vars.OTGW_SB_TEMP + value = await gateway.set_setback_temp(call.data[ATTR_TEMPERATURE]) + status = hass.data[DATA_OPENTHERM_GW][DATA_LATEST_STATUS] + status.update({gw_var: value}) + async_dispatcher_send(hass, SIGNAL_OPENTHERM_GW_UPDATE, status) + hass.services.async_register(DOMAIN, SERVICE_SET_SB_TEMP, set_setback_temp, + SERVICE_SET_SB_TEMP_SCHEMA) + + async def setup_monitored_vars(hass, config, monitored_vars): """Set up requested sensors.""" gw_vars = hass.data[DATA_OPENTHERM_GW][DATA_GW_VARS] @@ -203,4 +360,5 @@ async def setup_monitored_vars(hass, config, monitored_vars): hass.async_create_task(async_load_platform( hass, COMP_BINARY_SENSOR, DOMAIN, binary_sensors, config)) if sensors: - await async_load_platform(hass, COMP_SENSOR, DOMAIN, sensors, config) + hass.async_create_task(async_load_platform( + hass, COMP_SENSOR, DOMAIN, sensors, config)) diff --git a/homeassistant/components/opentherm_gw/services.yaml b/homeassistant/components/opentherm_gw/services.yaml new file mode 100644 index 00000000000..df08ccaa4f9 --- /dev/null +++ b/homeassistant/components/opentherm_gw/services.yaml @@ -0,0 +1,81 @@ +# Describes the format for available opentherm_gw services + +reset_gateway: + description: Reset the OpenTherm Gateway. + +set_clock: + description: Set the clock and day of the week on the connected thermostat. + fields: + date: + description: Optional date from which the day of the week will be extracted. Defaults to today. + example: '2018-10-23' + time: + description: Optional time in 24h format which will be provided to the thermostat. Defaults to the current time. + example: '19:34' + +set_control_setpoint: + description: > + Set the central heating control setpoint override on the gateway. + You will only need this if you are writing your own software thermostat. + fields: + temperature: + description: > + The central heating setpoint to set on the gateway. + Values between 0 and 90 are accepted, but not all boilers support this range. + A value of 0 disables the central heating setpoint override. + example: '37.5' + +set_gpio_mode: + description: Change the function of the GPIO pins of the gateway. + fields: + id: + description: The ID of the GPIO pin. Either "A" or "B". + example: 'B' + mode: + description: > + Mode to set on the GPIO pin. Values 0 through 6 are accepted for both GPIOs, 7 is only accepted for GPIO "B". + See https://www.home-assistant.io/components/opentherm_gw/#gpio-modes for an explanation of the values. + example: '5' + +set_led_mode: + description: Change the function of the LEDs of the gateway. + fields: + id: + description: The ID of the LED. Possible values are "A" through "F". + example: 'C' + mode: + description: > + The function to assign to the LED. One of "R", "X", "T", "B", "O", "F", "H", "W", "C", "E", "M" or "P". + See https://www.home-assistant.io/components/opentherm_gw/#led-modes for an explanation of the values. + example: 'F' + +set_max_modulation: + description: > + Override the maximum relative modulation level. + You will only need this if you are writing your own software thermostat. + fields: + level: + description: > + The modulation level to provide to the gateway. + Values between 0 and 100 will set the modulation level. + Provide a value of -1 to clear the override and forward the value from the thermostat again. + example: '42' + +set_outside_temperature: + description: > + Provide an outside temperature to the thermostat. + If your thermostat is unable to display an outside temperature and does not support OTC (Outside Temperature Correction), this has no effect. + fields: + temperature: + description: > + The temperature to provide to the thermostat. + Values between -40.0 and 64.0 will be accepted, but not all thermostats can display the full range. + Any value above 64.0 will clear a previously configured value (suggestion: 99) + example: '-2.3' + +set_setback_temperature: + description: Configure the setback temperature to be used with the GPIO away mode function. + fields: + temperature: + description: The setback temperature to configure on the gateway. Values between 0.0 and 30.0 are accepted. + example: '16.0' diff --git a/requirements_all.txt b/requirements_all.txt index 6dd7488a2e9..3a029503935 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1038,7 +1038,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.2b1 +pyotgw==0.3b0 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp