Add Climate Platform Support to ISY994 (#35440)

* ISY994 Add support for climate platform

Remove services from Climate (not added yet)

* Incorporate suggestions based on review.

* Collect string literals to Const. Rename device to entity

* Fix merge error
This commit is contained in:
shbatm 2020-05-09 22:03:05 -05:00 committed by GitHub
parent a97460d1ab
commit 8947ce5053
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 345 additions and 22 deletions

View File

@ -37,7 +37,16 @@ from .const import (
DOMAIN as ISY994_DOMAIN, DOMAIN as ISY994_DOMAIN,
ISY994_NODES, ISY994_NODES,
ISY994_PROGRAMS, ISY994_PROGRAMS,
SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT,
SUBNODE_DUSK_DAWN,
SUBNODE_HEARTBEAT,
SUBNODE_LOW_BATTERY,
SUBNODE_MOTION_DISABLED,
SUBNODE_NEGATIVE,
SUBNODE_TAMPER,
TYPE_CATEGORY_CLIMATE, TYPE_CATEGORY_CLIMATE,
TYPE_INSTEON_MOTION,
) )
from .entity import ISYNodeEntity, ISYProgramEntity from .entity import ISYNodeEntity, ISYProgramEntity
from .helpers import migrate_old_unique_ids from .helpers import migrate_old_unique_ids
@ -48,17 +57,6 @@ DEVICE_PARENT_REQUIRED = [
DEVICE_CLASS_MOTION, DEVICE_CLASS_MOTION,
] ]
SUBNODE_CLIMATE_COOL = 2
SUBNODE_CLIMATE_HEAT = 3
SUBNODE_NEGATIVE = 2
SUBNODE_HEARTBEAT = 4
SUBNODE_DUSK_DAWN = 2
SUBNODE_LOW_BATTERY = 3
SUBNODE_TAMPER = (10, 16) # Int->10 or Hex->0xA depending on firmware
SUBNODE_MOTION_DISABLED = (13, 19) # Int->13 or Hex->0xD depending on firmware
TYPE_INSTEON_MOTION = ("16.1.", "16.22.")
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistantType, hass: HomeAssistantType,

View File

@ -0,0 +1,260 @@
"""Support for Insteon Thermostats via ISY994 Platform."""
from typing import Callable, List, Optional, Union
from pyisy.constants import (
CMD_CLIMATE_FAN_SETTING,
CMD_CLIMATE_MODE,
ISY_VALUE_UNKNOWN,
PROP_HEAT_COOL_STATE,
PROP_HUMIDITY,
PROP_SETPOINT_COOL,
PROP_SETPOINT_HEAT,
PROP_UOM,
PROTO_INSTEON,
)
from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
DOMAIN as CLIMATE,
FAN_AUTO,
FAN_ON,
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
SUPPORT_FAN_MODE,
SUPPORT_TARGET_TEMPERATURE,
SUPPORT_TARGET_TEMPERATURE_RANGE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
PRECISION_TENTHS,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
)
from homeassistant.helpers.typing import HomeAssistantType
from .const import (
_LOGGER,
DOMAIN as ISY994_DOMAIN,
HA_FAN_TO_ISY,
HA_HVAC_TO_ISY,
ISY994_NODES,
ISY_HVAC_MODES,
UOM_DOUBLE_TEMP,
UOM_FAN_MODES,
UOM_HVAC_ACTIONS,
UOM_HVAC_MODE_GENERIC,
UOM_HVAC_MODE_INSTEON,
UOM_ISY_CELSIUS,
UOM_ISY_FAHRENHEIT,
UOM_ISYV4_DEGREES,
UOM_ISYV4_NONE,
UOM_TO_STATES,
)
from .entity import ISYNodeEntity
from .helpers import migrate_old_unique_ids
ISY_SUPPORTED_FEATURES = (
SUPPORT_FAN_MODE | SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE
)
async def async_setup_entry(
hass: HomeAssistantType,
entry: ConfigEntry,
async_add_entities: Callable[[list], None],
) -> bool:
"""Set up the ISY994 thermostat platform."""
entities = []
hass_isy_data = hass.data[ISY994_DOMAIN][entry.entry_id]
for node in hass_isy_data[ISY994_NODES][CLIMATE]:
entities.append(ISYThermostatEntity(node))
await migrate_old_unique_ids(hass, CLIMATE, entities)
async_add_entities(entities)
def convert_isy_temp_to_hass(
temp: Union[int, float, None], uom: str, precision: str
) -> float:
"""Fix Insteon Thermostats' Reported Temperature.
Insteon Thermostats report temperature in 0.5-deg precision as an int
by sending a value of 2 times the Temp. Correct by dividing by 2 here.
Z-Wave Thermostats report temps in tenths as an integer and precision.
Correct by shifting the decimal place left by the value of precision.
"""
if temp is None or temp == ISY_VALUE_UNKNOWN:
return None
if uom in [UOM_DOUBLE_TEMP, UOM_ISYV4_DEGREES]:
return round(float(temp) / 2.0, 1)
if precision != "0":
return round(float(temp) / 10 ** int(precision), int(precision))
return round(float(temp), 1)
class ISYThermostatEntity(ISYNodeEntity, ClimateEntity):
"""Representation of an ISY994 thermostat entity."""
def __init__(self, node) -> None:
"""Initialize the ISY Thermostat entity."""
super().__init__(node)
self._node = node
self._uom = self._node.uom
if isinstance(self._uom, list):
self._uom = self._node.uom[0]
self._hvac_action = None
self._hvac_mode = None
self._fan_mode = None
self._temp_unit = None
self._current_humidity = 0
self._target_temp_low = 0
self._target_temp_high = 0
@property
def supported_features(self) -> int:
"""Return the list of supported features."""
return ISY_SUPPORTED_FEATURES
@property
def precision(self) -> str:
"""Return the precision of the system."""
return PRECISION_TENTHS
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement."""
uom = self._node.aux_properties.get(PROP_UOM)
if not uom:
return self.hass.config.units.temperature_unit
if uom.value == UOM_ISY_CELSIUS:
return TEMP_CELSIUS
if uom.value == UOM_ISY_FAHRENHEIT:
return TEMP_FAHRENHEIT
@property
def current_humidity(self) -> Optional[int]:
"""Return the current humidity."""
humidity = self._node.aux_properties.get(PROP_HUMIDITY)
if not humidity:
return None
return int(humidity.value)
@property
def hvac_mode(self) -> Optional[str]:
"""Return hvac operation ie. heat, cool mode."""
hvac_mode = self._node.aux_properties.get(CMD_CLIMATE_MODE)
if not hvac_mode:
return None
# Which state values used depends on the mode property's UOM:
uom = hvac_mode.uom
# Handle special case for ISYv4 Firmware:
if uom == UOM_ISYV4_NONE:
uom = (
UOM_HVAC_MODE_INSTEON
if self._node.protocol == PROTO_INSTEON
else UOM_HVAC_MODE_GENERIC
)
return UOM_TO_STATES[uom].get(hvac_mode.value)
@property
def hvac_modes(self) -> List[str]:
"""Return the list of available hvac operation modes."""
return ISY_HVAC_MODES
@property
def hvac_action(self) -> Optional[str]:
"""Return the current running hvac operation if supported."""
hvac_action = self._node.aux_properties.get(PROP_HEAT_COOL_STATE)
if not hvac_action:
return None
return UOM_TO_STATES[UOM_HVAC_ACTIONS].get(hvac_action.value)
@property
def current_temperature(self) -> Optional[float]:
"""Return the current temperature."""
return convert_isy_temp_to_hass(self._node.status, self._uom, self._node.prec)
@property
def target_temperature_step(self) -> Optional[float]:
"""Return the supported step of target temperature."""
return 1.0
@property
def target_temperature(self) -> Optional[float]:
"""Return the temperature we try to reach."""
if self.hvac_mode == HVAC_MODE_COOL:
return self.target_temperature_high
if self.hvac_mode == HVAC_MODE_HEAT:
return self.target_temperature_low
return None
@property
def target_temperature_high(self) -> Optional[float]:
"""Return the highbound target temperature we try to reach."""
target = self._node.aux_properties.get(PROP_SETPOINT_COOL)
if not target:
return None
return convert_isy_temp_to_hass(target.value, target.uom, target.prec)
@property
def target_temperature_low(self) -> Optional[float]:
"""Return the lowbound target temperature we try to reach."""
target = self._node.aux_properties.get(PROP_SETPOINT_HEAT)
if not target:
return None
return convert_isy_temp_to_hass(target.value, target.uom, target.prec)
@property
def fan_modes(self):
"""Return the list of available fan modes."""
return [FAN_AUTO, FAN_ON]
@property
def fan_mode(self) -> str:
"""Return the current fan mode ie. auto, on."""
fan_mode = self._node.aux_properties.get(CMD_CLIMATE_FAN_SETTING)
if not fan_mode:
return None
return UOM_TO_STATES[UOM_FAN_MODES].get(fan_mode.value)
def 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.hvac_mode == HVAC_MODE_COOL:
target_temp_high = target_temp
if self.hvac_mode == HVAC_MODE_HEAT:
target_temp_low = target_temp
if target_temp_low is not None:
self._node.set_climate_setpoint_heat(int(target_temp_low))
# Presumptive setting--event stream will correct if cmd fails:
self._target_temp_low = target_temp_low
if target_temp_high is not None:
self._node.set_climate_setpoint_cool(int(target_temp_high))
# Presumptive setting--event stream will correct if cmd fails:
self._target_temp_high = target_temp_high
self.schedule_update_ha_state()
def set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
_LOGGER.debug("Requested fan mode %s", fan_mode)
self._node.set_fan_mode(HA_FAN_TO_ISY.get(fan_mode))
# Presumptive setting--event stream will correct if cmd fails:
self._fan_mode = fan_mode
self.schedule_update_ha_state()
def set_hvac_mode(self, hvac_mode: str) -> None:
"""Set new target hvac mode."""
_LOGGER.debug("Requested operation mode %s", hvac_mode)
self._node.set_climate_mode(HA_HVAC_TO_ISY.get(hvac_mode))
# Presumptive setting--event stream will correct if cmd fails:
self._hvac_mode = hvac_mode
self.schedule_update_ha_state()

View File

@ -22,6 +22,7 @@ from homeassistant.components.climate.const import (
CURRENT_HVAC_FAN, CURRENT_HVAC_FAN,
CURRENT_HVAC_HEAT, CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE, CURRENT_HVAC_IDLE,
DOMAIN as CLIMATE,
FAN_AUTO, FAN_AUTO,
FAN_HIGH, FAN_HIGH,
FAN_MEDIUM, FAN_MEDIUM,
@ -109,7 +110,7 @@ DEFAULT_PROGRAM_STRING = "HA."
KEY_ACTIONS = "actions" KEY_ACTIONS = "actions"
KEY_STATUS = "status" KEY_STATUS = "status"
SUPPORTED_PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH] SUPPORTED_PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH, CLIMATE]
SUPPORTED_PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH] SUPPORTED_PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH]
SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"]
@ -128,6 +129,19 @@ FILTER_NODE_DEF_ID = "node_def_id"
FILTER_INSTEON_TYPE = "insteon_type" FILTER_INSTEON_TYPE = "insteon_type"
FILTER_ZWAVE_CAT = "zwave_cat" FILTER_ZWAVE_CAT = "zwave_cat"
# Special Subnodes for some Insteon Devices
SUBNODE_CLIMATE_COOL = 2
SUBNODE_CLIMATE_HEAT = 3
SUBNODE_DUSK_DAWN = 2
SUBNODE_EZIO2X4_SENSORS = [9, 10, 11, 12]
SUBNODE_FANLINC_LIGHT = 1
SUBNODE_HEARTBEAT = 4
SUBNODE_IOLINC_RELAY = 2
SUBNODE_LOW_BATTERY = 3
SUBNODE_MOTION_DISABLED = (13, 19) # Int->13 or Hex->0xD depending on firmware
SUBNODE_NEGATIVE = 2
SUBNODE_TAMPER = (10, 16) # Int->10 or Hex->0xA depending on firmware
# Generic Insteon Type Categories for Filters # Generic Insteon Type Categories for Filters
TYPE_CATEGORY_CONTROLLERS = "0." TYPE_CATEGORY_CONTROLLERS = "0."
TYPE_CATEGORY_DIMMABLE = "1." TYPE_CATEGORY_DIMMABLE = "1."
@ -142,6 +156,9 @@ TYPE_CATEGORY_LOCK = "15."
TYPE_CATEGORY_SAFETY = "16." TYPE_CATEGORY_SAFETY = "16."
TYPE_CATEGORY_X10 = "113." TYPE_CATEGORY_X10 = "113."
TYPE_EZIO2X4 = "7.3.255."
TYPE_INSTEON_MOTION = ("16.1.", "16.22.")
UNDO_UPDATE_LISTENER = "undo_update_listener" UNDO_UPDATE_LISTENER = "undo_update_listener"
# Do not use the Home Assistant consts for the states here - we're matching exact API # Do not use the Home Assistant consts for the states here - we're matching exact API
@ -261,8 +278,27 @@ NODE_FILTERS = {
], ],
FILTER_ZWAVE_CAT: ["121", "122", "123", "137", "141", "147"], FILTER_ZWAVE_CAT: ["121", "122", "123", "137", "141", "147"],
}, },
CLIMATE: {
FILTER_UOM: ["2"],
FILTER_STATES: ["heating", "cooling", "idle", "fan_only", "off"],
FILTER_NODE_DEF_ID: ["TempLinc", "Thermostat"],
FILTER_INSTEON_TYPE: ["4.8", TYPE_CATEGORY_CLIMATE],
FILTER_ZWAVE_CAT: ["140"],
},
} }
UOM_ISYV4_DEGREES = "degrees"
UOM_ISYV4_NONE = "n/a"
UOM_ISY_CELSIUS = 1
UOM_ISY_FAHRENHEIT = 2
UOM_DOUBLE_TEMP = "101"
UOM_HVAC_ACTIONS = "66"
UOM_HVAC_MODE_GENERIC = "67"
UOM_HVAC_MODE_INSTEON = "98"
UOM_FAN_MODES = "99"
UOM_FRIENDLY_NAME = { UOM_FRIENDLY_NAME = {
"1": "A", "1": "A",
"3": f"btu/{TIME_HOURS}", "3": f"btu/{TIME_HOURS}",
@ -400,7 +436,7 @@ UOM_TO_STATES = {
26: "hardware failure", 26: "hardware failure",
27: "factory reset", 27: "factory reset",
}, },
"66": { # Thermostat Heat/Cool State UOM_HVAC_ACTIONS: { # Thermostat Heat/Cool State
0: CURRENT_HVAC_IDLE, 0: CURRENT_HVAC_IDLE,
1: CURRENT_HVAC_HEAT, 1: CURRENT_HVAC_HEAT,
2: CURRENT_HVAC_COOL, 2: CURRENT_HVAC_COOL,
@ -415,7 +451,7 @@ UOM_TO_STATES = {
10: CURRENT_HVAC_HEAT, 10: CURRENT_HVAC_HEAT,
11: CURRENT_HVAC_HEAT, 11: CURRENT_HVAC_HEAT,
}, },
"67": { # Thermostat Mode UOM_HVAC_MODE_GENERIC: { # Thermostat Mode
0: HVAC_MODE_OFF, 0: HVAC_MODE_OFF,
1: HVAC_MODE_HEAT, 1: HVAC_MODE_HEAT,
2: HVAC_MODE_COOL, 2: HVAC_MODE_COOL,
@ -524,7 +560,7 @@ UOM_TO_STATES = {
b: f"{b} %" for a, b in enumerate(list(range(1, 100))) b: f"{b} %" for a, b in enumerate(list(range(1, 100)))
}, # 1-99 are percentage open }, # 1-99 are percentage open
}, },
"98": { # Insteon Thermostat Mode UOM_HVAC_MODE_INSTEON: { # Insteon Thermostat Mode
0: HVAC_MODE_OFF, 0: HVAC_MODE_OFF,
1: HVAC_MODE_HEAT, 1: HVAC_MODE_HEAT,
2: HVAC_MODE_COOL, 2: HVAC_MODE_COOL,
@ -534,7 +570,7 @@ UOM_TO_STATES = {
6: HVAC_MODE_AUTO, # Program Heat-Set @ Local Device Only 6: HVAC_MODE_AUTO, # Program Heat-Set @ Local Device Only
7: HVAC_MODE_AUTO, # Program Cool-Set @ Local Device Only 7: HVAC_MODE_AUTO, # Program Cool-Set @ Local Device Only
}, },
"99": {7: FAN_ON, 8: FAN_AUTO}, # Insteon Thermostat Fan Mode UOM_FAN_MODES: {7: FAN_ON, 8: FAN_AUTO}, # Insteon Thermostat Fan Mode
"115": { # Most recent On style action taken for lamp control "115": { # Most recent On style action taken for lamp control
0: "on", 0: "on",
1: "off", 1: "off",
@ -552,6 +588,26 @@ UOM_TO_STATES = {
}, },
} }
ISY_HVAC_MODES = [
HVAC_MODE_OFF,
HVAC_MODE_HEAT,
HVAC_MODE_COOL,
HVAC_MODE_HEAT_COOL,
HVAC_MODE_AUTO,
HVAC_MODE_FAN_ONLY,
]
HA_HVAC_TO_ISY = {
HVAC_MODE_OFF: "off",
HVAC_MODE_HEAT: "heat",
HVAC_MODE_COOL: "cool",
HVAC_MODE_HEAT_COOL: "auto",
HVAC_MODE_FAN_ONLY: "fan_only",
HVAC_MODE_AUTO: "program_auto",
}
HA_FAN_TO_ISY = {FAN_ON: "on", FAN_AUTO: "auto"}
BINARY_SENSOR_DEVICE_TYPES_ISY = { BINARY_SENSOR_DEVICE_TYPES_ISY = {
DEVICE_CLASS_MOISTURE: ["16.8.", "16.13.", "16.14."], DEVICE_CLASS_MOISTURE: ["16.8.", "16.13.", "16.14."],
DEVICE_CLASS_OPENING: [ DEVICE_CLASS_OPENING: [

View File

@ -12,6 +12,7 @@ from pyisy.nodes import Group, Node, Nodes
from pyisy.programs import Programs from pyisy.programs import Programs
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR
from homeassistant.components.climate.const import DOMAIN as CLIMATE
from homeassistant.components.fan import DOMAIN as FAN from homeassistant.components.fan import DOMAIN as FAN
from homeassistant.components.light import DOMAIN as LIGHT from homeassistant.components.light import DOMAIN as LIGHT
from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.sensor import DOMAIN as SENSOR
@ -34,20 +35,20 @@ from .const import (
KEY_ACTIONS, KEY_ACTIONS,
KEY_STATUS, KEY_STATUS,
NODE_FILTERS, NODE_FILTERS,
SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT,
SUBNODE_EZIO2X4_SENSORS,
SUBNODE_FANLINC_LIGHT,
SUBNODE_IOLINC_RELAY,
SUPPORTED_PLATFORMS, SUPPORTED_PLATFORMS,
SUPPORTED_PROGRAM_PLATFORMS, SUPPORTED_PROGRAM_PLATFORMS,
TYPE_CATEGORY_SENSOR_ACTUATORS, TYPE_CATEGORY_SENSOR_ACTUATORS,
TYPE_EZIO2X4,
) )
BINARY_SENSOR_UOMS = ["2", "78"] BINARY_SENSOR_UOMS = ["2", "78"]
BINARY_SENSOR_ISY_STATES = ["on", "off"] BINARY_SENSOR_ISY_STATES = ["on", "off"]
TYPE_EZIO2X4 = "7.3.255."
SUBNODE_EZIO2X4_SENSORS = [9, 10, 11, 12]
SUBNODE_FANLINC_LIGHT = 1
SUBNODE_IOLINC_RELAY = 2
def _check_for_node_def( def _check_for_node_def(
hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None hass_isy_data: dict, node: Union[Group, Node], single_platform: str = None
@ -107,6 +108,14 @@ def _check_for_insteon_type(
hass_isy_data[ISY994_NODES][LIGHT].append(node) hass_isy_data[ISY994_NODES][LIGHT].append(node)
return True return True
# Thermostats, which has a "Heat" and "Cool" sub-node on address 2 and 3
if platform == CLIMATE and subnode_id in [
SUBNODE_CLIMATE_COOL,
SUBNODE_CLIMATE_HEAT,
]:
hass_isy_data[ISY994_NODES][BINARY_SENSOR].append(node)
return True
# IOLincs which have a sensor and relay on 2 different nodes # IOLincs which have a sensor and relay on 2 different nodes
if ( if (
platform == BINARY_SENSOR platform == BINARY_SENSOR