From db09ef0a6f2348d71c2afbd4643b813be96a72ab Mon Sep 17 00:00:00 2001 From: David-Leon Pohl Date: Sun, 29 Jan 2017 02:49:16 +0100 Subject: [PATCH 01/13] Fixes: Pilight Switch rejects alphanumeric IDs #5119 (#5601) --- homeassistant/components/switch/pilight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/switch/pilight.py b/homeassistant/components/switch/pilight.py index 40c459dc189..b56367e80be 100644 --- a/homeassistant/components/switch/pilight.py +++ b/homeassistant/components/switch/pilight.py @@ -33,7 +33,7 @@ COMMAND_SCHEMA = vol.Schema({ vol.Optional('off'): cv.positive_int, vol.Optional(CONF_UNIT): cv.positive_int, vol.Optional(CONF_UNITCODE): cv.positive_int, - vol.Optional(CONF_ID): cv.positive_int, + vol.Optional(CONF_ID): vol.Any(cv.positive_int, cv.string), vol.Optional(CONF_STATE): cv.string, vol.Optional(CONF_SYSTEMCODE): cv.positive_int, }, extra=vol.ALLOW_EXTRA) From a168bf64b6c252a881d16648f226359996dafbbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Mon, 30 Jan 2017 18:20:35 +0100 Subject: [PATCH 02/13] bug fix in hue (#5623) --- homeassistant/components/light/hue.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index 3e1e81b05ea..a934788f36b 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -59,11 +59,9 @@ DEFAULT_ALLOW_HUE_GROUPS = True PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_ALLOW_UNREACHABLE, - default=DEFAULT_ALLOW_UNREACHABLE): cv.boolean, - vol.Optional(CONF_FILENAME, default=PHUE_CONFIG_FILE): cv.string, - vol.Optional(CONF_ALLOW_IN_EMULATED_HUE, - default=DEFAULT_ALLOW_IN_EMULATED_HUE): cv.boolean, + vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean, + vol.Optional(CONF_FILENAME): cv.string, + vol.Optional(CONF_ALLOW_IN_EMULATED_HUE): cv.boolean, vol.Optional(CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS): cv.boolean, }) @@ -98,9 +96,11 @@ def _find_host_from_config(hass, filename=PHUE_CONFIG_FILE): def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the Hue lights.""" # Default needed in case of discovery - filename = config.get(CONF_FILENAME) - allow_unreachable = config.get(CONF_ALLOW_UNREACHABLE) - allow_in_emulated_hue = config.get(CONF_ALLOW_IN_EMULATED_HUE) + filename = config.get(CONF_FILENAME, PHUE_CONFIG_FILE) + allow_unreachable = config.get(CONF_ALLOW_UNREACHABLE, + DEFAULT_ALLOW_UNREACHABLE) + allow_in_emulated_hue = config.get(CONF_ALLOW_IN_EMULATED_HUE, + DEFAULT_ALLOW_IN_EMULATED_HUE) allow_hue_groups = config.get(CONF_ALLOW_HUE_GROUPS) if discovery_info is not None: From 26d0fd772be3b15423371a7b08c117f707cb8d33 Mon Sep 17 00:00:00 2001 From: Marcelo Moreira de Mello Date: Tue, 31 Jan 2017 03:19:49 -0500 Subject: [PATCH 03/13] Fixes issue #5627 by bumping external Amcrest module to version 1.1.4 (#5662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an optional extended description… --- homeassistant/components/camera/amcrest.py | 2 +- homeassistant/components/sensor/amcrest.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/camera/amcrest.py b/homeassistant/components/camera/amcrest.py index ecc93dfaaeb..f272dda4bb3 100644 --- a/homeassistant/components/camera/amcrest.py +++ b/homeassistant/components/camera/amcrest.py @@ -18,7 +18,7 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import ( async_get_clientsession, async_aiohttp_proxy_stream) -REQUIREMENTS = ['amcrest==1.1.3'] +REQUIREMENTS = ['amcrest==1.1.4'] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sensor/amcrest.py b/homeassistant/components/sensor/amcrest.py index 44fdeca54f1..08d551b8fde 100644 --- a/homeassistant/components/sensor/amcrest.py +++ b/homeassistant/components/sensor/amcrest.py @@ -20,7 +20,7 @@ import homeassistant.loader as loader from requests.exceptions import HTTPError, ConnectTimeout -REQUIREMENTS = ['amcrest==1.1.3'] +REQUIREMENTS = ['amcrest==1.1.4'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 2e983ea0f54..e8ca48f3fa3 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -39,7 +39,7 @@ aiohttp_cors==0.5.0 # homeassistant.components.camera.amcrest # homeassistant.components.sensor.amcrest -amcrest==1.1.3 +amcrest==1.1.4 # homeassistant.components.media_player.anthemav anthemav==1.1.8 From 27a91b357e4bf91462fe6457a5c03fe8fdc0a646 Mon Sep 17 00:00:00 2001 From: Erik Eriksson Date: Tue, 31 Jan 2017 20:08:11 +0100 Subject: [PATCH 04/13] Upgraded tellduslive (#5664) --- homeassistant/components/sensor/tellduslive.py | 2 +- homeassistant/components/tellduslive.py | 13 ++++++------- requirements_all.txt | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/tellduslive.py b/homeassistant/components/sensor/tellduslive.py index abc5843ad91..b7f3cf60892 100644 --- a/homeassistant/components/sensor/tellduslive.py +++ b/homeassistant/components/sensor/tellduslive.py @@ -58,7 +58,7 @@ class TelldusLiveSensor(TelldusLiveEntity): @property def _value(self): """Return value of the sensor.""" - return self.device.value(self._id[1:]) + return self.device.value(*self._id[1:]) @property def _value_as_temperature(self): diff --git a/homeassistant/components/tellduslive.py b/homeassistant/components/tellduslive.py index b470ae7daec..259d4e1becb 100644 --- a/homeassistant/components/tellduslive.py +++ b/homeassistant/components/tellduslive.py @@ -17,7 +17,7 @@ import voluptuous as vol DOMAIN = 'tellduslive' -REQUIREMENTS = ['tellduslive==0.1.13'] +REQUIREMENTS = ['tellduslive==0.3.0'] _LOGGER = logging.getLogger(__name__) @@ -133,8 +133,8 @@ class TelldusLiveClient(object): if device.device_id in known_ids: continue if device.is_sensor: - for item_id in device.items: - discover((device.device_id,) + item_id, + for item in device.items: + discover((device.device_id, item.name, item.scale), 'sensor') else: discover(device.device_id, @@ -145,8 +145,7 @@ class TelldusLiveClient(object): def device(self, device_id): """Return device representation.""" - import tellduslive - return tellduslive.Device(self._client, device_id) + return self._client.device(device_id) def is_available(self, device_id): """Return device availability.""" @@ -221,5 +220,5 @@ class TelldusLiveEntity(Entity): @property def _last_updated(self): """Return the last update of a device.""" - return str(datetime.fromtimestamp(self.device.last_updated)) \ - if self.device.last_updated else None + return str(datetime.fromtimestamp(self.device.lastUpdated)) \ + if self.device.lastUpdated else None diff --git a/requirements_all.txt b/requirements_all.txt index e8ca48f3fa3..84ab6be1bc1 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -620,7 +620,7 @@ steamodd==4.21 tellcore-py==1.1.2 # homeassistant.components.tellduslive -tellduslive==0.1.13 +tellduslive==0.3.0 # homeassistant.components.sensor.temper temperusb==1.5.1 From bc7fd5611e044915951cdd5e6f667331b4f356fa Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 1 Feb 2017 16:53:02 +0100 Subject: [PATCH 05/13] Bugfix sonos group coordinator (#5691) * Bugfix sonos group coordinator * Fix tests --- homeassistant/components/media_player/sonos.py | 18 ++++++++++-------- tests/components/media_player/test_sonos.py | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 38408914448..14b32260ca0 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -342,18 +342,20 @@ class SonosDevice(MediaPlayerDevice): if is_available: - if self._player.group.coordinator != self._player: + # set group coordinator + if self._player.is_coordinator: + self._coordinator = None + else: try: self._coordinator = _get_entity_from_soco( self.hass, self._player.group.coordinator) + + # protect for loop + if not self._coordinator.is_coordinator: + # pylint: disable=protected-access + self._coordinator._coordinator = None except ValueError: self._coordinator = None - else: - self._coordinator = None - - if self._coordinator == self: - _LOGGER.warning("Coordinator loop on: %s", self.unique_id) - self._coordinator = None track_info = None if self._last_avtransport_event: @@ -957,7 +959,7 @@ class SonosDevice(MediaPlayerDevice): try: # need catch exception if a coordinator is going to slave. # this state will recover with group part. - self.soco_snapshot.restore(True) + self.soco_snapshot.restore(False) except (TypeError, SoCoException): _LOGGER.debug("Error on restore %s", self.entity_id) diff --git a/tests/components/media_player/test_sonos.py b/tests/components/media_player/test_sonos.py index 3d80536fb82..bcbee544f81 100644 --- a/tests/components/media_player/test_sonos.py +++ b/tests/components/media_player/test_sonos.py @@ -314,4 +314,4 @@ class TestSonosMediaPlayer(unittest.TestCase): device._snapshot_coordinator.soco_device = SoCoMock('192.0.2.17') device.restore() self.assertEqual(restoreMock.call_count, 1) - self.assertEqual(restoreMock.call_args, mock.call(True)) + self.assertEqual(restoreMock.call_args, mock.call(False)) From 6ee7878236f2080cd63f76d082e8288952f3d0da Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 1 Feb 2017 17:20:52 +0100 Subject: [PATCH 06/13] Bugfix async blocking loop with xml parser. (#5694) --- homeassistant/components/device_tracker/upc_connect.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/device_tracker/upc_connect.py b/homeassistant/components/device_tracker/upc_connect.py index a8d39baed57..ff526ec7e8a 100644 --- a/homeassistant/components/device_tracker/upc_connect.py +++ b/homeassistant/components/device_tracker/upc_connect.py @@ -92,7 +92,8 @@ class UPCDeviceScanner(DeviceScanner): raw = yield from self._async_ws_function(CMD_DEVICES) try: - xml_root = ET.fromstring(raw) + xml_root = yield from self.hass.loop.run_in_executor( + None, ET.fromstring, raw) return [mac.text for mac in xml_root.iter('MACAddr')] except (ET.ParseError, TypeError): _LOGGER.warning("Can't read device from %s", self.host) From 9716cd3f48d803e1ae37a31c7cb4ae553b7889f2 Mon Sep 17 00:00:00 2001 From: Hugo Dupras Date: Wed, 1 Feb 2017 17:33:32 +0100 Subject: [PATCH 07/13] Hotfix for netatmo cameras (#5644) * Fix for missing netatmo tags in 0.37 Also fix issue with SSL certificate for vpn_url Signed-off-by: Hugo D. (jabesq) * Netatmo welcome: vpn_url can be empty Signed-off-by: Hugo D. (jabesq) * add config floag to disable SSL verification for vpn_url Signed-off-by: Hugo D. (jabesq) * Import CONF_VERIFY_SSL from const --- .../components/binary_sensor/netatmo.py | 30 +++++++++---------- homeassistant/components/camera/netatmo.py | 21 +++++++++---- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index 4ef29b9e5f5..661d1128086 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -26,8 +26,6 @@ WELCOME_SENSOR_TYPES = { "Someone known": "motion", "Someone unknown": "motion", "Motion": "motion", - "Tag Vibration": 'vibration', - "Tag Open": 'opening' } PRESENCE_SENSOR_TYPES = { "Outdoor motion": "motion", @@ -35,11 +33,16 @@ PRESENCE_SENSOR_TYPES = { "Outdoor animal": "motion", "Outdoor vehicle": "motion" } +TAG_SENSOR_TYPES = { + "Tag Vibration": 'vibration', + "Tag Open": 'opening' +} CONF_HOME = 'home' CONF_CAMERAS = 'cameras' CONF_WELCOME_SENSORS = 'welcome_sensors' CONF_PRESENCE_SENSORS = 'presence_sensors' +CONF_TAG_SENSORS = 'tag_sensors' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOME): cv.string, @@ -78,6 +81,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): CONF_WELCOME_SENSORS, WELCOME_SENSOR_TYPES) presence_sensors = config.get( CONF_PRESENCE_SENSORS, PRESENCE_SENSOR_TYPES) + tag_sensors = config.get(CONF_TAG_SENSORS, TAG_SENSOR_TYPES) for camera_name in data.get_camera_names(): camera_type = data.get_camera_type(camera=camera_name, home=home) @@ -103,13 +107,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): variable)]) for module_name in data.get_module_names(camera_name): - for variable in welcome_sensors: - if variable in ('Tag Vibration', 'Tag Open'): - add_devices([NetatmoBinarySensor(data, camera_name, - module_name, home, - timeout, offset, - camera_type, - variable)]) + for variable in tag_sensors: + camera_type = None + add_devices([NetatmoBinarySensor(data, camera_name, + module_name, home, + timeout, offset, + camera_type, + variable)]) class NetatmoBinarySensor(BinarySensorDevice): @@ -157,7 +161,7 @@ class NetatmoBinarySensor(BinarySensorDevice): elif self._cameratype == "NOC": return PRESENCE_SENSOR_TYPES.get(self._sensor_name) else: - return None + return TAG_SENSOR_TYPES.get(self._sensor_name) @property def is_on(self): @@ -184,8 +188,6 @@ class NetatmoBinarySensor(BinarySensorDevice): self._data.camera_data.motionDetected(self._home, self._camera_name, self._timeout*60) - else: - return None elif self._cameratype == "NOC": if self._sensor_name == "Outdoor motion": self._state =\ @@ -206,9 +208,7 @@ class NetatmoBinarySensor(BinarySensorDevice): self._data.camera_data.carDetected(self._home, self._camera_name, self._offset) - else: - return None - elif self._sensor_name == "Tag Vibration": + if self._sensor_name == "Tag Vibration": self._state =\ self._data.camera_data.moduleMotionDetected(self._home, self._module_name, diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index 563de206dea..6ede7c5a162 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -9,6 +9,7 @@ import logging import requests import voluptuous as vol +from homeassistant.const import CONF_VERIFY_SSL from homeassistant.components.netatmo import CameraData from homeassistant.components.camera import (Camera, PLATFORM_SCHEMA) from homeassistant.loader import get_component @@ -22,6 +23,7 @@ CONF_HOME = 'home' CONF_CAMERAS = 'cameras' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, vol.Optional(CONF_HOME): cv.string, vol.Optional(CONF_CAMERAS, default=[]): vol.All(cv.ensure_list, [cv.string]), @@ -33,6 +35,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Setup access to Netatmo cameras.""" netatmo = get_component('netatmo') home = config.get(CONF_HOME) + verify_ssl = config.get(CONF_VERIFY_SSL, True) import lnetatmo try: data = CameraData(netatmo.NETATMO_AUTH, home) @@ -42,7 +45,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if config[CONF_CAMERAS] != [] and \ camera_name not in config[CONF_CAMERAS]: continue - add_devices([NetatmoCamera(data, camera_name, home, camera_type)]) + add_devices([NetatmoCamera(data, camera_name, home, + camera_type, verify_ssl)]) except lnetatmo.NoDevice: return None @@ -50,11 +54,12 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class NetatmoCamera(Camera): """Representation of the images published from a Netatmo camera.""" - def __init__(self, data, camera_name, home, camera_type): + def __init__(self, data, camera_name, home, camera_type, verify_ssl): """Setup for access to the Netatmo camera images.""" super(NetatmoCamera, self).__init__() self._data = data self._camera_name = camera_name + self._verify_ssl = verify_ssl if home: self._name = home + ' / ' + camera_name else: @@ -74,11 +79,17 @@ class NetatmoCamera(Camera): if self._localurl: response = requests.get('{0}/live/snapshot_720.jpg'.format( self._localurl), timeout=10) - else: + elif self._vpnurl: response = requests.get('{0}/live/snapshot_720.jpg'.format( - self._vpnurl), timeout=10) + self._vpnurl), timeout=10, verify=self._verify_ssl) + else: + _LOGGER.error('Welcome VPN url is None') + self._data.update() + (self._vpnurl, self._localurl) = \ + self._data.camera_data.cameraUrls(camera=self._camera_name) + return None except requests.exceptions.RequestException as error: - _LOGGER.error('Welcome VPN url changed: %s', error) + _LOGGER.error('Welcome url changed: %s', error) self._data.update() (self._vpnurl, self._localurl) = \ self._data.camera_data.cameraUrls(camera=self._camera_name) From df7ca226567ac369db1e490146b47fd5cad100de Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 1 Feb 2017 23:55:16 +0100 Subject: [PATCH 08/13] Fix bug for UNREACH devices / Variable handling and update. (#5689) * Fix bug for UNREACH devices / Variable handling and update. * fix track_time * update after data after creation * add message output * change unreach * change unreach code * Revert "change unreach code" This reverts commit f58430de3cb854d5f6f886bfe4d59899c1efadbf. * update pyhomematic --- homeassistant/components/homematic.py | 201 ++++++++++--------------- homeassistant/components/services.yaml | 8 +- requirements_all.txt | 2 +- 3 files changed, 88 insertions(+), 123 deletions(-) diff --git a/homeassistant/components/homematic.py b/homeassistant/components/homematic.py index 9b38bb0efea..ce1df98d727 100644 --- a/homeassistant/components/homematic.py +++ b/homeassistant/components/homematic.py @@ -16,17 +16,16 @@ import homeassistant.helpers.config_validation as cv from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, CONF_USERNAME, CONF_PASSWORD, CONF_PLATFORM, CONF_HOSTS, CONF_NAME, ATTR_ENTITY_ID) -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers import discovery +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval from homeassistant.config import load_yaml_config_file -from homeassistant.util import Throttle DOMAIN = 'homematic' -REQUIREMENTS = ["pyhomematic==0.1.20"] +REQUIREMENTS = ["pyhomematic==0.1.21"] -MIN_TIME_BETWEEN_UPDATE_HUB = timedelta(seconds=300) -SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL_HUB = timedelta(seconds=300) +SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) DISCOVER_SWITCHES = 'homematic.switch' DISCOVER_LIGHTS = 'homematic.light' @@ -176,8 +175,9 @@ SCHEMA_SERVICE_VIRTUALKEY = vol.Schema({ }) SCHEMA_SERVICE_SET_VAR_VALUE = vol.Schema({ - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_VALUE): cv.match_all, + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, }) SCHEMA_SERVICE_SET_DEV_VALUE = vol.Schema({ @@ -236,8 +236,6 @@ def setup(hass, config): """Setup the Homematic component.""" from pyhomematic import HMConnection - component = EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) - hass.data[DATA_DELAY] = config[DOMAIN].get(CONF_DELAY) hass.data[DATA_DEVINIT] = {} hass.data[DATA_STORE] = [] @@ -281,11 +279,10 @@ def setup(hass, config): hass.config.components.append(DOMAIN) # init homematic hubs - hub_entities = [] + entity_hubs = [] for _, hub_data in hosts.items(): - hub_entities.append(HMHub(hass, component, hub_data[CONF_NAME], - hub_data[CONF_VARIABLES])) - component.add_entities(hub_entities) + entity_hubs.append(HMHub( + hass, hub_data[CONF_NAME], hub_data[CONF_VARIABLES])) # regeister homematic services descriptions = load_yaml_config_file( @@ -323,14 +320,23 @@ def setup(hass, config): schema=SCHEMA_SERVICE_VIRTUALKEY) def _service_handle_value(service): - """Set value on homematic variable object.""" - variable_list = component.extract_from_service(service) - + """Set value on homematic variable.""" + entity_ids = service.data.get(ATTR_ENTITY_ID) + name = service.data[ATTR_NAME] value = service.data[ATTR_VALUE] - for hm_variable in variable_list: - if isinstance(hm_variable, HMVariable): - hm_variable.hm_set(value) + if entity_ids: + entities = [entity for entity in entity_hubs if + entity.entity_id in entity_ids] + else: + entities = entity_hubs + + if not entities: + _LOGGER.error("Homematic controller not found!") + return + + for hub in entities: + hub.hm_set_variable(name, value) hass.services.register( DOMAIN, SERVICE_SET_VAR_VALUE, _service_handle_value, @@ -579,132 +585,87 @@ def _device_from_servicecall(hass, service): class HMHub(Entity): """The Homematic hub. I.e. CCU2/HomeGear.""" - def __init__(self, hass, component, name, use_variables): + def __init__(self, hass, name, use_variables): """Initialize Homematic hub.""" self.hass = hass + self.entity_id = "{}.{}".format(DOMAIN, name.lower()) self._homematic = hass.data[DATA_HOMEMATIC] - self._component = component + self._variables = {} self._name = name self._state = STATE_UNKNOWN - self._store = {} self._use_variables = use_variables # load data - self._update_hub_state() - self._init_variables() + track_time_interval(hass, self._update_hub, SCAN_INTERVAL_HUB) + self._update_hub(None) + + if self._use_variables: + track_time_interval( + hass, self._update_variables, SCAN_INTERVAL_VARIABLES) + self._update_variables(None) @property def name(self): """Return the name of the device.""" return self._name - @property - def state(self): - """Return the state of the entity.""" - return self._state - - @property - def device_state_attributes(self): - """Return device specific state attributes.""" - return {} - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:gradient" - - def update(self): - """Update Hub data and all HM variables.""" - self._update_hub_state() - self._update_variables_state() - - @Throttle(MIN_TIME_BETWEEN_UPDATE_HUB) - def _update_hub_state(self): - """Retrieve latest state.""" - state = self._homematic.getServiceMessages(self._name) - self._state = STATE_UNKNOWN if state is None else len(state) - - def _update_variables_state(self): - """Retrive all variable data and update hmvariable states.""" - if not self._use_variables: - return - - variables = self._homematic.getAllSystemVariables(self._name) - if variables is None: - return - - for key, value in variables.items(): - if key in self._store: - self._store.get(key).hm_update(value) - - def _init_variables(self): - """Load variables from hub.""" - if not self._use_variables: - return - - variables = self._homematic.getAllSystemVariables(self._name) - if variables is None: - return - - entities = [] - for key, value in variables.items(): - entities.append(HMVariable(self.hass, self._name, key, value)) - self._component.add_entities(entities) - - -class HMVariable(Entity): - """The Homematic system variable.""" - - def __init__(self, hass, hub_name, name, state): - """Initialize Homematic hub.""" - self.hass = hass - self._homematic = hass.data[DATA_HOMEMATIC] - self._state = state - self._name = name - self._hub_name = hub_name - - @property - def name(self): - """Return the name of the device.""" - return self._name - - @property - def state(self): - """Return the state of the entity.""" - return self._state - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return "mdi:code-string" - @property def should_poll(self): """Return false. Homematic Hub object update variable.""" return False @property - def device_state_attributes(self): - """Return device specific state attributes.""" - attr = { - 'hub': self._hub_name, - } + 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 - def hm_update(self, value): - """Update variable over Hub object.""" - if value != self._state: - self._state = value + @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.""" + state = self._homematic.getServiceMessages(self._name) + self._state = STATE_UNKNOWN if state is None else len(state) + self.schedule_update_ha_state() + + def _update_variables(self, now): + """Retrive 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(self, value): + def hm_set_variable(self, name, value): """Set variable on homematic controller.""" - if isinstance(self._state, bool): + 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._hub_name, self._name, value) - self._state = value + self._homematic.setSystemVariable(self.name, name, value) + + self._variables.update({name: value}) self.schedule_update_ha_state() @@ -817,7 +778,7 @@ class HMDevice(Entity): have_change = True # If available it has changed - if attribute is 'UNREACH': + if attribute == 'UNREACH': self._available = bool(value) have_change = True @@ -829,7 +790,7 @@ class HMDevice(Entity): def _subscribe_homematic_events(self): """Subscribe all required events to handle job.""" - channels_to_sub = {} + channels_to_sub = {0: True} # add channel 0 for UNREACH # Push data to channels_to_sub from hmdevice metadata for metadata in (self._hmdevice.SENSORNODE, self._hmdevice.BINARYNODE, @@ -856,7 +817,7 @@ class HMDevice(Entity): # Set callbacks for channel in channels_to_sub: _LOGGER.debug( - "Subscribe channel %s from %s", str(channel), self._name) + "Subscribe channel %d from %s", channel, self._name) self._hmdevice.setEventCallback( callback=self._hm_event_callback, bequeath=False, channel=channel) diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml index c390f65f5a0..9ab64c89571 100644 --- a/homeassistant/components/services.yaml +++ b/homeassistant/components/services.yaml @@ -99,8 +99,12 @@ homematic: fields: entity_id: - description: Name(s) of entities to set value - example: 'homematic.my_variable' + description: Name(s) of homematic central to set value + example: 'homematic.ccu2' + + name: + description: Name of the varaible to set + example: 'testvariable' value: description: New value diff --git a/requirements_all.txt b/requirements_all.txt index 84ab6be1bc1..f1eadf1a51d 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -444,7 +444,7 @@ pyharmony==1.0.12 pyhik==0.0.7 # homeassistant.components.homematic -pyhomematic==0.1.20 +pyhomematic==0.1.21 # homeassistant.components.device_tracker.icloud pyicloud==0.9.1 From 9ed8ee126132fd194a81e12d98e5f36f21ddfa92 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 2 Feb 2017 00:02:24 +0100 Subject: [PATCH 09/13] Bugfix sonso source input (#5699) --- homeassistant/components/media_player/sonos.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index 14b32260ca0..0bb34eee598 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -523,10 +523,6 @@ class SonosDevice(MediaPlayerDevice): update_media_position |= rel_time is not None and \ self._media_position is None - # used only if a media is playing - if self.state != STATE_PLAYING: - update_media_position = None - # position changed? if rel_time is not None and \ self._media_position is not None: @@ -541,7 +537,7 @@ class SonosDevice(MediaPlayerDevice): update_media_position = \ abs(calculated_position - rel_time) > 1.5 - if update_media_position: + if update_media_position and self.state == STATE_PLAYING: media_position = rel_time media_position_updated_at = utcnow() else: @@ -830,7 +826,7 @@ class SonosDevice(MediaPlayerDevice): """List of available input sources.""" model_name = self._speaker_info['model_name'] - sources = self._favorite_sources + sources = self._favorite_sources.copy() if 'PLAY:5' in model_name: sources += [SUPPORT_SOURCE_LINEIN] From 17c4f4d3914a90ec9270bb5688052ecfa6ac8b85 Mon Sep 17 00:00:00 2001 From: John Arild Berentsen Date: Thu, 2 Feb 2017 05:57:57 +0100 Subject: [PATCH 10/13] [lock.zwave] Bugfix Zwave lock (#5619) * Bugfix state * remove debug --- homeassistant/components/lock/zwave.py | 74 +++++++++++++++++--------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/lock/zwave.py b/homeassistant/components/lock/zwave.py index 6ff628f158f..1f5f2ca8b15 100644 --- a/homeassistant/components/lock/zwave.py +++ b/homeassistant/components/lock/zwave.py @@ -21,11 +21,21 @@ ATTR_NOTIFICATION = 'notification' ATTR_LOCK_STATUS = 'lock_status' ATTR_CODE_SLOT = 'code_slot' ATTR_USERCODE = 'usercode' +CONFIG_ADVANCED = 'Advanced' SERVICE_SET_USERCODE = 'set_usercode' SERVICE_GET_USERCODE = 'get_usercode' SERVICE_CLEAR_USERCODE = 'clear_usercode' +POLYCONTROL = 0x10E +DANALOCK_V2_BTZE = 0x2 +POLYCONTROL_DANALOCK_V2_BTZE_LOCK = (POLYCONTROL, DANALOCK_V2_BTZE) +WORKAROUND_V2BTZE = 'v2btze' + +DEVICE_MAPPINGS = { + POLYCONTROL_DANALOCK_V2_BTZE_LOCK: WORKAROUND_V2BTZE +} + LOCK_NOTIFICATION = { 1: 'Manual Lock', 2: 'Manual Unlock', @@ -110,7 +120,7 @@ CLEAR_USERCODE_SCHEMA = vol.Schema({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): - """Find and return Z-Wave switches.""" + """Find and return Z-Wave locks.""" if discovery_info is None or zwave.NETWORK is None: return @@ -197,29 +207,61 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): - """Representation of a Z-Wave switch.""" + """Representation of a Z-Wave Lock.""" def __init__(self, value): - """Initialize the Z-Wave switch device.""" + """Initialize the Z-Wave lock device.""" zwave.ZWaveDeviceEntity.__init__(self, value, DOMAIN) - self._node = value.node self._state = None self._notification = None self._lock_status = None + self._v2btze = None + + # Enable appropriate workaround flags for our device + # Make sure that we have values for the key before converting to int + if (value.node.manufacturer_id.strip() and + value.node.product_id.strip()): + specific_sensor_key = (int(value.node.manufacturer_id, 16), + int(value.node.product_id, 16)) + if specific_sensor_key in DEVICE_MAPPINGS: + if DEVICE_MAPPINGS[specific_sensor_key] == WORKAROUND_V2BTZE: + self._v2btze = 1 + _LOGGER.debug("Polycontrol Danalock v2 BTZE " + "workaround enabled") self.update_properties() def update_properties(self): """Callback on data changes for node values.""" + for value in self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_DOOR_LOCK).values(): + if value.type != zwave.const.TYPE_BOOL: + continue + if value.genre != zwave.const.GENRE_USER: + continue + self._state = value.data + _LOGGER.debug('Lock state set from Bool value and' + ' is %s', value.data) + break + for value in self._node.get_values( class_id=zwave.const.COMMAND_CLASS_ALARM).values(): if value.label != "Access Control": continue self._notification = LOCK_NOTIFICATION.get(value.data) - if self._notification: - self._state = LOCK_STATUS.get(value.data) - _LOGGER.debug('Lock state set from Access Control value and' - ' is %s', value.data) + notification_data = value.data + if self._v2btze: + for value in (self._node.get_values( + class_id=zwave.const.COMMAND_CLASS_CONFIGURATION) + .values()): + if value.index != 12: + continue + if value.data == CONFIG_ADVANCED: + self._state = LOCK_STATUS.get(notification_data) + _LOGGER.debug('Lock state set from Access Control ' + 'value and is %s', notification_data) + break + break for value in self._node.get_values( @@ -227,10 +269,6 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): if value.label != "Alarm Type": continue alarm_type = LOCK_ALARM_TYPE.get(value.data) - if alarm_type: - self._state = LOCK_STATUS.get(value.data) - _LOGGER.debug('Lock state set from Alarm Type value and' - ' is %s', value.data) break for value in self._node.get_values( @@ -256,18 +294,6 @@ class ZwaveLock(zwave.ZWaveDeviceEntity, LockDevice): self._lock_status = LOCK_ALARM_TYPE.get(alarm_type) break - if not self._notification and not self._lock_status: - for value in self._node.get_values( - class_id=zwave.const.COMMAND_CLASS_DOOR_LOCK).values(): - if value.type != zwave.const.TYPE_BOOL: - continue - if value.genre != zwave.const.GENRE_USER: - continue - self._state = value.data - _LOGGER.debug('Lock state set from Bool value and' - ' is %s', value.data) - break - @property def is_locked(self): """Return true if device is locked.""" From 96745abf5da6e457000e748fed01b078ba6c1e89 Mon Sep 17 00:00:00 2001 From: Johan Bloemberg Date: Thu, 2 Feb 2017 06:00:05 +0100 Subject: [PATCH 11/13] Prevent infinite loop in crossconfigured mqtt event streams (#5624) * Prevent events about MQTT messages received to cause infinite loop when two HA instances are crossconfigured for mqtt_eventstream. * Fix linting * Publish all MQTT received events except incoming from eventstream. Also make it configurable. --- homeassistant/components/mqtt_eventstream.py | 14 +++++++ tests/components/test_mqtt_eventstream.py | 43 ++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/homeassistant/components/mqtt_eventstream.py b/homeassistant/components/mqtt_eventstream.py index 293b644da1f..8632f8aa99d 100644 --- a/homeassistant/components/mqtt_eventstream.py +++ b/homeassistant/components/mqtt_eventstream.py @@ -15,18 +15,23 @@ from homeassistant.const import ( ATTR_SERVICE_DATA, EVENT_CALL_SERVICE, EVENT_SERVICE_EXECUTED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL) from homeassistant.core import EventOrigin, State +import homeassistant.helpers.config_validation as cv from homeassistant.remote import JSONEncoder +from .mqtt import EVENT_MQTT_MESSAGE_RECEIVED DOMAIN = "mqtt_eventstream" DEPENDENCIES = ['mqtt'] CONF_PUBLISH_TOPIC = 'publish_topic' CONF_SUBSCRIBE_TOPIC = 'subscribe_topic' +CONF_PUBLISH_EVENTSTREAM_RECEIVED = 'publish_eventstream_received' CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_PUBLISH_TOPIC): valid_publish_topic, vol.Optional(CONF_SUBSCRIBE_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_PUBLISH_EVENTSTREAM_RECEIVED, default=False): + cv.boolean, }), }, extra=vol.ALLOW_EXTRA) @@ -45,6 +50,15 @@ def setup(hass, config): if event.event_type == EVENT_TIME_CHANGED: return + # MQTT fires a bus event for every incoming message, also messages from + # eventstream. Disable publishing these messages to other HA instances + # and possibly creating an infinite loop if these instances publish + # back to this one. + if all([not conf.get(CONF_PUBLISH_EVENTSTREAM_RECEIVED), + event.event_type == EVENT_MQTT_MESSAGE_RECEIVED, + event.data.get('topic') == sub_topic]): + return + # Filter out the events that were triggered by publishing # to the MQTT topic, or you will end up in an infinite loop. if event.event_type == EVENT_CALL_SERVICE: diff --git a/tests/components/test_mqtt_eventstream.py b/tests/components/test_mqtt_eventstream.py index a60e54df016..3dbe6338e3f 100644 --- a/tests/components/test_mqtt_eventstream.py +++ b/tests/components/test_mqtt_eventstream.py @@ -1,10 +1,12 @@ """The tests for the MQTT eventstream component.""" +from collections import namedtuple import json import unittest from unittest.mock import ANY, patch from homeassistant.bootstrap import setup_component import homeassistant.components.mqtt_eventstream as eventstream +import homeassistant.components.mqtt as mqtt from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.core import State, callback from homeassistant.remote import JSONEncoder @@ -146,3 +148,44 @@ class TestMqttEventStream(unittest.TestCase): self.hass.block_till_done() self.assertEqual(1, len(calls)) + + @patch('homeassistant.components.mqtt.publish') + def test_mqtt_received_event(self, mock_pub): + """Don't filter events from the mqtt component about received message. + + Mqtt component sends an event if a message is received. Also + messages that originate from an incoming eventstream. + Broadcasting these messages result in an infinite loop if two HA + instances are crossconfigured for the same mqtt topics. + + """ + SUB_TOPIC = 'from_slaves' + self.assertTrue( + self.add_eventstream( + pub_topic='bar', + sub_topic=SUB_TOPIC)) + self.hass.block_till_done() + + # Reset the mock because it will have already gotten calls for the + # mqtt_eventstream state change on initialization, etc. + mock_pub.reset_mock() + + # Use MQTT component message handler to simulate firing message + # received event. + MQTTMessage = namedtuple('MQTTMessage', ['topic', 'qos', 'payload']) + message = MQTTMessage(SUB_TOPIC, 1, 'Hello World!'.encode('utf-8')) + mqtt.MQTT._mqtt_on_message(self, None, {'hass': self.hass}, message) + + self.hass.block_till_done() + + # 'normal' incoming mqtt messages should be broadcasted + self.assertEqual(mock_pub.call_count, 0) + + MQTTMessage = namedtuple('MQTTMessage', ['topic', 'qos', 'payload']) + message = MQTTMessage('test_topic', 1, 'Hello World!'.encode('utf-8')) + mqtt.MQTT._mqtt_on_message(self, None, {'hass': self.hass}, message) + + self.hass.block_till_done() + + # but event from the event stream not + self.assertEqual(mock_pub.call_count, 1) From e05c1bc160c62714d98c3aea70e10f1f283b36a6 Mon Sep 17 00:00:00 2001 From: Trevor Date: Wed, 1 Feb 2017 23:06:11 -0600 Subject: [PATCH 12/13] Fix hue lightgroups not syncing state (#5702) --- homeassistant/components/light/hue.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index a934788f36b..65ae9f30cf4 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -200,7 +200,8 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable, for light_id, info in api_lights.items(): if light_id not in lights: - lights[light_id] = HueLight(int(light_id), info, + lights[light_id] = HueLight(hass, + int(light_id), info, bridge, update_lights, bridge_type, allow_unreachable, allow_in_emulated_hue) @@ -218,6 +219,7 @@ def setup_bridge(host, hass, add_devices, filename, allow_unreachable, if lightgroup_id not in lightgroups: lightgroups[lightgroup_id] = HueLight( + hass, int(lightgroup_id), info, bridge, update_lights, bridge_type, allow_unreachable, allow_in_emulated_hue, True) @@ -280,10 +282,11 @@ def request_configuration(host, hass, add_devices, filename, class HueLight(Light): """Representation of a Hue light.""" - def __init__(self, light_id, info, bridge, update_lights, + def __init__(self, hass, light_id, info, bridge, update_lights, bridge_type, allow_unreachable, allow_in_emulated_hue, is_group=False): """Initialize the light.""" + self.hass = hass self.light_id = light_id self.info = info self.bridge = bridge From 181943e139e1a9a7bc90ca8ad4e35ed988edd419 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Feb 2017 21:19:45 -0800 Subject: [PATCH 13/13] Version bump to 0.37.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2bde15106d2..ee22f4ef7ad 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 37 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 4, 2)