diff --git a/homeassistant/components/insteon/climate.py b/homeassistant/components/insteon/climate.py new file mode 100644 index 00000000000..79af0892f94 --- /dev/null +++ b/homeassistant/components/insteon/climate.py @@ -0,0 +1,228 @@ +"""Support for Insteon thermostat.""" +import logging +from typing import List, Optional + +from pyinsteon.constants import ThermostatMode +from pyinsteon.operating_flag import CELSIUS + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + DOMAIN, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + SUPPORT_FAN_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT + +from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities + +_LOGGER = logging.getLogger(__name__) + +COOLING = 1 +HEATING = 2 +DEHUMIDIFYING = 3 +HUMIDIFYING = 4 + +TEMPERATURE = 10 +HUMIDITY = 11 +SYSTEM_MODE = 12 +FAN_MODE = 13 +COOL_SET_POINT = 14 +HEAT_SET_POINT = 15 +HUMIDITY_HIGH = 16 +HUMIDITY_LOW = 17 + + +HVAC_MODES = { + 0: HVAC_MODE_OFF, + 1: HVAC_MODE_HEAT, + 2: HVAC_MODE_COOL, + 3: HVAC_MODE_HEAT_COOL, +} +FAN_MODES = {4: HVAC_MODE_AUTO, 8: HVAC_MODE_FAN_ONLY} +SUPPORTED_FEATURES = ( + SUPPORT_FAN_MODE + | SUPPORT_TARGET_HUMIDITY + | SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Insteon platform.""" + async_add_insteon_entities( + hass, DOMAIN, InsteonClimateEntity, async_add_entities, discovery_info + ) + + +class InsteonClimateEntity(InsteonEntity, ClimateEntity): + """A Class for an Insteon climate entity.""" + + @property + def supported_features(self): + """Return the supported features for this entity.""" + return SUPPORTED_FEATURES + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement.""" + if self._insteon_device.properties[CELSIUS].value: + return TEMP_CELSIUS + return TEMP_FAHRENHEIT + + @property + def current_humidity(self) -> Optional[int]: + """Return the current humidity.""" + return self._insteon_device.groups[HUMIDITY].value + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + return HVAC_MODES[self._insteon_device.groups[SYSTEM_MODE].value] + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes.""" + return list(HVAC_MODES.values()) + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self._insteon_device.groups[TEMPERATURE].value + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.HEAT: + return self._insteon_device.groups[HEAT_SET_POINT].value + if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.COOL: + return self._insteon_device.groups[COOL_SET_POINT].value + return None + + @property + def target_temperature_high(self) -> Optional[float]: + """Return the highbound target temperature we try to reach.""" + if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.AUTO: + return self._insteon_device.groups[COOL_SET_POINT].value + return None + + @property + def target_temperature_low(self) -> Optional[float]: + """Return the lowbound target temperature we try to reach.""" + if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.AUTO: + return self._insteon_device.groups[HEAT_SET_POINT].value + return None + + @property + def fan_mode(self) -> Optional[str]: + """Return the fan setting.""" + return FAN_MODES[self._insteon_device.groups[FAN_MODE].value] + + @property + def fan_modes(self) -> Optional[List[str]]: + """Return the list of available fan modes.""" + return list(FAN_MODES.values()) + + @property + def target_humidity(self) -> Optional[int]: + """Return the humidity we try to reach.""" + high = self._insteon_device.groups[HUMIDITY_HIGH].value + low = self._insteon_device.groups[HUMIDITY_LOW].value + # May not be loaded yet so return a default if required + return (high + low) / 2 if high and low else None + + @property + def min_humidity(self) -> int: + """Return the minimum humidity.""" + return 1 + + @property + def hvac_action(self) -> Optional[str]: + """Return the current running hvac operation if supported. + + Need to be one of CURRENT_HVAC_*. + """ + if self._insteon_device.groups[COOLING].value: + return CURRENT_HVAC_COOL + if self._insteon_device.groups[HEATING].value: + return CURRENT_HVAC_HEAT + if self._insteon_device.groups[FAN_MODE].value == ThermostatMode.FAN_ALWAYS_ON: + return CURRENT_HVAC_FAN + return CURRENT_HVAC_IDLE + + @property + def device_state_attributes(self): + """Provide attributes for display on device card.""" + attr = super().device_state_attributes + humidifier = "off" + if self._insteon_device.groups[DEHUMIDIFYING].value: + humidifier = "dehumidifying" + if self._insteon_device.groups[HUMIDIFYING].value: + humidifier = "humidifying" + attr["humidifier"] = humidifier + return attr + + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if target_temp is not None: + if self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.HEAT: + await self._insteon_device.async_set_heat_set_point(target_temp) + elif self._insteon_device.groups[SYSTEM_MODE].value == ThermostatMode.COOL: + await self._insteon_device.async_set_cool_set_point(target_temp) + else: + await self._insteon_device.async_set_heat_set_point(target_temp_low) + await self._insteon_device.async_set_cool_set_point(target_temp_high) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + mode = list(FAN_MODES.keys())[list(FAN_MODES.values()).index(fan_mode)] + await self._insteon_device.async_set_mode(mode) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + mode = list(HVAC_MODES.keys())[list(HVAC_MODES.values()).index(hvac_mode)] + await self._insteon_device.async_set_mode(mode) + + async def async_set_humidity(self, humidity): + """Set new humidity level.""" + change = humidity - self.target_humidity + high = self._insteon_device.groups[HUMIDITY_HIGH].value + change + low = self._insteon_device.groups[HUMIDITY_LOW].value + change + await self._insteon_device.async_set_humidity_low_set_point(low) + await self._insteon_device.async_set_humidity_high_set_point(high) + + async def async_added_to_hass(self): + """Register INSTEON update events.""" + await super().async_added_to_hass() + await self._insteon_device.async_read_op_flags() + for group in [ + COOLING, + HEATING, + DEHUMIDIFYING, + HUMIDIFYING, + HEAT_SET_POINT, + FAN_MODE, + SYSTEM_MODE, + TEMPERATURE, + HUMIDITY, + HUMIDITY_HIGH, + HUMIDITY_LOW, + ]: + self._insteon_device.groups[group].subscribe(self.async_entity_update) diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index 950efd8dc7f..c55d733b73d 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -36,6 +36,7 @@ DOMAIN = "insteon" INSTEON_COMPONENTS = [ "binary_sensor", + "climate", "cover", "fan", "light", @@ -76,10 +77,12 @@ SRV_RESPONDER = "responder" SRV_HOUSECODE = "housecode" SRV_SCENE_ON = "scene_on" SRV_SCENE_OFF = "scene_off" +SRV_ADD_DEFAULT_LINKS = "add_default_links" SIGNAL_LOAD_ALDB = "load_aldb" SIGNAL_PRINT_ALDB = "print_aldb" SIGNAL_SAVE_DEVICES = "save_devices" +SIGNAL_ADD_DEFAULT_LINKS = "add_default_links" HOUSECODES = [ "a", diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index 80bb860477e..787c64ec841 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -9,6 +9,7 @@ from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.entity import Entity from .const import ( + SIGNAL_ADD_DEFAULT_LINKS, SIGNAL_LOAD_ALDB, SIGNAL_PRINT_ALDB, SIGNAL_SAVE_DEVICES, @@ -96,6 +97,10 @@ class InsteonEntity(Entity): ) print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}" async_dispatcher_connect(self.hass, print_signal, self._print_aldb) + default_links_signal = f"{self.entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}" + async_dispatcher_connect( + self.hass, default_links_signal, self._async_add_default_links + ) async def _async_read_aldb(self, reload): """Call device load process and print to log.""" @@ -116,3 +121,7 @@ class InsteonEntity(Entity): else: label = f"Group {self.group:d}" return label + + async def _async_add_default_links(self): + """Add default links between the device and the modem.""" + await self._insteon_device.async_add_default_links(self.address) diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py index aa3c0932919..a3e79fcd6d4 100644 --- a/homeassistant/components/insteon/ipdb.py +++ b/homeassistant/components/insteon/ipdb.py @@ -2,6 +2,8 @@ import logging from pyinsteon.device_types import ( + ClimateControl_Thermostat, + ClimateControl_WirelessThermostat, DimmableLightingControl, DimmableLightingControl_DinRail, DimmableLightingControl_FanLinc, @@ -40,6 +42,7 @@ from pyinsteon.device_types import ( ) from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.climate import DOMAIN as CLIMATE from homeassistant.components.cover import DOMAIN as COVER from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.light import DOMAIN as LIGHT @@ -95,6 +98,8 @@ DEVICE_PLATFORM = { SwitchedLightingControl_OutletLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, SwitchedLightingControl_SwitchLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, SwitchedLightingControl_ToggleLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + ClimateControl_Thermostat: {CLIMATE: [1]}, + ClimateControl_WirelessThermostat: {CLIMATE: [1]}, WindowCovering: {COVER: [1]}, X10Dimmable: {LIGHT: [1]}, X10OnOff: {SWITCH: [1]}, diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index b3192fc8f66..0fe8f30d95b 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -147,3 +147,6 @@ X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODE TRIGGER_SCENE_SCHEMA = vol.Schema( {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} ) + + +ADD_DEFAULT_LINKS_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) diff --git a/homeassistant/components/insteon/services.yaml b/homeassistant/components/insteon/services.yaml index 3d232569e9c..716b9a1e040 100644 --- a/homeassistant/components/insteon/services.yaml +++ b/homeassistant/components/insteon/services.yaml @@ -60,3 +60,9 @@ scene_off: group: description: INSTEON group or scene number example: 26 +add_default_links: + description: Add the default links between the device and the Insteon Modem (IM) + fields: + entity_id: + description: Name of the device to load. Use "all" to load the database of all devices. + example: "light.1a2b3c" diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index c0b93d93485..32a0949dfeb 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -36,10 +36,12 @@ from .const import ( EVENT_GROUP_ON, EVENT_GROUP_ON_FAST, ON_OFF_EVENTS, + SIGNAL_ADD_DEFAULT_LINKS, SIGNAL_LOAD_ALDB, SIGNAL_PRINT_ALDB, SIGNAL_SAVE_DEVICES, SRV_ADD_ALL_LINK, + SRV_ADD_DEFAULT_LINKS, SRV_ALL_LINK_GROUP, SRV_ALL_LINK_MODE, SRV_CONTROLLER, @@ -58,6 +60,7 @@ from .const import ( from .ipdb import get_device_platforms, get_platform_groups from .schemas import ( ADD_ALL_LINK_SCHEMA, + ADD_DEFAULT_LINKS_SCHEMA, DEL_ALL_LINK_SCHEMA, LOAD_ALDB_SCHEMA, PRINT_ALDB_SCHEMA, @@ -231,6 +234,13 @@ def async_register_services(hass): group = service.data.get(SRV_ALL_LINK_GROUP) await async_trigger_scene_off(group) + @callback + def async_add_default_links(service): + """Add the default All-Link entries to a device.""" + entity_id = service.data[CONF_ENTITY_ID] + signal = f"{entity_id}_{SIGNAL_ADD_DEFAULT_LINKS}" + async_dispatcher_send(hass, signal) + hass.services.async_register( DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA ) @@ -268,6 +278,13 @@ def async_register_services(hass): hass.services.async_register( DOMAIN, SRV_SCENE_OFF, async_srv_scene_off, schema=TRIGGER_SCENE_SCHEMA ) + + hass.services.async_register( + DOMAIN, + SRV_ADD_DEFAULT_LINKS, + async_add_default_links, + schema=ADD_DEFAULT_LINKS_SCHEMA, + ) async_dispatcher_connect(hass, SIGNAL_SAVE_DEVICES, async_srv_save_devices) _LOGGER.debug("Insteon Services registered")