From ced6df158bd66acace2e711c73a15760a95e8a02 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 14 Jan 2020 11:26:59 +0100 Subject: [PATCH] Refactor HomeMatic / Fix issue with 0.104/dev (#30752) * Refactor HomeMatic / Fix issue with 0.104/dev * Fix lock --- .../components/homematic/__init__.py | 552 ++---------------- .../components/homematic/binary_sensor.py | 6 +- homeassistant/components/homematic/climate.py | 5 +- homeassistant/components/homematic/const.py | 212 +++++++ homeassistant/components/homematic/cover.py | 10 +- homeassistant/components/homematic/entity.py | 297 ++++++++++ homeassistant/components/homematic/light.py | 5 +- homeassistant/components/homematic/lock.py | 8 +- homeassistant/components/homematic/notify.py | 3 +- homeassistant/components/homematic/sensor.py | 8 +- homeassistant/components/homematic/switch.py | 10 +- 11 files changed, 595 insertions(+), 521 deletions(-) create mode 100644 homeassistant/components/homematic/const.py create mode 100644 homeassistant/components/homematic/entity.py diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 01bc94ce58f..24c9e37a3be 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -1,5 +1,5 @@ """Support for HomeMatic devices.""" -from datetime import datetime, timedelta +from datetime import datetime from functools import partial import logging @@ -18,232 +18,68 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, - STATE_UNKNOWN, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_ADDRESS, + ATTR_CHANNEL, + ATTR_DISCOVER_DEVICES, + ATTR_DISCOVERY_TYPE, + ATTR_ERRORCODE, + ATTR_INTERFACE, + ATTR_LOW_BAT, + ATTR_LOWBAT, + ATTR_MESSAGE, + ATTR_PARAM, + ATTR_PARAMSET, + ATTR_PARAMSET_KEY, + ATTR_TIME, + ATTR_UNIQUE_ID, + ATTR_VALUE, + ATTR_VALUE_TYPE, + CONF_CALLBACK_IP, + CONF_CALLBACK_PORT, + CONF_INTERFACES, + CONF_JSONPORT, + CONF_LOCAL_IP, + CONF_LOCAL_PORT, + CONF_PATH, + CONF_PORT, + CONF_RESOLVENAMES, + CONF_RESOLVENAMES_OPTIONS, + DATA_CONF, + DATA_HOMEMATIC, + DATA_STORE, + DISCOVER_BATTERY, + DISCOVER_BINARY_SENSORS, + DISCOVER_CLIMATE, + DISCOVER_COVER, + DISCOVER_LIGHTS, + DISCOVER_LOCKS, + DISCOVER_SENSORS, + DISCOVER_SWITCHES, + DOMAIN, + EVENT_ERROR, + EVENT_IMPULSE, + EVENT_KEYPRESS, + HM_DEVICE_TYPES, + HM_IGNORE_DISCOVERY_NODE, + HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS, + HM_IMPULSE_EVENTS, + HM_PRESS_EVENTS, + SERVICE_PUT_PARAMSET, + SERVICE_RECONNECT, + SERVICE_SET_DEVICE_VALUE, + SERVICE_SET_INSTALL_MODE, + SERVICE_SET_VARIABLE_VALUE, + SERVICE_VIRTUALKEY, +) +from .entity import HMHub _LOGGER = logging.getLogger(__name__) -DOMAIN = "homematic" - -SCAN_INTERVAL_HUB = timedelta(seconds=300) -SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) - -DISCOVER_SWITCHES = "homematic.switch" -DISCOVER_LIGHTS = "homematic.light" -DISCOVER_SENSORS = "homematic.sensor" -DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" -DISCOVER_COVER = "homematic.cover" -DISCOVER_CLIMATE = "homematic.climate" -DISCOVER_LOCKS = "homematic.locks" -DISCOVER_BATTERY = "homematic.battery" - -ATTR_DISCOVER_DEVICES = "devices" -ATTR_PARAM = "param" -ATTR_CHANNEL = "channel" -ATTR_ADDRESS = "address" -ATTR_VALUE = "value" -ATTR_VALUE_TYPE = "value_type" -ATTR_INTERFACE = "interface" -ATTR_ERRORCODE = "error" -ATTR_MESSAGE = "message" -ATTR_TIME = "time" -ATTR_UNIQUE_ID = "unique_id" -ATTR_PARAMSET_KEY = "paramset_key" -ATTR_PARAMSET = "paramset" -ATTR_DISCOVERY_TYPE = "discovery_type" -ATTR_LOW_BAT = "LOW_BAT" -ATTR_LOWBAT = "LOWBAT" - - -EVENT_KEYPRESS = "homematic.keypress" -EVENT_IMPULSE = "homematic.impulse" -EVENT_ERROR = "homematic.error" - -SERVICE_VIRTUALKEY = "virtualkey" -SERVICE_RECONNECT = "reconnect" -SERVICE_SET_VARIABLE_VALUE = "set_variable_value" -SERVICE_SET_DEVICE_VALUE = "set_device_value" -SERVICE_SET_INSTALL_MODE = "set_install_mode" -SERVICE_PUT_PARAMSET = "put_paramset" - -HM_DEVICE_TYPES = { - DISCOVER_SWITCHES: [ - "Switch", - "SwitchPowermeter", - "IOSwitch", - "IPSwitch", - "RFSiren", - "IPSwitchPowermeter", - "HMWIOSwitch", - "Rain", - "EcoLogic", - "IPKeySwitchPowermeter", - "IPGarage", - "IPKeySwitch", - "IPKeySwitchLevel", - "IPMultiIO", - ], - DISCOVER_LIGHTS: [ - "Dimmer", - "KeyDimmer", - "IPKeyDimmer", - "IPDimmer", - "ColorEffectLight", - "IPKeySwitchLevel", - ], - DISCOVER_SENSORS: [ - "SwitchPowermeter", - "Motion", - "MotionV2", - "RemoteMotion", - "MotionIP", - "ThermostatWall", - "AreaThermostat", - "RotaryHandleSensor", - "WaterSensor", - "PowermeterGas", - "LuxSensor", - "WeatherSensor", - "WeatherStation", - "ThermostatWall2", - "TemperatureDiffSensor", - "TemperatureSensor", - "CO2Sensor", - "IPSwitchPowermeter", - "HMWIOSwitch", - "FillingLevel", - "ValveDrive", - "EcoLogic", - "IPThermostatWall", - "IPSmoke", - "RFSiren", - "PresenceIP", - "IPAreaThermostat", - "IPWeatherSensor", - "RotaryHandleSensorIP", - "IPPassageSensor", - "IPKeySwitchPowermeter", - "IPThermostatWall230V", - "IPWeatherSensorPlus", - "IPWeatherSensorBasic", - "IPBrightnessSensor", - "IPGarage", - "UniversalSensor", - "MotionIPV2", - "IPMultiIO", - "IPThermostatWall2", - ], - DISCOVER_CLIMATE: [ - "Thermostat", - "ThermostatWall", - "MAXThermostat", - "ThermostatWall2", - "MAXWallThermostat", - "IPThermostat", - "IPThermostatWall", - "ThermostatGroup", - "IPThermostatWall230V", - "IPThermostatWall2", - ], - DISCOVER_BINARY_SENSORS: [ - "ShutterContact", - "Smoke", - "SmokeV2", - "Motion", - "MotionV2", - "MotionIP", - "RemoteMotion", - "WeatherSensor", - "TiltSensor", - "IPShutterContact", - "HMWIOSwitch", - "MaxShutterContact", - "Rain", - "WiredSensor", - "PresenceIP", - "IPWeatherSensor", - "IPPassageSensor", - "SmartwareMotion", - "IPWeatherSensorPlus", - "MotionIPV2", - "WaterIP", - "IPMultiIO", - "TiltIP", - "IPShutterContactSabotage", - "IPContact", - ], - DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], - DISCOVER_LOCKS: ["KeyMatic"], -} - -HM_IGNORE_DISCOVERY_NODE = ["ACTUAL_TEMPERATURE", "ACTUAL_HUMIDITY"] - -HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { - "ACTUAL_TEMPERATURE": [ - "IPAreaThermostat", - "IPWeatherSensor", - "IPWeatherSensorPlus", - "IPWeatherSensorBasic", - "IPThermostatWall", - "IPThermostatWall2", - ] -} - -HM_ATTRIBUTE_SUPPORT = { - "LOWBAT": ["battery", {0: "High", 1: "Low"}], - "LOW_BAT": ["battery", {0: "High", 1: "Low"}], - "ERROR": ["error", {0: "No"}], - "ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], - "SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], - "RSSI_PEER": ["rssi_peer", {}], - "RSSI_DEVICE": ["rssi_device", {}], - "VALVE_STATE": ["valve", {}], - "LEVEL": ["level", {}], - "BATTERY_STATE": ["battery", {}], - "CONTROL_MODE": [ - "mode", - {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"}, - ], - "POWER": ["power", {}], - "CURRENT": ["current", {}], - "VOLTAGE": ["voltage", {}], - "OPERATING_VOLTAGE": ["voltage", {}], - "WORKING": ["working", {0: "No", 1: "Yes"}], - "STATE_UNCERTAIN": ["state_uncertain", {}], -} - -HM_PRESS_EVENTS = [ - "PRESS_SHORT", - "PRESS_LONG", - "PRESS_CONT", - "PRESS_LONG_RELEASE", - "PRESS", -] - -HM_IMPULSE_EVENTS = ["SEQUENCE_OK"] - -CONF_RESOLVENAMES_OPTIONS = ["metadata", "json", "xml", False] - -DATA_HOMEMATIC = "homematic" -DATA_STORE = "homematic_store" -DATA_CONF = "homematic_conf" - -CONF_INTERFACES = "interfaces" -CONF_LOCAL_IP = "local_ip" -CONF_LOCAL_PORT = "local_port" -CONF_PORT = "port" -CONF_PATH = "path" -CONF_CALLBACK_IP = "callback_ip" -CONF_CALLBACK_PORT = "callback_port" -CONF_RESOLVENAMES = "resolvenames" -CONF_JSONPORT = "jsonport" -CONF_VARIABLES = "variables" -CONF_DEVICES = "devices" -CONF_PRIMARY = "primary" - DEFAULT_LOCAL_IP = "0.0.0.0" DEFAULT_LOCAL_PORT = 0 DEFAULT_RESOLVENAMES = False @@ -776,277 +612,3 @@ def _device_from_servicecall(hass, service): for devices in hass.data[DATA_HOMEMATIC].devices.values(): if address in devices: return devices[address] - - -class HMHub(Entity): - """The HomeMatic hub. (CCU2/HomeGear).""" - - def __init__(self, hass, homematic, name): - """Initialize HomeMatic hub.""" - self.hass = hass - self.entity_id = "{}.{}".format(DOMAIN, name.lower()) - self._homematic = homematic - self._variables = {} - self._name = name - self._state = None - - # Load data - self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB) - self.hass.add_job(self._update_hub, None) - - self.hass.helpers.event.track_time_interval( - self._update_variables, SCAN_INTERVAL_VARIABLES - ) - self.hass.add_job(self._update_variables, None) - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def should_poll(self): - """Return false. HomeMatic Hub object updates variables.""" - return False - - @property - def state(self): - """Return the state of the entity.""" - return self._state - - @property - def state_attributes(self): - """Return the state attributes.""" - attr = self._variables.copy() - return attr - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:gradient" - - def _update_hub(self, now): - """Retrieve latest state.""" - service_message = self._homematic.getServiceMessages(self._name) - state = None if service_message is None else len(service_message) - - # state have change? - if self._state != state: - self._state = state - self.schedule_update_ha_state() - - def _update_variables(self, now): - """Retrieve all variable data and update hmvariable states.""" - variables = self._homematic.getAllSystemVariables(self._name) - if variables is None: - return - - state_change = False - for key, value in variables.items(): - if key in self._variables and value == self._variables[key]: - continue - - state_change = True - self._variables.update({key: value}) - - if state_change: - self.schedule_update_ha_state() - - def hm_set_variable(self, name, value): - """Set variable value on CCU/Homegear.""" - if name not in self._variables: - _LOGGER.error("Variable %s not found on %s", name, self.name) - return - old_value = self._variables.get(name) - if isinstance(old_value, bool): - value = cv.boolean(value) - else: - value = float(value) - self._homematic.setSystemVariable(self.name, name, value) - - self._variables.update({name: value}) - self.schedule_update_ha_state() - - -class HMDevice(Entity): - """The HomeMatic device base object.""" - - def __init__(self, config): - """Initialize a generic HomeMatic device.""" - self._name = config.get(ATTR_NAME) - self._address = config.get(ATTR_ADDRESS) - self._interface = config.get(ATTR_INTERFACE) - self._channel = config.get(ATTR_CHANNEL) - self._state = config.get(ATTR_PARAM) - self._unique_id = config.get(ATTR_UNIQUE_ID) - self._data = {} - self._homematic = None - self._hmdevice = None - self._connected = False - self._available = False - - # Set parameter to uppercase - if self._state: - self._state = self._state.upper() - - async def async_added_to_hass(self): - """Load data init callbacks.""" - await self.hass.async_add_job(self.link_homematic) - - @property - def unique_id(self): - """Return unique ID. HomeMatic entity IDs are unique by default.""" - return self._unique_id.replace(" ", "_") - - @property - def should_poll(self): - """Return false. HomeMatic states are pushed by the XML-RPC Server.""" - return False - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def available(self): - """Return true if device is available.""" - return self._available - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - attr = {} - - # Generate a dictionary with attributes - for node, data in HM_ATTRIBUTE_SUPPORT.items(): - # Is an attribute and exists for this object - if node in self._data: - value = data[1].get(self._data[node], self._data[node]) - attr[data[0]] = value - - # Static attributes - attr["id"] = self._hmdevice.ADDRESS - attr["interface"] = self._interface - - return attr - - def link_homematic(self): - """Connect to HomeMatic.""" - if self._connected: - return True - - # Initialize - self._homematic = self.hass.data[DATA_HOMEMATIC] - self._hmdevice = self._homematic.devices[self._interface][self._address] - self._connected = True - - try: - # Initialize datapoints of this object - self._init_data() - self._load_data_from_hm() - - # Link events from pyhomematic - self._subscribe_homematic_events() - self._available = not self._hmdevice.UNREACH - except Exception as err: # pylint: disable=broad-except - self._connected = False - _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) - - def _hm_event_callback(self, device, caller, attribute, value): - """Handle all pyhomematic device events.""" - _LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value) - has_changed = False - - # Is data needed for this instance? - if attribute in self._data: - # Did data change? - if self._data[attribute] != value: - self._data[attribute] = value - has_changed = True - - # Availability has changed - if self.available != (not self._hmdevice.UNREACH): - self._available = not self._hmdevice.UNREACH - has_changed = True - - # If it has changed data point, update Home Assistant - if has_changed: - self.schedule_update_ha_state() - - def _subscribe_homematic_events(self): - """Subscribe all required events to handle job.""" - channels_to_sub = set() - - # Push data to channels_to_sub from hmdevice metadata - for metadata in ( - self._hmdevice.SENSORNODE, - self._hmdevice.BINARYNODE, - self._hmdevice.ATTRIBUTENODE, - self._hmdevice.WRITENODE, - self._hmdevice.EVENTNODE, - self._hmdevice.ACTIONNODE, - ): - for node, channels in metadata.items(): - # Data is needed for this instance - if node in self._data: - # chan is current channel - if len(channels) == 1: - channel = channels[0] - else: - channel = self._channel - - # Prepare for subscription - try: - channels_to_sub.add(int(channel)) - except (ValueError, TypeError): - _LOGGER.error("Invalid channel in metadata from %s", self._name) - - # Set callbacks - for channel in channels_to_sub: - _LOGGER.debug("Subscribe channel %d from %s", channel, self._name) - self._hmdevice.setEventCallback( - callback=self._hm_event_callback, bequeath=False, channel=channel - ) - - def _load_data_from_hm(self): - """Load first value from pyhomematic.""" - if not self._connected: - return False - - # Read data from pyhomematic - for metadata, funct in ( - (self._hmdevice.ATTRIBUTENODE, self._hmdevice.getAttributeData), - (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), - (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), - (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData), - ): - for node in metadata: - if metadata[node] and node in self._data: - self._data[node] = funct(name=node, channel=self._channel) - - return True - - def _hm_set_state(self, value): - """Set data to main datapoint.""" - if self._state in self._data: - self._data[self._state] = value - - def _hm_get_state(self): - """Get data from main datapoint.""" - if self._state in self._data: - return self._data[self._state] - return None - - def _init_data(self): - """Generate a data dict (self._data) from the HomeMatic metadata.""" - # Add all attributes to data dictionary - for data_note in self._hmdevice.ATTRIBUTENODE: - self._data.update({data_note: STATE_UNKNOWN}) - - # Initialize device specific data - self._init_data_struct() - - def _init_data_struct(self): - """Generate a data dictionary from the HomeMatic device metadata.""" - raise NotImplementedError diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 1832652406d..731525c8460 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -9,9 +9,9 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_SMOKE, BinarySensorDevice, ) -from homeassistant.components.homematic import ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES, ATTR_DISCOVERY_TYPE, DISCOVER_BATTERY +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -45,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: devices.append(HMBinarySensor(conf)) - add_entities(devices) + add_entities(devices, True) class HMBinarySensor(HMDevice, BinarySensorDevice): diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 935ebb9b497..b4ab277a75b 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -14,7 +14,8 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from . import ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT, HMDevice +from .const import ATTR_DISCOVER_DEVICES, HM_ATTRIBUTE_SUPPORT +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -44,7 +45,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMThermostat(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMThermostat(HMDevice, ClimateDevice): diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py new file mode 100644 index 00000000000..cd2d528044a --- /dev/null +++ b/homeassistant/components/homematic/const.py @@ -0,0 +1,212 @@ +"""Constants for the homematic component.""" + +DOMAIN = "homematic" + +DISCOVER_SWITCHES = "homematic.switch" +DISCOVER_LIGHTS = "homematic.light" +DISCOVER_SENSORS = "homematic.sensor" +DISCOVER_BINARY_SENSORS = "homematic.binary_sensor" +DISCOVER_COVER = "homematic.cover" +DISCOVER_CLIMATE = "homematic.climate" +DISCOVER_LOCKS = "homematic.locks" +DISCOVER_BATTERY = "homematic.battery" + +ATTR_DISCOVER_DEVICES = "devices" +ATTR_PARAM = "param" +ATTR_CHANNEL = "channel" +ATTR_ADDRESS = "address" +ATTR_VALUE = "value" +ATTR_VALUE_TYPE = "value_type" +ATTR_INTERFACE = "interface" +ATTR_ERRORCODE = "error" +ATTR_MESSAGE = "message" +ATTR_TIME = "time" +ATTR_UNIQUE_ID = "unique_id" +ATTR_PARAMSET_KEY = "paramset_key" +ATTR_PARAMSET = "paramset" +ATTR_DISCOVERY_TYPE = "discovery_type" +ATTR_LOW_BAT = "LOW_BAT" +ATTR_LOWBAT = "LOWBAT" + +EVENT_KEYPRESS = "homematic.keypress" +EVENT_IMPULSE = "homematic.impulse" +EVENT_ERROR = "homematic.error" + +SERVICE_VIRTUALKEY = "virtualkey" +SERVICE_RECONNECT = "reconnect" +SERVICE_SET_VARIABLE_VALUE = "set_variable_value" +SERVICE_SET_DEVICE_VALUE = "set_device_value" +SERVICE_SET_INSTALL_MODE = "set_install_mode" +SERVICE_PUT_PARAMSET = "put_paramset" + +HM_DEVICE_TYPES = { + DISCOVER_SWITCHES: [ + "Switch", + "SwitchPowermeter", + "IOSwitch", + "IPSwitch", + "RFSiren", + "IPSwitchPowermeter", + "HMWIOSwitch", + "Rain", + "EcoLogic", + "IPKeySwitchPowermeter", + "IPGarage", + "IPKeySwitch", + "IPKeySwitchLevel", + "IPMultiIO", + ], + DISCOVER_LIGHTS: [ + "Dimmer", + "KeyDimmer", + "IPKeyDimmer", + "IPDimmer", + "ColorEffectLight", + "IPKeySwitchLevel", + ], + DISCOVER_SENSORS: [ + "SwitchPowermeter", + "Motion", + "MotionV2", + "RemoteMotion", + "MotionIP", + "ThermostatWall", + "AreaThermostat", + "RotaryHandleSensor", + "WaterSensor", + "PowermeterGas", + "LuxSensor", + "WeatherSensor", + "WeatherStation", + "ThermostatWall2", + "TemperatureDiffSensor", + "TemperatureSensor", + "CO2Sensor", + "IPSwitchPowermeter", + "HMWIOSwitch", + "FillingLevel", + "ValveDrive", + "EcoLogic", + "IPThermostatWall", + "IPSmoke", + "RFSiren", + "PresenceIP", + "IPAreaThermostat", + "IPWeatherSensor", + "RotaryHandleSensorIP", + "IPPassageSensor", + "IPKeySwitchPowermeter", + "IPThermostatWall230V", + "IPWeatherSensorPlus", + "IPWeatherSensorBasic", + "IPBrightnessSensor", + "IPGarage", + "UniversalSensor", + "MotionIPV2", + "IPMultiIO", + "IPThermostatWall2", + ], + DISCOVER_CLIMATE: [ + "Thermostat", + "ThermostatWall", + "MAXThermostat", + "ThermostatWall2", + "MAXWallThermostat", + "IPThermostat", + "IPThermostatWall", + "ThermostatGroup", + "IPThermostatWall230V", + "IPThermostatWall2", + ], + DISCOVER_BINARY_SENSORS: [ + "ShutterContact", + "Smoke", + "SmokeV2", + "Motion", + "MotionV2", + "MotionIP", + "RemoteMotion", + "WeatherSensor", + "TiltSensor", + "IPShutterContact", + "HMWIOSwitch", + "MaxShutterContact", + "Rain", + "WiredSensor", + "PresenceIP", + "IPWeatherSensor", + "IPPassageSensor", + "SmartwareMotion", + "IPWeatherSensorPlus", + "MotionIPV2", + "WaterIP", + "IPMultiIO", + "TiltIP", + "IPShutterContactSabotage", + "IPContact", + ], + DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], + DISCOVER_LOCKS: ["KeyMatic"], +} + +HM_IGNORE_DISCOVERY_NODE = ["ACTUAL_TEMPERATURE", "ACTUAL_HUMIDITY"] + +HM_IGNORE_DISCOVERY_NODE_EXCEPTIONS = { + "ACTUAL_TEMPERATURE": [ + "IPAreaThermostat", + "IPWeatherSensor", + "IPWeatherSensorPlus", + "IPWeatherSensorBasic", + "IPThermostatWall", + "IPThermostatWall2", + ] +} + +HM_ATTRIBUTE_SUPPORT = { + "LOWBAT": ["battery", {0: "High", 1: "Low"}], + "LOW_BAT": ["battery", {0: "High", 1: "Low"}], + "ERROR": ["error", {0: "No"}], + "ERROR_SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], + "SABOTAGE": ["sabotage", {0: "No", 1: "Yes"}], + "RSSI_PEER": ["rssi_peer", {}], + "RSSI_DEVICE": ["rssi_device", {}], + "VALVE_STATE": ["valve", {}], + "LEVEL": ["level", {}], + "BATTERY_STATE": ["battery", {}], + "CONTROL_MODE": [ + "mode", + {0: "Auto", 1: "Manual", 2: "Away", 3: "Boost", 4: "Comfort", 5: "Lowering"}, + ], + "POWER": ["power", {}], + "CURRENT": ["current", {}], + "VOLTAGE": ["voltage", {}], + "OPERATING_VOLTAGE": ["voltage", {}], + "WORKING": ["working", {0: "No", 1: "Yes"}], + "STATE_UNCERTAIN": ["state_uncertain", {}], +} + +HM_PRESS_EVENTS = [ + "PRESS_SHORT", + "PRESS_LONG", + "PRESS_CONT", + "PRESS_LONG_RELEASE", + "PRESS", +] + +HM_IMPULSE_EVENTS = ["SEQUENCE_OK"] + +CONF_RESOLVENAMES_OPTIONS = ["metadata", "json", "xml", False] + +DATA_HOMEMATIC = "homematic" +DATA_STORE = "homematic_store" +DATA_CONF = "homematic_conf" + +CONF_INTERFACES = "interfaces" +CONF_LOCAL_IP = "local_ip" +CONF_LOCAL_PORT = "local_port" +CONF_PORT = "port" +CONF_PATH = "path" +CONF_CALLBACK_IP = "callback_ip" +CONF_CALLBACK_PORT = "callback_port" +CONF_RESOLVENAMES = "resolvenames" +CONF_JSONPORT = "jsonport" diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index 893b3ce8921..0dea1181d73 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -6,9 +6,9 @@ from homeassistant.components.cover import ( ATTR_TILT_POSITION, CoverDevice, ) -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMCover(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMCover(HMDevice, CoverDevice): @@ -68,9 +68,9 @@ class HMCover(HMDevice, CoverDevice): def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" self._state = "LEVEL" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) if "LEVEL_2" in self._hmdevice.WRITENODE: - self._data.update({"LEVEL_2": STATE_UNKNOWN}) + self._data.update({"LEVEL_2": None}) @property def current_cover_tilt_position(self): diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py new file mode 100644 index 00000000000..4ed893bbf14 --- /dev/null +++ b/homeassistant/components/homematic/entity.py @@ -0,0 +1,297 @@ +"""Homematic base entity.""" +from abc import abstractmethod +from datetime import timedelta +import logging + +from homeassistant.const import ATTR_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_ADDRESS, + ATTR_CHANNEL, + ATTR_INTERFACE, + ATTR_PARAM, + ATTR_UNIQUE_ID, + DATA_HOMEMATIC, + DOMAIN, + HM_ATTRIBUTE_SUPPORT, +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL_HUB = timedelta(seconds=300) +SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) + + +class HMDevice(Entity): + """The HomeMatic device base object.""" + + def __init__(self, config): + """Initialize a generic HomeMatic device.""" + self._name = config.get(ATTR_NAME) + self._address = config.get(ATTR_ADDRESS) + self._interface = config.get(ATTR_INTERFACE) + self._channel = config.get(ATTR_CHANNEL) + self._state = config.get(ATTR_PARAM) + self._unique_id = config.get(ATTR_UNIQUE_ID) + self._data = {} + self._homematic = None + self._hmdevice = None + self._connected = False + self._available = False + + # Set parameter to uppercase + if self._state: + self._state = self._state.upper() + + async def async_added_to_hass(self): + """Load data init callbacks.""" + await self.hass.async_add_job(self._subscribe_homematic_events) + + @property + def unique_id(self): + """Return unique ID. HomeMatic entity IDs are unique by default.""" + return self._unique_id.replace(" ", "_") + + @property + def should_poll(self): + """Return false. HomeMatic states are pushed by the XML-RPC Server.""" + return False + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def available(self): + """Return true if device is available.""" + return self._available + + @property + def device_state_attributes(self): + """Return device specific state attributes.""" + attr = {} + + # Generate a dictionary with attributes + for node, data in HM_ATTRIBUTE_SUPPORT.items(): + # Is an attribute and exists for this object + if node in self._data: + value = data[1].get(self._data[node], self._data[node]) + attr[data[0]] = value + + # Static attributes + attr["id"] = self._hmdevice.ADDRESS + attr["interface"] = self._interface + + return attr + + def update(self): + """Connect to HomeMatic init values.""" + if self._connected: + return True + + # Initialize + self._homematic = self.hass.data[DATA_HOMEMATIC] + self._hmdevice = self._homematic.devices[self._interface][self._address] + self._connected = True + + try: + # Initialize datapoints of this object + self._init_data() + self._load_data_from_hm() + + # Link events from pyhomematic + self._available = not self._hmdevice.UNREACH + except Exception as err: # pylint: disable=broad-except + self._connected = False + _LOGGER.error("Exception while linking %s: %s", self._address, str(err)) + + def _hm_event_callback(self, device, caller, attribute, value): + """Handle all pyhomematic device events.""" + _LOGGER.debug("%s received event '%s' value: %s", self._name, attribute, value) + has_changed = False + + # Is data needed for this instance? + if attribute in self._data: + # Did data change? + if self._data[attribute] != value: + self._data[attribute] = value + has_changed = True + + # Availability has changed + if self.available != (not self._hmdevice.UNREACH): + self._available = not self._hmdevice.UNREACH + has_changed = True + + # If it has changed data point, update Home Assistant + if has_changed: + self.schedule_update_ha_state() + + def _subscribe_homematic_events(self): + """Subscribe all required events to handle job.""" + channels_to_sub = set() + + # Push data to channels_to_sub from hmdevice metadata + for metadata in ( + self._hmdevice.SENSORNODE, + self._hmdevice.BINARYNODE, + self._hmdevice.ATTRIBUTENODE, + self._hmdevice.WRITENODE, + self._hmdevice.EVENTNODE, + self._hmdevice.ACTIONNODE, + ): + for node, channels in metadata.items(): + # Data is needed for this instance + if node in self._data: + # chan is current channel + if len(channels) == 1: + channel = channels[0] + else: + channel = self._channel + + # Prepare for subscription + try: + channels_to_sub.add(int(channel)) + except (ValueError, TypeError): + _LOGGER.error("Invalid channel in metadata from %s", self._name) + + # Set callbacks + for channel in channels_to_sub: + _LOGGER.debug("Subscribe channel %d from %s", channel, self._name) + self._hmdevice.setEventCallback( + callback=self._hm_event_callback, bequeath=False, channel=channel + ) + + def _load_data_from_hm(self): + """Load first value from pyhomematic.""" + if not self._connected: + return False + + # Read data from pyhomematic + for metadata, funct in ( + (self._hmdevice.ATTRIBUTENODE, self._hmdevice.getAttributeData), + (self._hmdevice.WRITENODE, self._hmdevice.getWriteData), + (self._hmdevice.SENSORNODE, self._hmdevice.getSensorData), + (self._hmdevice.BINARYNODE, self._hmdevice.getBinaryData), + ): + for node in metadata: + if metadata[node] and node in self._data: + self._data[node] = funct(name=node, channel=self._channel) + + return True + + def _hm_set_state(self, value): + """Set data to main datapoint.""" + if self._state in self._data: + self._data[self._state] = value + + def _hm_get_state(self): + """Get data from main datapoint.""" + if self._state in self._data: + return self._data[self._state] + return None + + def _init_data(self): + """Generate a data dict (self._data) from the HomeMatic metadata.""" + # Add all attributes to data dictionary + for data_note in self._hmdevice.ATTRIBUTENODE: + self._data.update({data_note: None}) + + # Initialize device specific data + self._init_data_struct() + + @abstractmethod + def _init_data_struct(self): + """Generate a data dictionary from the HomeMatic device metadata.""" + + +class HMHub(Entity): + """The HomeMatic hub. (CCU2/HomeGear).""" + + def __init__(self, hass, homematic, name): + """Initialize HomeMatic hub.""" + self.hass = hass + self.entity_id = "{}.{}".format(DOMAIN, name.lower()) + self._homematic = homematic + self._variables = {} + self._name = name + self._state = None + + # Load data + self.hass.helpers.event.track_time_interval(self._update_hub, SCAN_INTERVAL_HUB) + self.hass.add_job(self._update_hub, None) + + self.hass.helpers.event.track_time_interval( + self._update_variables, SCAN_INTERVAL_VARIABLES + ) + self.hass.add_job(self._update_variables, None) + + @property + def name(self): + """Return the name of the device.""" + return self._name + + @property + def should_poll(self): + """Return false. HomeMatic Hub object updates variables.""" + return False + + @property + def state(self): + """Return the state of the entity.""" + return self._state + + @property + def state_attributes(self): + """Return the state attributes.""" + attr = self._variables.copy() + return attr + + @property + def icon(self): + """Return the icon to use in the frontend, if any.""" + return "mdi:gradient" + + def _update_hub(self, now): + """Retrieve latest state.""" + service_message = self._homematic.getServiceMessages(self._name) + state = None if service_message is None else len(service_message) + + # state have change? + if self._state != state: + self._state = state + self.schedule_update_ha_state() + + def _update_variables(self, now): + """Retrieve all variable data and update hmvariable states.""" + variables = self._homematic.getAllSystemVariables(self._name) + if variables is None: + return + + state_change = False + for key, value in variables.items(): + if key in self._variables and value == self._variables[key]: + continue + + state_change = True + self._variables.update({key: value}) + + if state_change: + self.schedule_update_ha_state() + + def hm_set_variable(self, name, value): + """Set variable value on CCU/Homegear.""" + if name not in self._variables: + _LOGGER.error("Variable %s not found on %s", name, self.name) + return + old_value = self._variables.get(name) + if isinstance(old_value, bool): + value = cv.boolean(value) + else: + value = float(value) + self._homematic.setSystemVariable(self.name, name, value) + + self._variables.update({name: value}) + self.schedule_update_ha_state() diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 29992bccef3..52b2f9a7996 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -12,7 +12,8 @@ from homeassistant.components.light import ( Light, ) -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -29,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMLight(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMLight(HMDevice, Light): diff --git a/homeassistant/components/homematic/lock.py b/homeassistant/components/homematic/lock.py index 7f796b32885..0094ecd2e81 100644 --- a/homeassistant/components/homematic/lock.py +++ b/homeassistant/components/homematic/lock.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.lock import SUPPORT_OPEN, LockDevice -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -18,7 +18,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for conf in discovery_info[ATTR_DISCOVER_DEVICES]: devices.append(HMLock(conf)) - add_entities(devices) + add_entities(devices, True) class HMLock(HMDevice, LockDevice): @@ -44,7 +44,7 @@ class HMLock(HMDevice, LockDevice): def _init_data_struct(self): """Generate the data dictionary (self._data) from metadata.""" self._state = "STATE" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) @property def supported_features(self): diff --git a/homeassistant/components/homematic/notify.py b/homeassistant/components/homematic/notify.py index 9fd94b9832c..3d48adc6df2 100644 --- a/homeassistant/components/homematic/notify.py +++ b/homeassistant/components/homematic/notify.py @@ -11,7 +11,7 @@ from homeassistant.components.notify import ( import homeassistant.helpers.config_validation as cv import homeassistant.helpers.template as template_helper -from . import ( +from .const import ( ATTR_ADDRESS, ATTR_CHANNEL, ATTR_INTERFACE, @@ -22,6 +22,7 @@ from . import ( ) _LOGGER = logging.getLogger(__name__) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(ATTR_ADDRESS): vol.All(cv.string, vol.Upper), diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 10c402a0dd4..bba8325650d 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -8,10 +8,10 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ENERGY_WATT_HOUR, POWER_WATT, - STATE_UNKNOWN, ) -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -82,7 +82,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMSensor(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMSensor(HMDevice): @@ -117,6 +117,6 @@ class HMSensor(HMDevice): def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" if self._state: - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) else: _LOGGER.critical("Unable to initialize sensor: %s", self._name) diff --git a/homeassistant/components/homematic/switch.py b/homeassistant/components/homematic/switch.py index b77b3a1f700..53679818083 100644 --- a/homeassistant/components/homematic/switch.py +++ b/homeassistant/components/homematic/switch.py @@ -2,9 +2,9 @@ import logging from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_UNKNOWN -from . import ATTR_DISCOVER_DEVICES, HMDevice +from .const import ATTR_DISCOVER_DEVICES +from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -19,7 +19,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): new_device = HMSwitch(conf) devices.append(new_device) - add_entities(devices) + add_entities(devices, True) class HMSwitch(HMDevice, SwitchDevice): @@ -55,8 +55,8 @@ class HMSwitch(HMDevice, SwitchDevice): def _init_data_struct(self): """Generate the data dictionary (self._data) from metadata.""" self._state = "STATE" - self._data.update({self._state: STATE_UNKNOWN}) + self._data.update({self._state: None}) # Need sensor values for SwitchPowermeter for node in self._hmdevice.SENSORNODE: - self._data.update({node: STATE_UNKNOWN}) + self._data.update({node: None})