From ff3a4637a4fa8f2da4ae73e38849e8900e05700d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 10 Aug 2017 23:28:04 +0200 Subject: [PATCH 01/93] Version bump to 0.52.0.dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 85846e32f59..978dafa457b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 51 +MINOR_VERSION = 52 PATCH_VERSION = '0.dev0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From d427063acd29a6dc71f0a86596e23c430b3b32fc Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 11 Aug 2017 02:35:45 -0400 Subject: [PATCH 02/93] Update python-wink version to fix Dome water valve bug. (#8923) --- homeassistant/components/switch/wink.py | 3 ++- homeassistant/components/wink.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switch/wink.py b/homeassistant/components/switch/wink.py index ec9311ac9e9..aa33c2f7132 100644 --- a/homeassistant/components/switch/wink.py +++ b/homeassistant/components/switch/wink.py @@ -65,7 +65,8 @@ class WinkToggleDevice(WinkDevice, ToggleEntity): attributes = super(WinkToggleDevice, self).device_state_attributes try: event = self.wink.last_event() - attributes["last_event"] = event + if event is not None: + attributes["last_event"] = event except AttributeError: pass return attributes diff --git a/homeassistant/components/wink.py b/homeassistant/components/wink.py index 316c939492b..8d40f5dad48 100644 --- a/homeassistant/components/wink.py +++ b/homeassistant/components/wink.py @@ -25,7 +25,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['python-wink==1.4.2', 'pubnubsub-handler==1.0.2'] +REQUIREMENTS = ['python-wink==1.5.1', 'pubnubsub-handler==1.0.2'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 91be5eb7be3..b071f7ad98c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -775,7 +775,7 @@ python-velbus==2.0.11 python-vlc==1.1.2 # homeassistant.components.wink -python-wink==1.4.2 +python-wink==1.5.1 # homeassistant.components.zwave python_openzwave==0.4.0.31 From 0999e2ddc476f4bddf710005168b082f03a7cdc0 Mon Sep 17 00:00:00 2001 From: Philipp Schmitt Date: Fri, 11 Aug 2017 11:22:22 +0200 Subject: [PATCH 03/93] Update roombapy to 1.3.1 to avoid installing all the mapping dependencies (#8925) --- homeassistant/components/vacuum/roomba.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vacuum/roomba.py b/homeassistant/components/vacuum/roomba.py index b6dcda4bcaa..cf9ee064283 100644 --- a/homeassistant/components/vacuum/roomba.py +++ b/homeassistant/components/vacuum/roomba.py @@ -17,7 +17,7 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['roombapy==1.3.0'] +REQUIREMENTS = ['roombapy==1.3.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index b071f7ad98c..07bb6846c61 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -838,7 +838,7 @@ rflink==0.0.34 ring_doorbell==0.1.4 # homeassistant.components.vacuum.roomba -roombapy==1.3.0 +roombapy==1.3.1 # homeassistant.components.switch.rpi_rf # rpi-rf==0.9.6 From 49733b7fdfaa568c3536012f2761069f0addbcb5 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 12 Aug 2017 04:55:57 +0200 Subject: [PATCH 04/93] Remove not needed call to update (#8930) * This will ensure no I/O in entity properties. --- homeassistant/components/switch/rachio.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/switch/rachio.py b/homeassistant/components/switch/rachio.py index 547442a4233..c9b6011bcbd 100644 --- a/homeassistant/components/switch/rachio.py +++ b/homeassistant/components/switch/rachio.py @@ -199,7 +199,6 @@ class RachioZone(SwitchDevice): @property def is_on(self): """Whether the zone is currently running.""" - self._device.update() schedule = self._device.current_schedule return self.zone_id == schedule.get('zoneId') From c4550d02c530a53b0adbb617eb11aaea32e2545e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sat, 12 Aug 2017 08:52:56 +0200 Subject: [PATCH 05/93] Add version sensor (#8912) * Add version sensor * Set version directly * Rework tests and fix typo * Remove additional blank line --- homeassistant/components/sensor/version.py | 55 ++++++++++++++++++++++ tests/components/sensor/test_version.py | 50 ++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 homeassistant/components/sensor/version.py create mode 100644 tests/components/sensor/test_version.py diff --git a/homeassistant/components/sensor/version.py b/homeassistant/components/sensor/version.py new file mode 100644 index 00000000000..c19d2743563 --- /dev/null +++ b/homeassistant/components/sensor/version.py @@ -0,0 +1,55 @@ +""" +Support for displaying the current version of Home Assistant. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.version/ +""" +import asyncio +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import __version__, CONF_NAME +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "Current Version" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, +}) + + +@asyncio.coroutine +def async_setup_platform(hass, config, async_add_devices, discovery_info=None): + """Set up the Version sensor platform.""" + name = config.get(CONF_NAME) + + async_add_devices([VersionSensor(name)]) + + +class VersionSensor(Entity): + """Representation of a Home Assistant version sensor.""" + + def __init__(self, name): + """Initialize the Version sensor.""" + self._name = name + self._state = __version__ + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def state(self): + """Return the state of the sensor.""" + return self._state diff --git a/tests/components/sensor/test_version.py b/tests/components/sensor/test_version.py new file mode 100644 index 00000000000..270cfd1709d --- /dev/null +++ b/tests/components/sensor/test_version.py @@ -0,0 +1,50 @@ +"""The test for the version sensor platform.""" +import asyncio +import unittest +from unittest.mock import patch + +from homeassistant.setup import setup_component + +from tests.common import get_test_home_assistant + +MOCK_VERSION = '10.0' + + +class TestVersionSensor(unittest.TestCase): + """Test the Version sensor.""" + + def setup_method(self, method): + """Set up things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + + def test_version_sensor(self): + """Test the Version sensor.""" + config = { + 'sensor': { + 'platform': 'version', + } + } + + assert setup_component(self.hass, 'sensor', config) + + @asyncio.coroutine + def test_version(self): + """Test the Version sensor.""" + config = { + 'sensor': { + 'platform': 'version', + 'name': 'test', + } + } + + with patch('homeassistant.const.__version__', MOCK_VERSION): + assert setup_component(self.hass, 'sensor', config) + self.hass.block_till_done() + + state = self.hass.states.get('sensor.test') + + self.assertEqual(state.state, '10.0') From 489a02b2c267aef025fe985450816eb7b6880669 Mon Sep 17 00:00:00 2001 From: groth-its Date: Sat, 12 Aug 2017 17:38:12 +0200 Subject: [PATCH 06/93] Fix hue lights for Philips and non-philips lights (#8905) --- homeassistant/components/light/hue.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index cdbea7d2194..27c3b43e926 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -384,7 +384,6 @@ class HueLight(Light): hue, sat = color_util.color_xy_to_hs(*kwargs[ATTR_XY_COLOR]) command['hue'] = hue command['sat'] = sat - command['bri'] = self.info['bri'] else: command['xy'] = kwargs[ATTR_XY_COLOR] elif ATTR_RGB_COLOR in kwargs: @@ -399,14 +398,13 @@ class HueLight(Light): *(int(val) for val in kwargs[ATTR_RGB_COLOR])) command['xy'] = xyb[0], xyb[1] command['bri'] = xyb[2] + elif ATTR_COLOR_TEMP in kwargs: + temp = kwargs[ATTR_COLOR_TEMP] + command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) if ATTR_BRIGHTNESS in kwargs: command['bri'] = kwargs[ATTR_BRIGHTNESS] - if ATTR_COLOR_TEMP in kwargs: - temp = kwargs[ATTR_COLOR_TEMP] - command['ct'] = max(self.min_mireds, min(temp, self.max_mireds)) - flash = kwargs.get(ATTR_FLASH) if flash == FLASH_LONG: @@ -425,9 +423,9 @@ class HueLight(Light): elif effect == EFFECT_RANDOM: command['hue'] = random.randrange(0, 65535) command['sat'] = random.randrange(150, 254) - elif self.bridge_type == 'hue': - if self.info.get('manufacturername') != "OSRAM": - command['effect'] = 'none' + elif (self.bridge_type == 'hue' and + self.info.get('manufacturername') == 'Philips'): + command['effect'] = 'none' self._command_func(self.light_id, command) From 369caeedbd2eefb4497877d07596e2ac747dfc4d Mon Sep 17 00:00:00 2001 From: cribbstechnologies Date: Sat, 12 Aug 2017 11:50:02 -0400 Subject: [PATCH 07/93] fixing emulated hue issue and testing it (#8928) * fixing emulated hue issue and testing it * fixing hound issues * I should probably stop using vim * Check against dict directly instead of items. --- .../components/emulated_hue/__init__.py | 4 +- tests/components/emulated_hue/test_init.py | 59 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 315fc564999..ae0a26aaea4 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -193,7 +193,9 @@ class Config(object): if entity_id == ent_id: return number - number = str(max(int(k) for k in self.numbers) + 1) + number = '1' + if self.numbers: + number = str(max(int(k) for k in self.numbers) + 1) self.numbers[number] = entity_id self._save_numbers_json() return number diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 8c0a6dc4f60..2dcb9ecbf21 100755 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -36,6 +36,65 @@ def test_config_google_home_entity_id_to_number(): assert entity_id == 'light.test2' +def test_config_google_home_entity_id_to_number_altered(): + """Test config adheres to the type.""" + conf = Config(Mock(), { + 'type': 'google_home' + }) + + mop = mock_open(read_data=json.dumps({'21': 'light.test2'})) + handle = mop() + + with patch('homeassistant.components.emulated_hue.open', mop, create=True): + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '21': 'light.test2', + '22': 'light.test', + } + + number = conf.entity_id_to_number('light.test') + assert number == '22' + assert handle.write.call_count == 1 + + number = conf.entity_id_to_number('light.test2') + assert number == '21' + assert handle.write.call_count == 1 + + entity_id = conf.number_to_entity_id('21') + assert entity_id == 'light.test2' + + +def test_config_google_home_entity_id_to_number_empty(): + """Test config adheres to the type.""" + conf = Config(Mock(), { + 'type': 'google_home' + }) + + mop = mock_open(read_data='') + handle = mop() + + with patch('homeassistant.components.emulated_hue.open', mop, create=True): + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 + assert json.loads(handle.write.mock_calls[0][1][0]) == { + '1': 'light.test', + } + + number = conf.entity_id_to_number('light.test') + assert number == '1' + assert handle.write.call_count == 1 + + number = conf.entity_id_to_number('light.test2') + assert number == '2' + assert handle.write.call_count == 2 + + entity_id = conf.number_to_entity_id('2') + assert entity_id == 'light.test2' + + def test_config_alexa_entity_id_to_number(): """Test config adheres to the type.""" conf = Config(None, { From fbb67820813b1bd674162339ad71ea253c18f261 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 12 Aug 2017 18:39:05 +0200 Subject: [PATCH 08/93] Fix SET_TEMPERATURE_SCHEMA in climate component (#8879) * Require either temperature or high/low target temperatures. * Add tests. --- homeassistant/components/climate/__init__.py | 18 +++++---- tests/common.py | 5 ++- tests/components/climate/test_init.py | 40 ++++++++++++++++++++ 3 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 tests/components/climate/test_init.py diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 6dd66817d43..1f919301254 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -86,13 +86,17 @@ SET_AUX_HEAT_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_AUX_HEAT): cv.boolean, }) -SET_TEMPERATURE_SCHEMA = vol.Schema({ - vol.Exclusive(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float), - vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float), - vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_OPERATION_MODE): cv.string, -}) +SET_TEMPERATURE_SCHEMA = vol.Schema(vol.All( + cv.has_at_least_one_key( + ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW), + { + vol.Exclusive(ATTR_TEMPERATURE, 'temperature'): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_HIGH, 'temperature'): vol.Coerce(float), + vol.Inclusive(ATTR_TARGET_TEMP_LOW, 'temperature'): vol.Coerce(float), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Optional(ATTR_OPERATION_MODE): cv.string, + } +)) SET_FAN_MODE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Required(ATTR_FAN_MODE): cv.string, diff --git a/tests/common.py b/tests/common.py index 5e328959a7a..5fdec2fc411 100644 --- a/tests/common.py +++ b/tests/common.py @@ -174,7 +174,7 @@ def get_test_instance_port(): @ha.callback -def async_mock_service(hass, domain, service): +def async_mock_service(hass, domain, service, schema=None): """Set up a fake service & return a calls log list to this service.""" calls = [] @@ -183,7 +183,8 @@ def async_mock_service(hass, domain, service): """Mock service call.""" calls.append(call) - hass.services.async_register(domain, service, mock_service_log) + hass.services.async_register( + domain, service, mock_service_log, schema=schema) return calls diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py new file mode 100644 index 00000000000..2e942c5988c --- /dev/null +++ b/tests/components/climate/test_init.py @@ -0,0 +1,40 @@ +"""The tests for the climate component.""" +import asyncio + +from homeassistant.components.climate import SET_TEMPERATURE_SCHEMA +from tests.common import async_mock_service + + +@asyncio.coroutine +def test_set_temp_schema_no_req(hass, caplog): + """Test the set temperature schema with missing required data.""" + domain = 'climate' + service = 'test_set_temperature' + schema = SET_TEMPERATURE_SCHEMA + calls = async_mock_service(hass, domain, service, schema) + + data = {'operation_mode': 'test', 'entity_id': ['climate.test_id']} + yield from hass.services.async_call(domain, service, data) + yield from hass.async_block_till_done() + + assert len(calls) == 0 + assert 'ERROR' in caplog.text + assert 'Invalid service data' in caplog.text + + +@asyncio.coroutine +def test_set_temp_schema(hass, caplog): + """Test the set temperature schema with ok required data.""" + domain = 'climate' + service = 'test_set_temperature' + schema = SET_TEMPERATURE_SCHEMA + calls = async_mock_service(hass, domain, service, schema) + + data = { + 'temperature': 20.0, 'operation_mode': 'test', + 'entity_id': ['climate.test_id']} + yield from hass.services.async_call(domain, service, data) + yield from hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[-1].data == data From 4a98b32a03986e9633402142313359b45e94fb8f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Aug 2017 14:54:50 -0700 Subject: [PATCH 09/93] Update frontend --- homeassistant/components/frontend/version.py | 2 +- .../www_static/home-assistant-polymer | 2 +- .../www_static/panels/ha-panel-config.html | 2 +- .../www_static/panels/ha-panel-config.html.gz | Bin 20661 -> 20665 bytes 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 001e369602b..59b09aa4ca1 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -6,7 +6,7 @@ FINGERPRINTS = { "frontend.html": "fb225cfababf965f8e19a8eb5c5a2a7e", "mdi.html": "e91f61a039ed0a9936e7ee5360da3870", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", - "panels/ha-panel-config.html": "878fd176dad70fe5cb8fc3c4ca72145c", + "panels/ha-panel-config.html": "ec48185c79000d0cfe5bbf38c7974944", "panels/ha-panel-dev-event.html": "d409e7ab537d9fe629126d122345279c", "panels/ha-panel-dev-info.html": "b0e55eb657fd75f21aba2426ac0cedc0", "panels/ha-panel-dev-mqtt.html": "94b222b013a98583842de3e72d5888c6", diff --git a/homeassistant/components/frontend/www_static/home-assistant-polymer b/homeassistant/components/frontend/www_static/home-assistant-polymer index a51b9c1eb5a..823a6996708 160000 --- a/homeassistant/components/frontend/www_static/home-assistant-polymer +++ b/homeassistant/components/frontend/www_static/home-assistant-polymer @@ -1 +1 @@ -Subproject commit a51b9c1eb5aecd119706a64c353b5df1de0af8cd +Subproject commit 823a6996708d5b85097fb1db0defbc03516e77bf diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html index 98742c6ac29..d645ff87f8e 100644 --- a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html +++ b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz index 775e05b4a0207047491e526b6172a078c99639a2..e794d3735977c39139d3b34360a40ad42c371c37 100644 GIT binary patch delta 7808 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 SCB=BO`~Ly;5tJT)e*plD&QI_F delta 7804 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 Date: Sun, 13 Aug 2017 02:49:15 -0400 Subject: [PATCH 10/93] Fixed cert_expiry sensor to delay firing on HA startup (#8920) * Fixed cert_expiry sensor to delay firing on HA startup * Addressed Travis complaints * Added imports * Fixed cert_expiry sensor to delay firing on HA startup * Changed comment --- .../components/sensor/cert_expiry.py | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/cert_expiry.py b/homeassistant/components/sensor/cert_expiry.py index dfc15510d6f..1ccaf2f6925 100644 --- a/homeassistant/components/sensor/cert_expiry.py +++ b/homeassistant/components/sensor/cert_expiry.py @@ -4,16 +4,17 @@ Counter for the days till a HTTPS (TLS) certificate will expire. For more details about this sensor please refer to the documentation at https://home-assistant.io/components/sensor.cert_expiry/ """ -import datetime import logging import socket import ssl +from datetime import datetime, timedelta import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT) +from homeassistant.const import (CONF_NAME, CONF_HOST, CONF_PORT, + EVENT_HOMEASSISTANT_START) from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = 'SSL Certificate Expiry' DEFAULT_PORT = 443 -SCAN_INTERVAL = datetime.timedelta(hours=12) +SCAN_INTERVAL = timedelta(hours=12) TIMEOUT = 10.0 @@ -34,11 +35,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up certificate expiry sensor.""" - server_name = config.get(CONF_HOST) - server_port = config.get(CONF_PORT) - sensor_name = config.get(CONF_NAME) + def run_setup(event): + """Wait until Home Assistant is fully initialized before creating. - add_devices([SSLCertificate(sensor_name, server_name, server_port)], True) + Delay the setup until Home Assistant is fully initialized. + """ + server_name = config.get(CONF_HOST) + server_port = config.get(CONF_PORT) + sensor_name = config.get(CONF_NAME) + + add_devices([SSLCertificate(sensor_name, server_name, server_port)], + True) + + # To allow checking of the HA certificate we must first be running. + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, run_setup) class SSLCertificate(Entity): @@ -97,6 +107,6 @@ class SSLCertificate(Entity): return ts_seconds = ssl.cert_time_to_seconds(cert['notAfter']) - timestamp = datetime.datetime.fromtimestamp(ts_seconds) - expiry = timestamp - datetime.datetime.today() + timestamp = datetime.fromtimestamp(ts_seconds) + expiry = timestamp - datetime.today() self._state = expiry.days From 73d6227021633cd78841be25922b740e32724842 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sun, 13 Aug 2017 09:54:43 +0200 Subject: [PATCH 11/93] Remove spaces from Xiami switch attributes (#8952) * Attributes of the xiaomi zigbee plug changed. * Reformat. --- homeassistant/components/switch/xiaomi.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switch/xiaomi.py b/homeassistant/components/switch/xiaomi.py index 3e4ea4f6d72..767043a8bc9 100644 --- a/homeassistant/components/switch/xiaomi.py +++ b/homeassistant/components/switch/xiaomi.py @@ -6,9 +6,13 @@ from homeassistant.components.xiaomi import (PY_XIAOMI_GATEWAY, XiaomiDevice) _LOGGER = logging.getLogger(__name__) -ATTR_LOAD_POWER = 'Load power' # Load power in watts (W) -ATTR_POWER_CONSUMED = 'Power consumed' -ATTR_IN_USE = 'In use' +# Load power in watts (W) +ATTR_LOAD_POWER = 'load_power' + +# Total (lifetime) power consumption in watts +ATTR_POWER_CONSUMED = 'power_consumed' +ATTR_IN_USE = 'in_use' + LOAD_POWER = 'load_power' POWER_CONSUMED = 'power_consumed' IN_USE = 'inuse' From c92e5c147a360175b964121525d20bc03cf4ef18 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Sun, 13 Aug 2017 15:02:48 +0200 Subject: [PATCH 12/93] fix DeviceException handling when updating xiaomi vacuum (#8954) * Fix DeviceException handling when updating entity * add DeviceException error handling to generic request --- homeassistant/components/vacuum/xiaomi.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vacuum/xiaomi.py b/homeassistant/components/vacuum/xiaomi.py index c5fb0d3c003..5e5081a2aa8 100644 --- a/homeassistant/components/vacuum/xiaomi.py +++ b/homeassistant/components/vacuum/xiaomi.py @@ -219,12 +219,12 @@ class MiroboVacuum(VacuumDevice): @asyncio.coroutine def _try_command(self, mask_error, func, *args, **kwargs): """Call a vacuum command handling error messages.""" - from mirobo import VacuumException + from mirobo import DeviceException, VacuumException try: yield from self.hass.async_add_job(partial(func, *args, **kwargs)) return True - except VacuumException as ex: - _LOGGER.error(mask_error, ex) + except (DeviceException, VacuumException) as exc: + _LOGGER.error(mask_error, exc) return False @asyncio.coroutine @@ -341,7 +341,7 @@ class MiroboVacuum(VacuumDevice): @asyncio.coroutine def async_update(self): """Fetch state from the device.""" - from mirobo import VacuumException + from mirobo import DeviceException try: state = yield from self.hass.async_add_job(self._vacuum.status) _LOGGER.debug("Got new state from the vacuum: %s", state.data) @@ -351,6 +351,6 @@ class MiroboVacuum(VacuumDevice): except OSError as exc: _LOGGER.error("Got OSError while fetching the state: %s", exc) # self._available = False - except VacuumException as exc: + except DeviceException as exc: _LOGGER.warning("Got exception while fetching the state: %s", exc) # self._available = False From 811fdc5533db53170733a08e587e6798bd1becc6 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Sun, 13 Aug 2017 18:57:48 +0100 Subject: [PATCH 13/93] Add service to alarm control panel for night mode arming (#8614) * Update const.py * Update __init__.py * Update services.yaml * Update totalconnect.py * Update manual.py Add night arm service for manual alarm control panel * Update test_manual.py Add tests for night mode arming * Update manual.py Fix docstring --- .../alarm_control_panel/__init__.py | 27 ++++++- .../components/alarm_control_panel/manual.py | 24 +++++- .../alarm_control_panel/services.yaml | 11 +++ .../alarm_control_panel/totalconnect.py | 14 +++- homeassistant/const.py | 4 + .../alarm_control_panel/test_manual.py | 80 ++++++++++++++++++- 6 files changed, 152 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/alarm_control_panel/__init__.py b/homeassistant/components/alarm_control_panel/__init__.py index 39c86f3215f..005048ba8c1 100644 --- a/homeassistant/components/alarm_control_panel/__init__.py +++ b/homeassistant/components/alarm_control_panel/__init__.py @@ -13,7 +13,8 @@ import voluptuous as vol from homeassistant.const import ( ATTR_CODE, ATTR_CODE_FORMAT, ATTR_ENTITY_ID, SERVICE_ALARM_TRIGGER, - SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY) + SERVICE_ALARM_DISARM, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_NIGHT) from homeassistant.config import load_yaml_config_file from homeassistant.loader import bind_hass from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa @@ -31,6 +32,7 @@ SERVICE_TO_METHOD = { SERVICE_ALARM_DISARM: 'alarm_disarm', SERVICE_ALARM_ARM_HOME: 'alarm_arm_home', SERVICE_ALARM_ARM_AWAY: 'alarm_arm_away', + SERVICE_ALARM_ARM_NIGHT: 'alarm_arm_night', SERVICE_ALARM_TRIGGER: 'alarm_trigger' } @@ -81,6 +83,18 @@ def alarm_arm_away(hass, code=None, entity_id=None): hass.services.call(DOMAIN, SERVICE_ALARM_ARM_AWAY, data) +@bind_hass +def alarm_arm_night(hass, code=None, entity_id=None): + """Send the alarm the command for arm night.""" + data = {} + if code: + data[ATTR_CODE] = code + if entity_id: + data[ATTR_ENTITY_ID] = entity_id + + hass.services.call(DOMAIN, SERVICE_ALARM_ARM_NIGHT, data) + + @bind_hass def alarm_trigger(hass, code=None, entity_id=None): """Send the alarm the command for trigger.""" @@ -187,6 +201,17 @@ class AlarmControlPanel(Entity): """ return self.hass.async_add_job(self.alarm_arm_away, code) + def alarm_arm_night(self, code=None): + """Send arm night command.""" + raise NotImplementedError() + + def async_alarm_arm_night(self, code=None): + """Send arm night command. + + This method must be run in the event loop and returns a coroutine. + """ + return self.hass.async_add_job(self.alarm_arm_night, code) + def alarm_trigger(self, code=None): """Send alarm trigger command.""" raise NotImplementedError() diff --git a/homeassistant/components/alarm_control_panel/manual.py b/homeassistant/components/alarm_control_panel/manual.py index c87aea862d5..97820ab4b2b 100644 --- a/homeassistant/components/alarm_control_panel/manual.py +++ b/homeassistant/components/alarm_control_panel/manual.py @@ -12,9 +12,10 @@ import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm import homeassistant.util.dt as dt_util from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, CONF_PLATFORM, CONF_NAME, - CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, CONF_DISARM_AFTER_TRIGGER) + STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, + CONF_PLATFORM, CONF_NAME, CONF_CODE, CONF_PENDING_TIME, CONF_TRIGGER_TIME, + CONF_DISARM_AFTER_TRIGGER) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import track_point_in_time @@ -87,7 +88,8 @@ class ManualAlarm(alarm.AlarmControlPanel): def state(self): """Return the state of the device.""" if self._state in (STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_AWAY) and \ + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_NIGHT) and \ self._pending_time and self._state_ts + self._pending_time > \ dt_util.utcnow(): return STATE_ALARM_PENDING @@ -145,6 +147,20 @@ class ManualAlarm(alarm.AlarmControlPanel): self._hass, self.async_update_ha_state, self._state_ts + self._pending_time) + def alarm_arm_night(self, code=None): + """Send arm night command.""" + if not self._validate_code(code, STATE_ALARM_ARMED_NIGHT): + return + + self._state = STATE_ALARM_ARMED_NIGHT + self._state_ts = dt_util.utcnow() + self.schedule_update_ha_state() + + if self._pending_time: + track_point_in_time( + self._hass, self.async_update_ha_state, + self._state_ts + self._pending_time) + def alarm_trigger(self, code=None): """Send alarm trigger command. No code needed.""" self._pre_trigger_state = self._state diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index 6cc3946ca66..19c3ca0233d 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -31,6 +31,17 @@ alarm_arm_away: description: An optional code to arm away the alarm control panel with example: 1234 +alarm_arm_night: + description: Send the alarm the command for arm night + + fields: + entity_id: + description: Name of alarm control panel to arm night + example: 'alarm_control_panel.downstairs' + code: + description: An optional code to arm night the alarm control panel with + example: 1234 + alarm_trigger: description: Send the alarm the command for trigger diff --git a/homeassistant/components/alarm_control_panel/totalconnect.py b/homeassistant/components/alarm_control_panel/totalconnect.py index 9c0b5108fee..05dc8aeef20 100644 --- a/homeassistant/components/alarm_control_panel/totalconnect.py +++ b/homeassistant/components/alarm_control_panel/totalconnect.py @@ -13,8 +13,8 @@ import homeassistant.components.alarm_control_panel as alarm from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_UNKNOWN, - CONF_NAME) + STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, + STATE_ALARM_ARMING, STATE_ALARM_DISARMING, STATE_UNKNOWN, CONF_NAME) REQUIREMENTS = ['total_connect_client==0.11'] @@ -74,6 +74,12 @@ class TotalConnect(alarm.AlarmControlPanel): state = STATE_ALARM_ARMED_HOME elif status == self._client.ARMED_AWAY: state = STATE_ALARM_ARMED_AWAY + elif status == self._client.ARMED_STAY_NIGHT: + state = STATE_ALARM_ARMED_NIGHT + elif status == self._client.ARMING: + state = STATE_ALARM_ARMING + elif status == self._client.DISARMING: + state = STATE_ALARM_DISARMING else: state = STATE_UNKNOWN @@ -90,3 +96,7 @@ class TotalConnect(alarm.AlarmControlPanel): def alarm_arm_away(self, code=None): """Send arm away command.""" self._client.arm_away() + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + self._client.arm_stay_night() diff --git a/homeassistant/const.py b/homeassistant/const.py index 978dafa457b..19ce7e470c2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -199,7 +199,10 @@ STATE_STANDBY = 'standby' STATE_ALARM_DISARMED = 'disarmed' STATE_ALARM_ARMED_HOME = 'armed_home' STATE_ALARM_ARMED_AWAY = 'armed_away' +STATE_ALARM_ARMED_NIGHT = 'armed_night' STATE_ALARM_PENDING = 'pending' +STATE_ALARM_ARMING = 'arming' +STATE_ALARM_DISARMING = 'disarming' STATE_ALARM_TRIGGERED = 'triggered' STATE_LOCKED = 'locked' STATE_UNLOCKED = 'unlocked' @@ -347,6 +350,7 @@ SERVICE_SHUFFLE_SET = 'shuffle_set' SERVICE_ALARM_DISARM = 'alarm_disarm' SERVICE_ALARM_ARM_HOME = 'alarm_arm_home' SERVICE_ALARM_ARM_AWAY = 'alarm_arm_away' +SERVICE_ALARM_ARM_NIGHT = 'alarm_arm_night' SERVICE_ALARM_TRIGGER = 'alarm_trigger' SERVICE_LOCK = 'lock' diff --git a/tests/components/alarm_control_panel/test_manual.py b/tests/components/alarm_control_panel/test_manual.py index 7bd89d12a0a..e5d819bc815 100644 --- a/tests/components/alarm_control_panel/test_manual.py +++ b/tests/components/alarm_control_panel/test_manual.py @@ -6,7 +6,7 @@ from unittest.mock import patch from homeassistant.setup import setup_component from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_AWAY, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED) from homeassistant.components import alarm_control_panel import homeassistant.util.dt as dt_util @@ -182,6 +182,84 @@ class TestAlarmControlPanelManual(unittest.TestCase): self.assertEqual(STATE_ALARM_DISARMED, self.hass.states.get(entity_id).state) + def test_arm_night_no_pending(self): + """Test arm night method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 0, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_night(self.hass, CODE) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_NIGHT, + self.hass.states.get(entity_id).state) + + def test_arm_night_with_pending(self): + """Test arm night method.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 1, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_night(self.hass, CODE, entity_id) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_PENDING, + self.hass.states.get(entity_id).state) + + future = dt_util.utcnow() + timedelta(seconds=1) + with patch(('homeassistant.components.alarm_control_panel.manual.' + 'dt_util.utcnow'), return_value=future): + fire_time_changed(self.hass, future) + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_ARMED_NIGHT, + self.hass.states.get(entity_id).state) + + def test_arm_night_with_invalid_code(self): + """Attempt to night home without a valid code.""" + self.assertTrue(setup_component( + self.hass, alarm_control_panel.DOMAIN, + {'alarm_control_panel': { + 'platform': 'manual', + 'name': 'test', + 'code': CODE, + 'pending_time': 1, + 'disarm_after_trigger': False + }})) + + entity_id = 'alarm_control_panel.test' + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + + alarm_control_panel.alarm_arm_night(self.hass, CODE + '2') + self.hass.block_till_done() + + self.assertEqual(STATE_ALARM_DISARMED, + self.hass.states.get(entity_id).state) + def test_trigger_no_pending(self): """Test triggering when no pending submitted method.""" self.assertTrue(setup_component( From cbe5225e04c9354fd9929a5a2dd94b677596bec4 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sun, 13 Aug 2017 20:28:33 +0200 Subject: [PATCH 14/93] Fix call to ha_send_commands (#8956) * Name keyword arguments correctly according to dependency lib. * Only pass keyword arguments that are not None. --- homeassistant/components/remote/harmony.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/remote/harmony.py b/homeassistant/components/remote/harmony.py index 19dcc10c545..b25741207de 100755 --- a/homeassistant/components/remote/harmony.py +++ b/homeassistant/components/remote/harmony.py @@ -214,14 +214,15 @@ class HarmonyRemote(remote.RemoteDevice): if device is None: _LOGGER.error("Missing required argument: device") return + params = {} num_repeats = kwargs.pop(ATTR_NUM_REPEATS, None) if num_repeats is not None: - kwargs[ATTR_NUM_REPEATS] = num_repeats + params['repeat_num'] = num_repeats delay_secs = kwargs.pop(ATTR_DELAY_SECS, None) if delay_secs is not None: - kwargs[ATTR_DELAY_SECS] = delay_secs + params['delay_secs'] = delay_secs pyharmony.ha_send_commands( - self._token, self.host, self._port, device, command, **kwargs) + self._token, self.host, self._port, device, command, **params) def sync(self): """Sync the Harmony device with the web service.""" From 4b3a932d88b19a6af92013d376a95153e44abfbe Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Aug 2017 11:29:48 -0700 Subject: [PATCH 15/93] Sabnzbd: do not assume discovery info is a dict (#8951) --- homeassistant/components/sensor/sabnzbd.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index dd02ce389f3..e2b7584d865 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -130,15 +130,16 @@ def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the SABnzbd platform.""" from pysabnzbd import SabnzbdApi - host = config.get(CONF_HOST) or discovery_info.get(CONF_HOST) - port = config.get(CONF_PORT) or discovery_info.get(CONF_PORT) - name = config.get(CONF_NAME, DEFAULT_NAME) - use_ssl = DEFAULT_SSL - - if config.get(CONF_SSL): - use_ssl = True - elif discovery_info.get('properties', {}).get('https', '0') == '1': - use_ssl = True + if discovery_info is not None: + host = discovery_info.get(CONF_HOST) + port = discovery_info.get(CONF_PORT) + name = DEFAULT_NAME + use_ssl = discovery_info.get('properties', {}).get('https', '0') == '1' + else: + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + name = config.get(CONF_NAME, DEFAULT_NAME) + use_ssl = config.get(CONF_SSL) uri_scheme = 'https://' if use_ssl else 'http://' base_url = "{}{}:{}/".format(uri_scheme, host, port) From 74adebc2fdce0abb2b3b4651c9cd4c1e387eef9d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Aug 2017 13:28:36 -0700 Subject: [PATCH 16/93] fix issue #8948 in pushbullet (#8965) * fix issue #8948 in pushbullet * pushbullet --- homeassistant/components/notify/pushbullet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index f3bb5a119d0..8ac2bd06dad 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -91,7 +91,7 @@ class PushBulletNotificationService(BaseNotificationService): # Backward compatibility, notify all devices in own account if url: self.pushbullet.push_link(title, url, body=message) - if self.hass.config.is_allowed_path(filepath): + if filepath and self.hass.config.is_allowed_path(filepath): with open(filepath, "rb") as fileh: filedata = self.pushbullet.upload_file(fileh, filepath) self.pushbullet.push_file(title=title, body=message, @@ -115,7 +115,7 @@ class PushBulletNotificationService(BaseNotificationService): if url: self.pushbullet.push_link( title, url, body=message, email=tname) - if self.hass.config.is_allowed_path(filepath): + if filepath and self.hass.config.is_allowed_path(filepath): with open(filepath, "rb") as fileh: filedata = self.pushbullet.upload_file(fileh, filepath) self.pushbullet.push_file(title=title, body=message, From 23273d3e882fe434e8edb7f16622b35ed8e95300 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 14 Aug 2017 04:15:59 +0300 Subject: [PATCH 17/93] Fix zwave power_consumption attribute (#8968) --- homeassistant/components/zwave/util.py | 7 ++-- tests/components/zwave/test_init.py | 49 ++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zwave/util.py b/homeassistant/components/zwave/util.py index 6abdc180742..8c74b731ad6 100644 --- a/homeassistant/components/zwave/util.py +++ b/homeassistant/components/zwave/util.py @@ -54,9 +54,12 @@ def check_value_schema(value, schema): value.instance, schema[const.DISC_INSTANCE]) return False if const.DISC_SCHEMAS in schema: + found = False for schema_item in schema[const.DISC_SCHEMAS]: - if not check_value_schema(value, schema_item): - return False + found = found or check_value_schema(value, schema_item) + if not found: + return False + return True diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 19335caa47e..2fa4dd0b929 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -3,6 +3,9 @@ import asyncio from collections import OrderedDict from datetime import datetime +import unittest +from unittest.mock import patch, MagicMock + from homeassistant.bootstrap import async_setup_component from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.components import zwave @@ -12,8 +15,6 @@ from homeassistant.components.zwave import ( from homeassistant.setup import setup_component import pytest -import unittest -from unittest.mock import patch, MagicMock from tests.common import ( get_test_home_assistant, async_fire_time_changed) @@ -312,6 +313,50 @@ def test_value_discovery_existing_entity(hass, mock_openzwave): 'current_temperature'] == 23.5 +@asyncio.coroutine +def test_power_schemes(hass, mock_openzwave): + """Test power attribute.""" + mock_receivers = [] + + def mock_connect(receiver, signal, *args, **kwargs): + if signal == MockNetwork.SIGNAL_VALUE_ADDED: + mock_receivers.append(receiver) + + with patch('pydispatch.dispatcher.connect', new=mock_connect): + yield from async_setup_component(hass, 'zwave', {'zwave': { + 'new_entity_ids': True, + }}) + + assert len(mock_receivers) == 1 + + node = MockNode(node_id=11, generic=const.GENERIC_TYPE_SWITCH_BINARY) + switch = MockValue( + data=True, node=node, index=12, instance=13, + command_class=const.COMMAND_CLASS_SWITCH_BINARY, + genre=const.GENRE_USER, type=const.TYPE_BOOL) + hass.async_add_job(mock_receivers[0], node, switch) + + yield from hass.async_block_till_done() + + assert hass.states.get('switch.mock_node_mock_value').state == 'on' + assert 'power_consumption' not in hass.states.get( + 'switch.mock_node_mock_value').attributes + + def mock_update(self): + self.hass.async_add_job(self.async_update_ha_state) + + with patch.object(zwave.node_entity.ZWaveBaseEntity, + 'maybe_schedule_update', new=mock_update): + power = MockValue( + data=23.5, node=node, index=const.INDEX_SENSOR_MULTILEVEL_POWER, + instance=13, command_class=const.COMMAND_CLASS_SENSOR_MULTILEVEL) + hass.async_add_job(mock_receivers[0], node, power) + yield from hass.async_block_till_done() + + assert hass.states.get('switch.mock_node_mock_value').attributes[ + 'power_consumption'] == 23.5 + + @asyncio.coroutine def test_network_ready(hass, mock_openzwave): """Test Node network ready event.""" From a0ddb24245cd751b3fec6de047e1536c51e4acc9 Mon Sep 17 00:00:00 2001 From: Andrey Date: Mon, 14 Aug 2017 04:16:38 +0300 Subject: [PATCH 18/93] Turn foscam verbose mode off (#8967) --- homeassistant/components/camera/foscam.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/camera/foscam.py b/homeassistant/components/camera/foscam.py index 6e9f036074a..15138e2c253 100644 --- a/homeassistant/components/camera/foscam.py +++ b/homeassistant/components/camera/foscam.py @@ -56,7 +56,7 @@ class FoscamCam(Camera): from foscam import FoscamCamera self._foscam_session = FoscamCamera(ip_address, port, self._username, - self._password) + self._password, verbose=False) def camera_image(self): """Return a still image reponse from the camera.""" From 8fcec03adf0ebb14377d729c1879990fd3400f18 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Aug 2017 21:52:36 -0700 Subject: [PATCH 19/93] Update frontend --- homeassistant/components/frontend/version.py | 2 +- .../frontend/www_static/frontend.html | 8 ++++---- .../frontend/www_static/frontend.html.gz | Bin 165967 -> 165958 bytes .../www_static/home-assistant-polymer | 2 +- .../frontend/www_static/service_worker.js | 2 +- .../frontend/www_static/service_worker.js.gz | Bin 5139 -> 5141 bytes 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/frontend/version.py b/homeassistant/components/frontend/version.py index 59b09aa4ca1..07cd39ca581 100644 --- a/homeassistant/components/frontend/version.py +++ b/homeassistant/components/frontend/version.py @@ -3,7 +3,7 @@ FINGERPRINTS = { "compatibility.js": "1686167ff210e001f063f5c606b2e74b", "core.js": "2a7d01e45187c7d4635da05065b5e54e", - "frontend.html": "fb225cfababf965f8e19a8eb5c5a2a7e", + "frontend.html": "5a2a3d6181cc820f5b3e94d1a50def74", "mdi.html": "e91f61a039ed0a9936e7ee5360da3870", "micromarkdown-js.html": "93b5ec4016f0bba585521cf4d18dec1a", "panels/ha-panel-config.html": "ec48185c79000d0cfe5bbf38c7974944", diff --git a/homeassistant/components/frontend/www_static/frontend.html b/homeassistant/components/frontend/www_static/frontend.html index 1281ef93b0d..71c4381108a 100644 --- a/homeassistant/components/frontend/www_static/frontend.html +++ b/homeassistant/components/frontend/www_static/frontend.html @@ -50,7 +50,7 @@ .input-content.is-invalid ::slotted(textarea), .input-content.is-invalid ::slotted(iron-autogrow-textarea), .input-content.is-invalid ::slotted(.paper-input-input){@apply --paper-input-container-input-invalid;}.prefix ::slotted(*){display:inline-block;@apply --paper-font-subhead;@apply --layout-flex-none;@apply --paper-input-prefix;}.suffix ::slotted(*){display:inline-block;@apply --paper-font-subhead;@apply --layout-flex-none;@apply --paper-input-suffix;}.input-content ::slotted(input){min-width:0;}.input-content ::slotted(textarea){resize:none;}.add-on-content{position:relative;}.add-on-content.is-invalid ::slotted(*){color:var(--paper-input-container-invalid-color, var(--error-color));}.add-on-content.is-highlighted ::slotted(*){color:var(--paper-input-container-focus-color, var(--primary-color));}
\ No newline at end of file + ha-automation-editor{height:100%;} \ No newline at end of file diff --git a/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz b/homeassistant/components/frontend/www_static/panels/ha-panel-config.html.gz index e794d3735977c39139d3b34360a40ad42c371c37..9c8b72d6bccbb68931d502f47b45899b3c0387f8 100644 GIT binary patch literal 26600 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 literal 20665 zcmV(>K-j+@iwFP!000021MPk5dfP^l=>PK+5H_O$)*vN0$$T@UA*~`iiFa(-6UoU= zbhP>)vMC}W0R{kd3B~U_PjepZJjto5zT?7+F19z5-HZhS-PP6Ab?@7YVKE-H9ulR1d*7q?h0>5Zm+7P@hkCauAU-8fK&;L5Pah`9(Kil1Lbkn+y`^B)cv;7skf*09EoOHIGXj-J*c^lDZ+3Y`} z$z*ivc&*kXngGHBfZ6KDIiPX7<9E|Z)QgMT&Q+8_k$#k2wkBB&ux?x8lMk?p^nw*b zfW{kX73rk&<#T9s-j0(ZOY2+7r{{=--<5c7|H(l6t8I;SD~m6NMTh#a8b*^SVIvcr z*7L6jANt^QI{7Fq3fA}j-8V~7A<_HJ`6%sOLX{-Svh-R}Tch|FLP!>HYRT~2F0zQ~ z_-zy?%s0w&@^Ny-zp>5^A}GeYbq-xf$Gi!PQ0DM|O>~^U*dngstlAqzdHzM{o}Bd3 z@nl-C1Kzg-e9v+(&VP#g%s)MKjq<>;ggm@M;yFn)X2!cMdO}zXjO-C$B=0QG8uivs{YQGPzqS2IfHg@I#r%2Q1c~Cn_oxwCA ztUTt=GFD8p#9@1`U{lBKs7H>KtKl>Pa3u>nAOPtUpgi*c<}l6T-vC%N0;pHm|6Vkb zVEy^r?*hR&0h_g45?#d?5s}fs_KQhW3`4gc<-_wd%KBUD>OU?zG1Qs$rdgh5ok0D?{kSISK|tzT{*lKFooA9ON0z(Fas-nlb15bj+OnJdg7tN(z7tg?AmZj?Jjw zGpH07(3CStq1rk0q)45f0tlqd92|@^f$_P?>*?K^6O>Y1XBLO3qu?dR`*Fmc0NTW$~ z!Ez!|rEENaasw=MCK&5Y3Z(0MC>Q~O;6knk3Q%bk(Ij-w1UE42a|^AhQ(UJl)Pra= zHSB|ELcA)#lKjJ%rU{sUTf68>#+lJ9E~LNI*6s^pthk3Hf`3AH+>blGA-lrYq)buo zs6rM5TbUS-CWW^u9C%+HWXR7lHMCft1T?1N>wcC+x8mE$46up7NpR6e7>l-gobVlY z-+0;AB<{Ee8AHB6E@pk;dtEzkgVs?{duhfDcv8TqWcxN6k4kXgMhRdlrNt=h4B`=Q z4;epa8OTZ74MxO%osDgy&EKx0E3?s`TR+i@#)fzSCt(GO58!_a3fN|FtvJ&lQMxy2 z^Y-uVuH&Q+4g(I%haw)e#uAQleRwu zOQXDo2Uw@zYK=6KicAvzRajT*?wYt+w7#$6)m&QDyIkW+g{wTirH+>2sV#8SUgyU{ zHfDKQ?HFL%1(n#`luzmfN3DIyZga2n(Ql7fa#0Ld6jNciTBN^CNyQiI3n>_zxDTZf zd;vUW#Y23kTGGk%yOmEphCZJ7<9COQLY^HViVAqf^699U_0kbIhCEojNYjfEYh7fF zB`t`H{4NDn*Bk=8kWrM>18fWs1rg$eKuhfb-#~Aazd{ViZfYCNSiio>3JC5!a6z06 zz@`)(azxC|jna$R^$_%N3w*jB>r67%y3V4><9NPDWKbP$6+tbm;PM`z=0{(%##r~D z&N-NCrvPh_qnit2)MI(xc79^cWdf$sIR543u1TMam%s?qAu)MiC+S?*5rAmB6l~A&ujwo5;-N&JwN{`^E@iKt2 zTNzwXBIamPx9YMaM&QI_n&pD_14CaJJ}i>6BT@xX1_5w7D8;{|G%|t$>Yx~+tCX={ zr*Vc#CW2=umO+^OJ*WeGrOJXFO%r+sTNi^IPO}VcYgHa)A@KFDfzqs9%`6eZ5ox7d z_$nZ%EsQwg9+Tr$a!efVB7;7KE=96Ot|MxpBw^EMXg$w*VnjK`oCA2r{hDe!Yqx)q zrPE2JT~}nP$uZlYGDh4^PMJXFJN%YE8ahT$xfmy1umLe0i9Q-~+zDEO7%$X-)DeAL zK&}-fsbYN>$q=RlA$Z^>rzAo=F8+W2=l}8?Q7OT+nAyQtWeJzx211HTvfMp{fFu~n zM-OX{xAb$R01DW4YB2OzCJ-xU7h<+M?J|feKZWa$Q{ttU*JOd0^-a z9h1Z=CRv{Thy$UU{CQ9o_IpD#)C+($MfKhwg;MY}S-6?g0R0)|K{WZpjqSjEg@go4 zZolJ00I$0cBRFx0AG=fn@o6I~?oVRRegE$0*bRgY-=b)XEWflGKE`v2r?4sfS#065 z6L_(RFUYGE*53`ouxN9K&fkkGcv0C6w-Z~vDB29d(=4C+YsQV(sq;8W9wBxV^Jm6{ z*kAPImld{&+({e6zL@6aDKtB-vNWF+f0>b7pA8})kD6|zZ#4nkV}xMeke~CtARI;w z+)=qHoDsWXqva9V%Q)|)Xh_&iFD4f$>i1jNK>)@Qh*o;6)@;NH{2h&WR@T|?_rY0m z-u~nGSQr#1rQn9lPJ~YhbdL3%;~$-?IC8#!`Sx||;MIY0Z5Mdd`4mFfBO0LkDTc~) zgh-4WVa2~=X2Sr1#uzvZG`slf+LIlPMzn5@Q#Eyr2{2EBgf<+@x##R z0YZ|7%!^&qoV#eQzQj+pq6ry85P-KJb+3~_dK^tA3<8xmul9d9bl$%E;Z-?|STJl7 z-(hRh26(agEi9RvC0dpiV1hH ztJ4XpT2ZWQVrutd!DiAa?Un4smUn6O?n&Z@0CB-xBf5*_q3%?u&e{wbNm{`QA=vdtT~9 z?aWUR=%g^AWljH>x~#7M1@OjsUS!^OumjwaegKio4x*fS{xkJ$r-VnDQ3(a4Q*ICj zybO@K9`m-&wgT62{jO*-U(jS;*Cc40m0A=Hn8UFVhWX~Eo212=cnb;#Us}+GE~}+! zsjfBlLcDF&YI3nCV5++%;%z&ACHdY01(eXd=9a)nN_>U?*Y(u+^uQ87?LnF40`tJw z$|^bKG|idwXW@7K86B4?ytp0$8t8}+9CD1#quwP;`c99kz)ST(@k7Xv&_tOtiq6?6bZLIZb>wtK3*6-kPzBr*RaHmeTAimiqL&Jg zYyljr5&2M|JDbU8f;^wE;PJg6$STN!JEG@5W zEp8AZ-p6@1t_G5}U+zfR<-SneN_y zReb)(TNo1X2e^%m=Wv9dopw^V++y#@cBr`Iey(<^|tRAJK`I1mEGlNK%X zpmiRk5KAV4*BnFfECvUZzUJ<3XPD-NTvtPDsnBcbPl3a~ zMLnz~vZ21TGFjA*AyoT%`%m!Z1~acXH=#_>P1nq<;tY8?ty)^rEtdIwP3c}x*WC1M zcL8}^E8IzdkaA-LPYO@cJf;{qW0=gmVqF-W-&znsWWrV9m0IJx=H)dzzl@9Ohsq1F z%K}^ctwmlNU?r=A5o!$q@ev}=d)5)=mj2&gShC2X1jZewul0bbq3F&}_}t{_Ozv1g zgq)2SxGKX`Zg);x;GF=$;!}SnH`V~Ef}Hd$srFZRt~S%c$_f&+6rDo^4A9I0)TLzs z8dJ$4{PRV|ZfOmazlgyUfGVdMV7??UHQ37P-MLz{J*z3EK6d9A3C@fyNanh#B&AhV zm`5Ls((4Wb-7i0zix7gRux;HrqH;Ht02c`!+iFunYuGSq}_?i+^cuBkQ4`!TnS4Jm4?) zGmOmyl=?7WF9E$fn-TZS=V}7Mh#YFX3~hpfb2f~w;uNDlQKbDIzkWl0@HgV?spW`3 zvrW^)h*#4$R2U7j&=5`!P<0-s`vp`3C!Eo8ed?ACPll@<_l&-Zco65GtrwNp989eO z?zs_+G?zIM>(R5Z>R!>>j5G_wkKi7`?)KeXp`LBSZ+Uyw=ub~fw=gM%;f>)C=&uVS zK^^~b%U;@~xL_}oVuG2nh!kCQWgv*Ai|-rUF|F8+QT0Ul(5ExG=kP04iEpfoY)+~WV6olzDOtmN*Z z4Rngzb!El!*5vbHG-jSIWY02}_Sw~Jv#YE5gq!V_C7_k!k|Oto9sTR(C!=W&e9HwK z7dsfc+%)m}{e6@8I$V2{OhDB@JkdU5gQ?66c|$JUEMJaSj^P84WqNh|QCH6&gnUB(9on zbRRDRfsuGBY})Fn2(#9TIf;^I5+|sGTHHaQM-g;x*M(RmucaI~rIcY9kI_1VrRuhD zy=HHU81-9KjQL70Gc2ln4IK1#{H2gEoLUS`mI$2I4nh=RNJoTpR)%8!Z&dZ5yz@yV zVmQ^74IaO86qi#*?V4J>f<4388AH%AV&!47>hT|HGYWJa2&H|{=sZtHAP8MN zB?Hq$=$+96cPr_blGVE<2Vt$Q=}OgPhE)*657%7+6s$spRn#q9T?+nX5=i{=_ShW4AN}Ob3KTt*8&;50JYbhe}yOZy?vyEoMHj0GF64G%n%UOSsmbTK7CVdSWEv+vHQNZD$(ha4H zv(U|okz0ajQfb>hyJ$V4nPXX-4zI~vug#+Cs2Q#Kzpgf2$G`W{OAkcD9t*=(eO)>y zine`P{)($VNFuuBEr+6Ku6^*51&>;8IIL-vl&0m=Hs&@DIJsVpXd9KzaUwZXiVz~9 z4C0**N6Za)#!yh&#hfJs^SShJO4I__xTNR|`Z^ggMQOf#<%_mW z|N3RJve!Q_z%vu;Vxs|g9f!aa{4FgD3XDp)oEIHdTFi)TU9jRnER{M!9{3T_hcYWV zQMd>ZSkd$Ol1vtYnatU@xY5FU2z(oMP=s$GdT9?vX_|Ro{yX??#h;#UgzQhxH#duC z&+Ik8I2Y>8+=Xtt+bN#8KnB{@vU+0IgeW#zt`}QTrJ9`YqA-e!vVDA=oW85-4Qhes zUqm5Lo6kvYDlxYuaE-i0wf}W;_&Q4yOi7$a*-G*!AT^2{)cQ8fj>NL8T5-dpe_+>dG16vz>}pl7 zBLDA;f}TMd;3G|&frcOvFy25@Y4Tz6cXn$H9|uMS-)9WEgc}Y5)}CY(>%HWQAUy%F z4qK0dewkh;3xJ~%zaQW?;9!*jFA&YwApLD!%^VqU(WYoc^W}~HfPUOPT9x(g(X6Gl z#b1<#GZe;j5V{f-Fn0Wvb96gCPe+@ZZcY#74|v#B78Pl>SF^ZClw|YTl}7-xxjBZ* zH~EA}nt|n2?>FQ#jB;9}SZz<@)=>?1Hw^xEaWy>V<9dbLmG2#fI}X&Z#7l5C6N!&FV%M7O2kfW49Gn6QX!g~=YH83Z%5?l`?zUQXFi~Nq8 z)1!s}Z2@RdetwW`ZpM4*Nh&aG?VLUfqfQEg{%S7`PomRKD$vF4RvGODo`!fBh1|p9 zOxsD?r`&Ov1XD$&FohpFQ66%Pi#<-EP61yp*=^-Dz6=xj;sw4iTpW{7|)=y$-jH=j5vr`)F48yXfbvaM1lD?^VUMIUl7xjfn}em%%;k3z z&38;acDY6|S`%yj--ElBqlcl0m->>4Vq~J!=U)(^llT+`4*rBG{G&F9B^tLW?>Bdq zlD$6YeN;PbKA$aOyTc=ormH(!Ti4gu?dvbwX?C&oe0zI)E5EvM10Z;PMRc{)9=IBk zvY^nYOpr`qg(>}aNU~%8YG-VmUaw1J(r5rONGI zo+HzTDaeOQjk+n#K_XaB4P?6KPCea5RU;|dX7mWOlWU{)C0V>>)c$X7N_0MoN z_x+Ou_1ABH5YWC(h+|1jZPM=?C1#r^2SDh%LuSM=5fvE4OF} zu0oanzJajz@_%>sBw2VH5b^8~KTY>4VC#i^=Wl=e+ZF+HgA!#mxlGhLHFh7zeM(!1 ziu*Sgq}ZUf{49ZYJ|{THl9YNbO4K6stcLdl zv^{j?6wE4x2x!&m0jj_V8OmtrX`?R9DOk{-r3HP-6Gv~sG!qp43qS>|txVR-xGxIT zyfm%LK&!tX@@HQ>dj`DuYsmR1HvGD{4RH|5j(+@3Xy@P`q7-P_4euMylRlZAEWA)) zBc;OuT4rcugb7X<1{C09aK)VYL96uzVDJyz_YN-M5VJ~|M)$(h)}*Px3XH;EX{$DI zsnFP{tSi2NucLrE@4>bh0LSteP;u{ZlS;|44Qk4G0V=8kE^Nt`Az%at%pEYQu)su- z{Bcr5U>AdP=q|?aeh{5}bBf2;MA*}s5XNv{L?>?~sVJrJYv`h}wfq-gbYM0XUnG3# z3aJ?Nb#wFA04M-LkKT>&s*PsQ>xb@6z3QjnSLmzHS-HzFU62>ltF9ke<86KewR8{C z%#jHu5NT*YK^`KtFrGl7fo=`@q4tvmJ8t}AbF)v5Z`c<>fHt>^tbH;hoX;l)*Gx>V z;fyzF;|VZF@j0%Q+?$w;!l8J^J9-7|0vPmF3?`bvXbSq+CpQ0we4V!&jH4fnp>$$+ zg#iTQzlNhA(sFZEmYYnvi%8xaMCjx+n1;9;KHLU!Z3n$D6Nm*v_&EUgE)g#}+rgFe z^>5|FSUwy`y#-u{r0OZJ;w;?mp1tUG&z?QwNL+^5$=PY}F1&<+3a-P!=H|P-OGDh=pf-wkI!~a z_a+{`Lwg!0Im?P~7^)F~imy4pf_6YU=8A?#XdkA&L6swbdhX%h03veoDZBj_;jpU< zL8CA%t=6fJMGvr?X6bV?_AnK7x7d5{6~G9>JOOWkHi3UZiwyLWaMj6CN~DZ4QXCjd z1h?>y(H#CHg$8j)8=M89&{$UnXFLOlc}AfFxOWYVJsaA9;6w0os@gs3Yp|2 z739PdE$|tGl{CO-`bQ4=ufDFI_^#A+sO{04iA_HmUhsCH6$AlPb}8j5OUtY~d3p?r zbg(JE+BjB+rghH_bfju!(%P8y?|T0Q&J&0x{sC>{`!e640i4;S`7dysuG% zor5DIA%DP_Vya@#i+P%ca%;g6o!jBX1Q&Qhd_AJCzLoD|K`dZ^76^1;9ECDV)QvEY zh0z*BLZw_GCh5rnd;|VwNCo7tASERPo^=!er*lD+PLb~YrJV-hUZl|oZGF(i-wBmY zejBA$X!3!F7UBnb4EXa5*NmfsKLAM*c|H2czfwL5b(r}T^HJac;flVyyJ*kwBY%oV zqpEkQ-g>ARtk*xV9*eISWgjYa%#ScCu^*sYNS>Kks=&P_5UvCMo4RQH)+MWAd5cC6#Eq_B2 z?B>?A3O$Ggk zj03Q^qppSzG${xCh64_+;nxRXb_`{pCzr&9Jn{yH3ZPv?*SoP;P|gKhfe8fUKTrw$ zU$x9(IPh+Q185C%6usaI+%F?~pmr(ixw&}=3<-ZO;m<9n*?wr2?e6n3`!qXZg&l{% zl^^_q&Se<`2u7~3qulBFWpner_Y00Pb~8K-4l!+`1N&D%pYKA}{Vx2jLb|T+y$Zkc zMa}M^+0v`vupue^${gr9;;;cSo$meIo+kM)9$A5`iTwCD))V^C#df)nOpTp1z1>rcYCBv~4ak?cSS8^DmU(Yn9q zy#r??55A)gfNL1jSYze@8l6Iw_c-MQs!H&73?gV?nWIyKic>oqG_$XSlLfLSZ|H;F z5Keqy>LtUeuo^zjI3EYbk+>t43c$;0to!}JasW=(=@9dUm7UkrVF}+DAb0X}Qn7G)-6viw3=S{dBdIz(IC(pXuq;q*f{t^x%zbgrq5-1n>|u zWB6x=Tz16pF>CmC%;72z<$MlI1Edmdm`^P8d7fT7AwKa%Nn_(Fm`^aavA}{mk_U+x>$*IVUfvb}8vy z?B38BhGn#j?>8Tpy<<7+T--T#=>8d6>cCimxk}{XG;zU^q&4<5jL-`` zX$-%1=JGTYzWrIKfFuyT?xb@v5VbxRuCC?iHs;Y5;ZI+>we6Qkn<#=xw9s;>);Z5q zo6-qf#t(FX7)H`73TNXu`H40jez^@ciJn~Ccj(UbVjs~SPT`3oEkoBh6BG4GkjNFt z=!Qd#7P$dGfv1xSlpy@Ubt#=FfPtk~YK1aCp;gipJB4eT(F`-z#PDT)XZPlJ&FxHc z>LF)P7Pv3~7Z*TTj8FKQ;;4wki8^8Rm8gd?TO(ta>(nvGN)CQ02x$vkgBi7O1_~%!wvjhq$=PItUE0#%1ofq8HWoqdiznmtVX-qn<8yHjrjmy>OM`l1;z)!b{ z;R!9oVdYn=a9Fv{6LDBaDPXxj;$F_wlStfWlRY_R7L_Eo)gDc5dSVmmP(z;&2-$%Bt=}@p2dCePl`4WJ-rSx zc#6)`E4H5HAxO^azA`)#^zuP!1iFuCNPrRl!GUG#1N#PhCxM;w9~@Y;08<5psZVAwZCX`ClCH?OL=$|V{@{b%sN(4OcgfM3ePhyG#Km~6Br!3B zj*ra3_t0VA0yYxffSy<9xr%Ppigg8{$~HF<5}xDD&zJf(%{q=7lwzGs#yTe3#f#Yv zJA!|UR54iec|{NZlD-|uL^>EibgP4iCv^4VPvPoKlPt~|xo+aoUtR?$B5L51-z;PA zhtK6KzoiKypJ}0`_;}9J&w!7F_ZvO0 z5;{DBC5t|RC8I+V2}^odX2nJfFB^g4Z|K2t*QSfzH6*3Vxv7Rp+$Ky~CB_1pZ0N)+ zur4R>0FR7+Es74_CyDcPaqoBpI@+sOA=b5hExJ?0h;De(B6;m5X)8)% zaT>2n$MGvT;yOSS;86+;UAekY9kbKPP;%+)X8uaZsxg+xpY3rp@#>AIKI>WZhWT@! z9LNYX4xI*61cGd)%8Q{?$0CFKA<3r`yaa{y9W{IqkZB4AR=`#Y)Y6(jE$Dr8!}^j| zNrn1x71@zxD_n>dwgA6oV_c$;(beD1HJ+?qqz+$?wdeXPN(EoSww&$DgNp}tYgVNJ zo6-854nJZYxz#n;kGNN9=PIWL?l8^b-|%oWaSS6E6Mm4m^+|*j9k=y0d{75Bny-yk zjXMC)*5OSTbX0ghEpl~(5^%mP5z^~1jqXzMFnzPVp1Nr!Sv zE6oQTYw*$cx#ZN)*lfDba9Z48^5I8j@~Gbz+9))CXhxTM(y%4gi&aEI!>tht-j^Cf zAug^o4FFyCj-Ej|X#-uVlN+XG#KyX2LOZce1FJ84~SryIg*)cR#GY<6(^u(-sn<7zqeHoryUMuJn;Kr1klu__)e6 z2Cty9KYVxog7l9|XNtBM9xB>WSA3HTzc@2wHnd7=$mEMpJi)<<;-pRU92U`+nYTd2 z1!_5SRL#lJ7^_j)$6pT|l(cS8jAm9D6GON^@sJQjnn%PGk!9+-WMPEboSAba zhG+he$&v#J<`YbpoIQBDM32ep6DGP2&6sHVuRUe*?vE*xKc-B6XHzDZ0C(}(G_P)Ds6u*^CcRw1v4h^ zj2RP6Gi%PIOqVC0HBmz^=1mOG))Il6Pw#X`J(Ww$S#||OZEt9-zFdXSC<2{jrw)nF1kOj&YX1|O~clJ6HVxFECQ^I%O5uf6QXe@NV@*h9L}rsJ&H z;*y^cAvBG#@-z;FA~Ny&;_o}UnB(S1&uC&q5fhq zoj~H3ro9)nJI@b4D;5NIM`Vgbsp_lM0!X{gqpwc?Pkt$naaLMj9}?o9uOwE-HOa_(f>bps@`=lTDdry#i z9IcRhe0jgzW2SSDoLFll9))6FKhOB;k38cadB)#ao^ijNXMFSV^Nd{igvPUxVdT=P z?Sl1t#oXe4Z`H&iztdG^6?LG#Je_DEwqh>PZtfE%5;-E2Mie+L$RWNma)?6vS_wpp z5>K8z152VXp$iz!?g`r-0SJq0@o zr4%>CXTzw)V;S)6gm#^|ao)OyCfrU+Vw+E*L>ayzi~6)XHufvu;-tvZ{*=Q3N&b*C zM*uG$W`v5JOadm-K+_vWRFpaK6XU*q2+2o$DCF(JMUh689`12*p)UI7ZBF z1Nf)U&&|dHA}7fmay^0=^ba#O=+Nz$d1q@22fn3tF}LG%tDpArE#1*Af!r3v%A{;M z?%DY$N-o7NYvAKCu9c0>F{qcFZM%FFhfHgZ@)SUMAJIccUB%?9jJqyOBM)OMmy|ja z7=-%q7Bo)qENHyJ9>*Kc|HU&Pv9!1|GfT(LkgiHY&bc14MA8L}hGVomO4CaRT;E$K zkH_%{RZOb+1#C0nMg{uufX-Sj+Rnb>7h((o0(?to$vC*Q54P@HjU~H$O9Y*^>m9= z6I;z0Bfd2%_NEzj4?ZGlw9@zf?92VFd`rv8YV51aO9sBSW?C{lA-TlH)v4WwGV;F6 zaxPU~O41GeRi`}qsbIsQX#|k$nlT?t?3E}i(3RwUgXeEk{m!UF*OJF;CjG9#@dBRQ zS3P+JBamk1Y31he`oDOI_kj-8N1uBhi0V^*o5*{*`Jr^u`7fRk(U|v11o#;bj0?zD+z)Z&HH}-0P;(ui8!IMJF$`Tpxw^YAWHxg51sz8WV%r-{`;6f5 zTfleQ49{23XA^MELA*N8wBWe(U|d^Y zZ*TkEd0Uuj38QtMg6BWxpyt}zW28L6Was%P?Oj4Wsg8pG<Ro}h4srfi-%(X6fk z37Z2PMCTgeL43pdT`?#`4~{cEYP1E>M7h>qarBBTN(2<^v(x@EH`+Q8%I9<@hyE+o z3OyP{=`NeoncdoUcMd#{9J%`fj7*CwtQL$73idF{TQJD!2=BH8#=F4ugX@|_9U_!e zbPjb{6)DnmbRK29FDlt02cJXA6-6Ki*R2s76fT&FRuN4?7f-G~Mx)mUNL!WKAfEA+ zGmene;$pXz(#od0gX5`Wb}>?9=y_Y69~6* zJQT+fbU30Pl8pFC(?ydKTHG~>V~x6n_LkOzpv$;!e4mbP$1L+^F@y}YiZPh*TRviz zcV7^~b1Zl+hI_eBw3uZQ21I?LWo&09QO=oaO08 zeGCeTHyT^wpH(9QJpgG~KIz~9F=P^?`ikd4C}9(r*GfQPL2Fh-y zd;I2lb>ns$q&dX!-+Xg-r^-{xYkv}Z?gj2%b2+|d5GLNZ&q_f2Hn?<&XHw!s9pYy0 zXHj$Kr?|=b6^w_j6sx4|NqpMH+vE1LEV^ySIsNq*-n)a}LcCOL?psvb32I1Op`kxF zxWw;(Dm7^zSh-&B-X!}-?AL=L!mzsk5XF3H#3~|{sL46LvDm9)% z7$)YX^714VotU=g`geB#5ft)m8ym!i%g8i~Z`k0F^WpfSjfO0k8A{XWc{Q5S5-wF3 zec{jZprRCP%T=C=5OV_t@9eqmvzK^b48*mb-zhPmEnnRl83mthnUI>CR~Auo>@<^V z^ni=yNQnDI`6VnoE~A;(t!mq7O|fMmoWKsubHQWgwpK3I@GUtEEa{F`t4E3F6f-8`EABuR?7B#3lWUp|dWpN-SLTKI5)dQ(r+ufMp zCgx1WBXzFGY{-KtyltlB>Smy+m_z@fheZ$QVF4Nt27r^r>EAK&#I;kQJDZ(RXqIlRQ)I!df0IAY0e05E-+o%0 zM5h(M(wvl%u&|(Y4{8;m+|t_oMy?Z8$y~oCPUy+~s_UrkqnBmCL+l!Q*#-O>%)rYO zgK{szEBxX<-mDoJUEADD`9*XQI-DtAKWCCq`mCPi5tEY#p0(a`4dyp}lV0O3cRBO? zAPT*-rbgOWL(M_Ii1u1L9kv_o?R4;>v!KEj9UDPWF$H{<**W1-w)%{hDO>HB1U6&RXKnRE2Ca9e6flp>!!soy8FvEWn-1=p#eCUb^#iO z?z^9Va^3*qR#s`qMiWY3!s|(f+yMQ9Qj}i87b1gW=CF*VA+BsnB;2wHZ|nr)sj@>E z8-Ph2Qo}#+XPF?9X*Q7~(m1bY;N*ZT%Lj>$+366To53LuG^Uk!MN|#lHz^U*ffo+6 z*>1PppvKYv?$t5)`J5}DC}LN3=F<}B)OL~=aUj73@1eL(v&+wt58Eb>w6CL#rWMf> z0SYO-Aky@UXCI360^bmyVp_5xQ?K*h+ucP8^m?^a>KzAgk+IwE{A3vS`z$$CMG`!O zW3ickg>ytN$bGG8fEuo${ke}b z)j9We_igWYnbTjwtZCkM9tbC^;`)~Wgd25lS8>W>dm0-gnO&`=?PCEpU!aZ2hRab`W@QMc9BK5hMQcw2BOY)CQtIUL8p~tPk|B! zpE?Xp(m1)q2c7Ch+F%3w^ahjNn1`tkN*mM0bzrKcGd5unC<$BvL?R}4<6$rNiz4G0 zsWo4$d#0|gNSy{bC0Ke&F2xhlM zg~RFioMl1+TRSUNC5o91&I5ZCN{c=C=gKrHPoa_=Qx`TjTRR(JXl>2mUrxjZd5waZ zL=7nIY2`cX6P+R<$)33#$9-0qbK#Zlg(73=&sR_?M7{j;Oab&6seV2eGy4xZ@x+0M zoUi|B>D_>SxAKuM&=Y_Bu3ULYN{rBwZ;VA~<>iT75vu&wY$YJ~Raigrfv~Ds1W=W& zjlT}XdQZ61{RMHl;gs# zvn$Rv%FCj8V}+4I=SUy$;Z=G>*!@88I2Sj&k5w!37&b=eu%)Z{%Eo5SZAa-{qovWG zsF(Ft2rLG2EK3;L85hw|R6vs8sTK0vnFIcN9$3H2RtC?JD57}O&Zj-x5oB%uG!`?N zZf9F$qPbG>3d>0N^WbatrLQLnExD}Q?alZmzcw-bGuQE6C%6p1$IuwbkFj|7%niW% z!X?AjM|knf#W~L!Zm(;TY#SdRuy!T_3PeeyYoKgsLwtAF+6l$FCzBfDJyY3xPP3W~ zcwniQBxTa3{Ye-Dg+h`(dwKbf_oCz{?R`ru4-NbD*vZi2OjI z^#gx+L7(0WqJh9+NPBT4t?&IjB7#?>xUZ_n($VAjM#r?%2*I!DAQ6L?!?fiN5|#X@ z9+K`^#ZA(mD!$ShV?wv(Jl;qUxD^M1i}>23KutXEP^`A5OpgkoP#F+SrFG(~B)3+cO+l%wG<;{*Dk&TQTav~4)i71KZ|!PTxK(ki&$VyYtiCK&tN{P4 zqB25iX;fArMuYFo#Acq-ut?{-!~|kdr)s4%D&=nS7JS)U*GO-2-q*1iv~Ow&bu>|N zcXtBLSi;;>)oO{yR%=y6NLM1Jt;4}=UoD=L3t9JyP6d&rq3_at1kA^Pm}D8l-D=DN zpGzNpowDo}FQ2(!R!CfRJnDmSh;4Wtm^FmORX=W}*q9k0ET`k4k#&Q8Fi+DeYT`*g zx)MxNGY<-;nPzdJJ!63fDeMdRN-J`j^ zP~veW4uFTxILeK20LRn}1dKisx<{;Vc&Rmf{rjaH__@7!uo75nypQnFvJV{cdkJ{O zJOpRLwet|A#y(ndD9%eQR}vAW?}78@H_+yvACe{#V|G=Y)3n87HUoj{OBD2+KvnkVb!JM%Xf{d=1hkw?sP`JQWBtH5 zV9D@n&jtv=41GksRs9%?jjon9d~)(Z?CN|&D(w%E_*5dXDD@~cGGX^UF5!cZ<^1B) zF@PKUS8N+-I1Td2H%fMbkG8jp2TmGSWKCXclxibt$X*zN}KR<4%*y1Z=-fqgKMt8Vnz}0tHu<1og^mVJ=(j z9xu$}mvnjweUnnw+`)I~t~k*Z$PC^iVP&rusHa*)R$8Z1R=cGSniTOBh+<(-k+b8 z2WFl@b+V{gzPO3=VbN?p8ndb@d0Jx_!Fl)>nv=rq0!tM^D&n!SZdIML2k%R4gc?T!LnV&nOHF8V9(Z01p$r71+W( z&Zc-3)wj_o2Nf1H;=k80LL5M+=_`jTrTfwGUqWtv;$A}_*f7myN@ATbpoY4;+t?^d zovABp80SSUEuZUNb5;6Ky2qcZitM?nNX=D6#$1(sGUQ@KxsVO~bU*Q&uJVat;1=1|7;q-!6ew@%?5k`tPWOR6FL>e*I$t+8_GZ89Dr=IlG~CW z?vWP$Act{j?pHQqW`FsT4lE@p34X#GmZ<#ic0;Q7(XwhNXQiZ?6#B+wnx)$tNpRGn5ubecJ6x`J(hrF4H@PYHP^wR8>(^`hnGZM5>APe4z$n zS7YpQ;zL%jcL?V?@}<&>bF^EB42ri}j6GjmRcNzO*i?~gMxfMTbm-;&UM)JpYj!&e zzUDf|Q6uf9SDPy>P_!gKUhT&< zylt=?{P0XU)3c^;z2NqlHGJgyWlebKLCct(t-_F3&o1|}#re>Gqw_c9iE^FNk#b$@ z)Vwv_`d_Ab5f5V4_q%iM|1CeTuIf%{|NVZR`N;`A&?(2}B)7aomo{$Dh{tL%Gx?tP z_uO9*|E%b@%cei^b}oas#AVn)@aBlR3p{z_)Y$i$FFgctB+zrKzzr5&8T?`Tk_J65 z0Y6L~NkV6UXT5AVotC%a0t^HG(S)vfFQd*$mh+G5#1&upD7|ZauI^td zttT)KFEa4@7C_iYk($Cc>N*|s-xW`$F>lJ@{A!EDN&|be|KqFjAvFuA4Ymc3C_?jS z(AGY3QXYe+5ajoc-y6l3uim^mt{_<*i~An^R52HeBLKtQ#&6oi<~NMi&C7DO$D5Z0 zn>VZ~ke@Y7gWipo1KB!jX^g=8s+&;6u0=gbTU=A;ZtXQh;17ZaQGcBdLVh(0`)O~A z$CUC*CD^MG9a?H8F5Bu5X?a}))g*r=RGv*BZtuX4ZMs#me=v+k!e8K3M18A|wm-qC z@6z*usW+;JrUHY${LK!F+uOzswi||U9}NEW!U9OWKB7;!v5pw^dhi|Lv~Wb z0;l?f^#CHL_5g;6&ZzL}m_QeXe#GE?Qe>0t)c_8+J4rd@<6p}23Zi`C&)FzvPA#mi z{-h)|w8hfmc8jX=Kq^T(G2Jd?Mc%SxmH>~v%Sl&mQnF8%)0AR&bDZ!yM;b$f@*Oj= z2}-iW9FL<4wEe2pAfk?7S7WZ+YFz{IYA(2GYJ71JL5_}y2`aa_a8I6ggWM}ut^$P4 zUne(?xN%a$SBs$UZzfx7S05UIo`H!M?1zd!oho^vsyQGhg&q)}Yd+atL8zs%_J=smmFOIa)4cyl-d5jeq9N zB<0)w;cn$%kdZUtrncE{+cXgH|AGYeH1 zb^%G=zdKq^6Ajw%DvFi21Iq1ZOH^Ft4)2DGt8kgReYx!}zdXP?LPM0WF7X!S9N}Cz zB8T8a4wS`9=mn;2=Xl6+CJ)ld`L*5@tOH1X<=gwyvt z&3*9UN4OEL3UtG|m5JwRHaA_7@y5%Z%mM(3PD?HK3SWM6iydF}qCm0D;!_>Uy!I~I z|JjR29_t#G$G#rsbG)Yf>>l@&3**3Vp2?P9QC`)U^>-JR^gTeS)@)c2WS!eycaKpc zY+j%)=elsYk-UWmR|`G#3LgFiSC8KN7M?W^zsnzQantYdu)Er0?(~NGCD(o8Kot@{C8(g}B4LFH7l<+EM*AC)OF9h2!JdJ(%w8+vfJ=umrF{v$X z+-X=bbl9+3NZ}{$c6;i(flO4(H&Ccied4>22H+F3{)6eo&rKs(ALT|R3++!pJDP}m zEaDNck3_v1#6LRuz)?wx2U6`l!gyL@nP{$0qtzx=Yffu91bs}pI9Yg>J9d4wqugc5 z8a@0J2e~gD*P3J8trKv=tG+0#vf?WVmI{||hN?u?2C&BtE6htc zDw452-j2A$z`R*T^@YEAnjY}e{vM+T;)q1^)N$1V{^n_VV4QgJ7~Rk*_nwh#uQat< zUuKvyN62dFGpT~oj*R*pbj$mKgs#jbXZr|o!%|3}wyJq&x zN(rt#4{4>Et1}W}YCudy$VrIm;Wm|d2Y%GEoOD?0h#Rx;y-&JXb9TfM05wa}c#GS5 z%jWXViQC}!(A?lcX9G`Xs4-rP`*l>UPMo__gHsmV=GBS9 zrD;A(#$>m;9XQtJgZlZP!q~1O=7Wm5`5=8%F{_&oVtVox;1|82$Mw!h`_%PUnG|ZC z-r_6NYR*@#Jx80CJLciK+fBNUKZB;rU8Ql~!6kNe2U>4#di5kVr`XH_NbD*YzT;+N z;W(@|5H%X8x~--x72x~^i;lxkTfbL2_@YvX4i;8#pJt1^wZ$Eu&(iZ>j#Cn7ue6CK zc*3iztgELUh{r>Yx3-R_UKI~ZvxOUCz>>6x2e3TN_0Z^D$e6`T0Ehn<9?J6&r@8;?8JB zzjZaY=uxj(0kgb$4byC&Tn)2nk$heoJk>KH_tMM)jKA*z7po!IJa4fq!!hq=j9Jt& z2A7J2?tA!13&`np8zhG>Na9ZSG~=YDc1mz1XA&Q-omjDSL}Owlq19C?utmlAmL*g5 zg2uUN2O;MIog&fVARs)s(@KKjHI?!tzJX(a;y>t)EUUD6z(X1s zQTJZjDJ1rF;g*ZrzgQcWKtfC?A{_B9V}tkx0xqe>#nd=CFCHBWLI22FTUu)6ZoxQX k9-di4jL_-{YL-t4*g8-9x5y>Mc(nWf0re4-9)N!V0ES>1Z2$lO diff --git a/homeassistant/components/frontend/www_static/service_worker.js b/homeassistant/components/frontend/www_static/service_worker.js index 21824a12326..a5544a8b165 100644 --- a/homeassistant/components/frontend/www_static/service_worker.js +++ b/homeassistant/components/frontend/www_static/service_worker.js @@ -37,7 +37,7 @@ /* eslint-disable indent, no-unused-vars, no-multiple-empty-lines, max-nested-callbacks, space-before-function-paren, quotes, comma-spacing */ 'use strict'; -var precacheConfig = [["/","07ae53d16e9e97de8c721f5032cf79bf"],["/frontend/panels/dev-event-d409e7ab537d9fe629126d122345279c.html","936814991f2a5e23d61d29f0d40f81b8"],["/frontend/panels/dev-info-b0e55eb657fd75f21aba2426ac0cedc0.html","1fa953b0224470f70d4e87bbe4dff191"],["/frontend/panels/dev-mqtt-94b222b013a98583842de3e72d5888c6.html","dc3ddfac58397feda97317358f0aecbb"],["/frontend/panels/dev-service-422b2c181ee0713fa31d45a64e605baf.html","ae7d26b1c8c3309fd3c65944f89ea03f"],["/frontend/panels/dev-state-7948d3dba058f31517d880df8ed0e857.html","ff8156bb1a52490fcc07466556fce0e1"],["/frontend/panels/dev-template-f47b6910d8e4880e22cc508ca452f9b6.html","9aa0675e01373c6bc2737438bb84a9ec"],["/frontend/panels/map-c2544fff3eedb487d44105cf94b335ec.html","113c5bf9a68a74c62e50cd354034e78b"],["/static/compatibility-1686167ff210e001f063f5c606b2e74b.js","6ee7b5e2dd82b510c3bd92f7e215988e"],["/static/core-2a7d01e45187c7d4635da05065b5e54e.js","90a0a8a6a6dd0ca41b16f40e7d23924d"],["/static/frontend-5a2a3d6181cc820f5b3e94d1a50def74.html","6cd425233aeb180178dccae238533d65"],["/static/mdi-e91f61a039ed0a9936e7ee5360da3870.html","5e587bc82719b740a4f0798722a83aee"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"]]; +var precacheConfig = [["/","535d629ec4d3936dba0ca4ca84dabeb2"],["/frontend/panels/dev-event-d409e7ab537d9fe629126d122345279c.html","936814991f2a5e23d61d29f0d40f81b8"],["/frontend/panels/dev-info-b0e55eb657fd75f21aba2426ac0cedc0.html","1fa953b0224470f70d4e87bbe4dff191"],["/frontend/panels/dev-mqtt-94b222b013a98583842de3e72d5888c6.html","dc3ddfac58397feda97317358f0aecbb"],["/frontend/panels/dev-service-422b2c181ee0713fa31d45a64e605baf.html","ae7d26b1c8c3309fd3c65944f89ea03f"],["/frontend/panels/dev-state-7948d3dba058f31517d880df8ed0e857.html","ff8156bb1a52490fcc07466556fce0e1"],["/frontend/panels/dev-template-f47b6910d8e4880e22cc508ca452f9b6.html","9aa0675e01373c6bc2737438bb84a9ec"],["/frontend/panels/map-c2544fff3eedb487d44105cf94b335ec.html","113c5bf9a68a74c62e50cd354034e78b"],["/static/compatibility-1686167ff210e001f063f5c606b2e74b.js","6ee7b5e2dd82b510c3bd92f7e215988e"],["/static/core-2a7d01e45187c7d4635da05065b5e54e.js","90a0a8a6a6dd0ca41b16f40e7d23924d"],["/static/frontend-6c8192a4393c9e83516dc8177b75c23d.html","56d5bfe9e11a8b81a686f20aeae3c359"],["/static/mdi-e91f61a039ed0a9936e7ee5360da3870.html","5e587bc82719b740a4f0798722a83aee"],["static/fonts/roboto/Roboto-Bold.ttf","d329cc8b34667f114a95422aaad1b063"],["static/fonts/roboto/Roboto-Light.ttf","7b5fb88f12bec8143f00e21bc3222124"],["static/fonts/roboto/Roboto-Medium.ttf","fe13e4170719c2fc586501e777bde143"],["static/fonts/roboto/Roboto-Regular.ttf","ac3f799d5bbaf5196fab15ab8de8431c"],["static/icons/favicon-192x192.png","419903b8422586a7e28021bbe9011175"],["static/icons/favicon.ico","04235bda7843ec2fceb1cbe2bc696cf4"],["static/images/card_media_player_bg.png","a34281d1c1835d338a642e90930e61aa"]]; var cacheName = 'sw-precache-v3--' + (self.registration ? self.registration.scope : ''); diff --git a/homeassistant/components/frontend/www_static/service_worker.js.gz b/homeassistant/components/frontend/www_static/service_worker.js.gz index 980bc5d26a82fa3ea0c79973de2f85b2116ab20e..97665c7f31e22c8c6a05b60599ba2bd3ff53391e 100644 GIT binary patch literal 5137 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 Date: Wed, 16 Aug 2017 07:04:57 +0100 Subject: [PATCH 29/93] MQTT Switch - Add configurable availability payload (#8934) * Add configurable availabilty payload * Fix * Fix * Lint fixes * Fix tests * Fix tests * Move from const.py to mqtt switch * New test * Fix flake* --- homeassistant/components/switch/mqtt.py | 20 ++++++++--- tests/components/switch/test_mqtt.py | 48 ++++++++++++++++++++++++- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 95f9a779327..308cce4de46 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -24,17 +24,25 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['mqtt'] +CONF_PAYLOAD_AVAILABLE = 'payload_available' +CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' + DEFAULT_NAME = 'MQTT Switch' DEFAULT_PAYLOAD_ON = 'ON' DEFAULT_PAYLOAD_OFF = 'OFF' DEFAULT_OPTIMISTIC = False +DEFAULT_PAYLOAD_AVAILABLE = 'ON' +DEFAULT_PAYLOAD_NOT_AVAILABLE = 'OFF' PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, - vol.Optional(CONF_AVAILABILITY_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_PAYLOAD_AVAILABLE, + default=DEFAULT_PAYLOAD_AVAILABLE): cv.string, + vol.Optional(CONF_PAYLOAD_NOT_AVAILABLE, + default=DEFAULT_PAYLOAD_NOT_AVAILABLE): cv.string, }) @@ -58,6 +66,8 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_OFF), config.get(CONF_OPTIMISTIC), + config.get(CONF_PAYLOAD_AVAILABLE), + config.get(CONF_PAYLOAD_NOT_AVAILABLE), value_template, )]) @@ -67,7 +77,7 @@ class MqttSwitch(SwitchDevice): def __init__(self, name, state_topic, command_topic, availability_topic, qos, retain, payload_on, payload_off, optimistic, - value_template): + payload_available, payload_not_available, value_template): """Initialize the MQTT switch.""" self._state = False self._name = name @@ -81,6 +91,8 @@ class MqttSwitch(SwitchDevice): self._payload_off = payload_off self._optimistic = optimistic self._template = value_template + self._payload_available = payload_available + self._payload_not_available = payload_not_available @asyncio.coroutine def async_added_to_hass(self): @@ -104,9 +116,9 @@ class MqttSwitch(SwitchDevice): @callback def availability_message_received(topic, payload, qos): """Handle new MQTT availability messages.""" - if payload == self._payload_on: + if payload == self._payload_available: self._available = True - elif payload == self._payload_off: + elif payload == self._payload_not_available: self._available = False self.hass.async_add_job(self.async_update_ha_state()) diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index 133978a7bd8..cc97fe1c9c3 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -122,7 +122,9 @@ class TestSensorMQTT(unittest.TestCase): 'command_topic': 'command-topic', 'availability_topic': 'availability_topic', 'payload_on': 1, - 'payload_off': 0 + 'payload_off': 0, + 'payload_available': 1, + 'payload_not_available': 0 } }) @@ -153,3 +155,47 @@ class TestSensorMQTT(unittest.TestCase): state = self.hass.states.get('switch.test') self.assertEqual(STATE_ON, state.state) + + def test_custom_availability_payload(self): + """Test the availability payload.""" + assert setup_component(self.hass, switch.DOMAIN, { + switch.DOMAIN: { + 'platform': 'mqtt', + 'name': 'test', + 'state_topic': 'state-topic', + 'command_topic': 'command-topic', + 'availability_topic': 'availability_topic', + 'payload_on': 1, + 'payload_off': 0, + 'payload_available': 'online', + 'payload_not_available': 'offline' + } + }) + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability_topic', 'online') + self.hass.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_OFF, state.state) + self.assertFalse(state.attributes.get(ATTR_ASSUMED_STATE)) + + fire_mqtt_message(self.hass, 'availability_topic', 'offline') + self.hass.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'state-topic', '1') + self.hass.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_UNAVAILABLE, state.state) + + fire_mqtt_message(self.hass, 'availability_topic', 'online') + self.hass.block_till_done() + + state = self.hass.states.get('switch.test') + self.assertEqual(STATE_ON, state.state) From e7ce110dc6aa2ac48b49b50c54d4cf098ddc4604 Mon Sep 17 00:00:00 2001 From: mjj4791 Date: Wed, 16 Aug 2017 08:07:04 +0200 Subject: [PATCH 30/93] Buienradar newconditions (#8897) * new monitored conditions and support for new weathercard * new monitored conditions and support for new weathercard * minor changes --- homeassistant/components/sensor/buienradar.py | 177 +++++++++++++++--- .../components/weather/buienradar.py | 100 ++++++++-- requirements_all.txt | 2 +- 3 files changed, 241 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/sensor/buienradar.py b/homeassistant/components/sensor/buienradar.py index 755d88bb443..8961fa1dc74 100755 --- a/homeassistant/components/sensor/buienradar.py +++ b/homeassistant/components/sensor/buienradar.py @@ -23,12 +23,14 @@ from homeassistant.helpers.event import ( async_track_point_in_utc_time) from homeassistant.util import dt as dt_util -REQUIREMENTS = ['buienradar==0.8'] +REQUIREMENTS = ['buienradar==0.9'] _LOGGER = logging.getLogger(__name__) MEASURED_LABEL = 'Measured' TIMEFRAME_LABEL = 'Timeframe' +SYMBOL = 'symbol' + # Schedule next call after (minutes): SCHEDULE_OK = 10 # When an error occurred, new call after (minutes): @@ -38,6 +40,10 @@ SCHEDULE_NOK = 2 # Key: ['label', unit, icon] SENSOR_TYPES = { 'stationname': ['Stationname', None, None], + 'condition': ['Condition', None, None], + 'conditioncode': ['Condition code', None, None], + 'conditiondetailed': ['Detailed condition', None, None], + 'conditionexact': ['Full condition', None, None], 'symbol': ['Symbol', None, None], 'humidity': ['Humidity', '%', 'mdi:water-percent'], 'temperature': ['Temperature', TEMP_CELSIUS, 'mdi:thermometer'], @@ -55,7 +61,67 @@ SENSOR_TYPES = { 'precipitation_forecast_average': ['Precipitation forecast average', 'mm/h', 'mdi:weather-pouring'], 'precipitation_forecast_total': ['Precipitation forecast total', - 'mm', 'mdi:weather-pouring'] + 'mm', 'mdi:weather-pouring'], + 'temperature_1d': ['Temperature 1d', TEMP_CELSIUS, 'mdi:thermometer'], + 'temperature_2d': ['Temperature 2d', TEMP_CELSIUS, 'mdi:thermometer'], + 'temperature_3d': ['Temperature 3d', TEMP_CELSIUS, 'mdi:thermometer'], + 'temperature_4d': ['Temperature 4d', TEMP_CELSIUS, 'mdi:thermometer'], + 'temperature_5d': ['Temperature 5d', TEMP_CELSIUS, 'mdi:thermometer'], + 'mintemp_1d': ['Minimum temperature 1d', TEMP_CELSIUS, 'mdi:thermometer'], + 'mintemp_2d': ['Minimum temperature 2d', TEMP_CELSIUS, 'mdi:thermometer'], + 'mintemp_3d': ['Minimum temperature 3d', TEMP_CELSIUS, 'mdi:thermometer'], + 'mintemp_4d': ['Minimum temperature 4d', TEMP_CELSIUS, 'mdi:thermometer'], + 'mintemp_5d': ['Minimum temperature 5d', TEMP_CELSIUS, 'mdi:thermometer'], + 'rain_1d': ['Rain 1d', 'mm', 'mdi:weather-pouring'], + 'rain_2d': ['Rain 2d', 'mm', 'mdi:weather-pouring'], + 'rain_3d': ['Rain 3d', 'mm', 'mdi:weather-pouring'], + 'rain_4d': ['Rain 4d', 'mm', 'mdi:weather-pouring'], + 'rain_5d': ['Rain 5d', 'mm', 'mdi:weather-pouring'], + 'snow_1d': ['Snow 1d', 'cm', 'mdi:snowflake'], + 'snow_2d': ['Snow 2d', 'cm', 'mdi:snowflake'], + 'snow_3d': ['Snow 3d', 'cm', 'mdi:snowflake'], + 'snow_4d': ['Snow 4d', 'cm', 'mdi:snowflake'], + 'snow_5d': ['Snow 5d', 'cm', 'mdi:snowflake'], + 'rainchance_1d': ['Rainchance 1d', '%', 'mdi:weather-pouring'], + 'rainchance_2d': ['Rainchance 2d', '%', 'mdi:weather-pouring'], + 'rainchance_3d': ['Rainchance 3d', '%', 'mdi:weather-pouring'], + 'rainchance_4d': ['Rainchance 4d', '%', 'mdi:weather-pouring'], + 'rainchance_5d': ['Rainchance 5d', '%', 'mdi:weather-pouring'], + 'sunchance_1d': ['Sunchance 1d', '%', 'mdi:weather-partlycloudy'], + 'sunchance_2d': ['Sunchance 2d', '%', 'mdi:weather-partlycloudy'], + 'sunchance_3d': ['Sunchance 3d', '%', 'mdi:weather-partlycloudy'], + 'sunchance_4d': ['Sunchance 4d', '%', 'mdi:weather-partlycloudy'], + 'sunchance_5d': ['Sunchance 5d', '%', 'mdi:weather-partlycloudy'], + 'windforce_1d': ['Wind force 1d', 'Bft', 'mdi:weather-windy'], + 'windforce_2d': ['Wind force 2d', 'Bft', 'mdi:weather-windy'], + 'windforce_3d': ['Wind force 3d', 'Bft', 'mdi:weather-windy'], + 'windforce_4d': ['Wind force 4d', 'Bft', 'mdi:weather-windy'], + 'windforce_5d': ['Wind force 5d', 'Bft', 'mdi:weather-windy'], + 'condition_1d': ['Condition 1d', None, None], + 'condition_2d': ['Condition 2d', None, None], + 'condition_3d': ['Condition 3d', None, None], + 'condition_4d': ['Condition 4d', None, None], + 'condition_5d': ['Condition 5d', None, None], + 'conditioncode_1d': ['Condition code 1d', None, None], + 'conditioncode_2d': ['Condition code 2d', None, None], + 'conditioncode_3d': ['Condition code 3d', None, None], + 'conditioncode_4d': ['Condition code 4d', None, None], + 'conditioncode_5d': ['Condition code 5d', None, None], + 'conditiondetailed_1d': ['Detailed condition 1d', None, None], + 'conditiondetailed_2d': ['Detailed condition 2d', None, None], + 'conditiondetailed_3d': ['Detailed condition 3d', None, None], + 'conditiondetailed_4d': ['Detailed condition 4d', None, None], + 'conditiondetailed_5d': ['Detailed condition 5d', None, None], + 'conditionexact_1d': ['Full condition 1d', None, None], + 'conditionexact_2d': ['Full condition 2d', None, None], + 'conditionexact_3d': ['Full condition 3d', None, None], + 'conditionexact_4d': ['Full condition 4d', None, None], + 'conditionexact_5d': ['Full condition 5d', None, None], + 'symbol_1d': ['Symbol 1d', None, None], + 'symbol_2d': ['Symbol 2d', None, None], + 'symbol_3d': ['Symbol 3d', None, None], + 'symbol_4d': ['Symbol 4d', None, None], + 'symbol_5d': ['Symbol 5d', None, None], } CONF_TIMEFRAME = 'timeframe' @@ -126,23 +192,86 @@ class BrSensor(Entity): def load_data(self, data): """Load the sensor with relevant data.""" # Find sensor - from buienradar.buienradar import (ATTRIBUTION, IMAGE, MEASURED, + from buienradar.buienradar import (ATTRIBUTION, CONDITION, CONDCODE, + DETAILED, EXACT, EXACTNL, FORECAST, + IMAGE, MEASURED, PRECIPITATION_FORECAST, STATIONNAME, - SYMBOL, TIMEFRAME) + TIMEFRAME) self._attribution = data.get(ATTRIBUTION) self._stationname = data.get(STATIONNAME) self._measured = data.get(MEASURED) - if self.type == SYMBOL: - # update weather symbol & status text - new_state = data.get(self.type) - img = data.get(IMAGE) - # pylint: disable=protected-access - if new_state != self._state or img != self._entity_picture: - self._state = new_state - self._entity_picture = img - return True + if self.type.endswith('_1d') or \ + self.type.endswith('_2d') or \ + self.type.endswith('_3d') or \ + self.type.endswith('_4d') or \ + self.type.endswith('_5d'): + + fcday = 0 + if self.type.endswith('_2d'): + fcday = 1 + if self.type.endswith('_3d'): + fcday = 2 + if self.type.endswith('_4d'): + fcday = 3 + if self.type.endswith('_5d'): + fcday = 4 + + # update all other sensors + if self.type.startswith(SYMBOL) or self.type.startswith(CONDITION): + condition = data.get(FORECAST)[fcday].get(CONDITION) + if condition: + new_state = condition.get(CONDITION, None) + if self.type.startswith(SYMBOL): + new_state = condition.get(EXACTNL, None) + if self.type.startswith('conditioncode'): + new_state = condition.get(CONDCODE, None) + if self.type.startswith('conditiondetailed'): + new_state = condition.get(DETAILED, None) + if self.type.startswith('conditionexact'): + new_state = condition.get(EXACT, None) + + img = condition.get(IMAGE, None) + + if new_state != self._state or img != self._entity_picture: + self._state = new_state + self._entity_picture = img + return True + return False + else: + new_state = data.get(FORECAST)[fcday].get(self.type[:-3]) + + if new_state != self._state: + self._state = new_state + return True + return False + + return False + + if self.type == SYMBOL or self.type.startswith(CONDITION): + # update weather symbol & status text + condition = data.get(CONDITION, None) + if condition: + if self.type == SYMBOL: + new_state = condition.get(EXACTNL, None) + if self.type == CONDITION: + new_state = condition.get(CONDITION, None) + if self.type == 'conditioncode': + new_state = condition.get(CONDCODE, None) + if self.type == 'conditiondetailed': + new_state = condition.get(DETAILED, None) + if self.type == 'conditionexact': + new_state = condition.get(EXACT, None) + + img = condition.get(IMAGE, None) + + # pylint: disable=protected-access + if new_state != self._state or img != self._entity_picture: + self._state = new_state + self._entity_picture = img + return True + return False if self.type.startswith(PRECIPITATION_FORECAST): @@ -187,11 +316,6 @@ class BrSensor(Entity): @property def entity_picture(self): """Weather symbol if type is symbol.""" - from buienradar.buienradar import SYMBOL - - if self.type != SYMBOL: - return None - return self._entity_picture @property @@ -360,8 +484,8 @@ class BrData(object): @property def condition(self): """Return the condition.""" - from buienradar.buienradar import SYMBOL - return self.data.get(SYMBOL) + from buienradar.buienradar import CONDITION + return self.data.get(CONDITION) @property def temperature(self): @@ -390,6 +514,15 @@ class BrData(object): except (ValueError, TypeError): return None + @property + def visibility(self): + """Return the visibility, or None.""" + from buienradar.buienradar import VISIBILITY + try: + return int(self.data.get(VISIBILITY)) + except (ValueError, TypeError): + return None + @property def wind_speed(self): """Return the windspeed, or None.""" @@ -402,9 +535,9 @@ class BrData(object): @property def wind_bearing(self): """Return the wind bearing, or None.""" - from buienradar.buienradar import WINDDIRECTION + from buienradar.buienradar import WINDAZIMUTH try: - return int(self.data.get(WINDDIRECTION)) + return int(self.data.get(WINDAZIMUTH)) except (ValueError, TypeError): return None diff --git a/homeassistant/components/weather/buienradar.py b/homeassistant/components/weather/buienradar.py index bca50182a16..f37914b3b0f 100755 --- a/homeassistant/components/weather/buienradar.py +++ b/homeassistant/components/weather/buienradar.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/weather.buienradar/ import logging import asyncio from homeassistant.components.weather import ( - WeatherEntity, PLATFORM_SCHEMA) + WeatherEntity, PLATFORM_SCHEMA, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME) from homeassistant.const import \ CONF_NAME, TEMP_CELSIUS, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.helpers import config_validation as cv @@ -16,14 +16,37 @@ from homeassistant.components.sensor.buienradar import ( BrData) import voluptuous as vol -REQUIREMENTS = ['buienradar==0.8'] +REQUIREMENTS = ['buienradar==0.9'] _LOGGER = logging.getLogger(__name__) +DATA_CONDITION = 'buienradar_condition' + DEFAULT_TIMEFRAME = 60 CONF_FORECAST = 'forecast' +ATTR_FORECAST_CONDITION = 'condition' +ATTR_FORECAST_TEMP_LOW = 'templow' + + +CONDITION_CLASSES = { + 'cloudy': ['c', 'p'], + 'fog': ['d', 'n'], + 'hail': [], + 'lightning': ['g'], + 'lightning-rainy': ['s'], + 'partlycloudy': ['b', 'j', 'o', 'r'], + 'pouring': ['l', 'q'], + 'rainy': ['f', 'h', 'k', 'm'], + 'snowy': ['u', 'i', 'v', 't'], + 'snowy-rainy': ['w'], + 'sunny': ['a'], + 'windy': [], + 'windy-variant': [], + 'exceptional': [], +} + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, @@ -50,8 +73,16 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): # create weather device: _LOGGER.debug("Initializing buienradar weather: coordinates %s", coordinates) - async_add_devices([BrWeather(data, config.get(CONF_FORECAST, True), - config.get(CONF_NAME, None))]) + + # create condition helper + if DATA_CONDITION not in hass.data: + cond_keys = [str(chr(x)) for x in range(97, 123)] + hass.data[DATA_CONDITION] = dict.fromkeys(cond_keys) + for cond, condlst in CONDITION_CLASSES.items(): + for condi in condlst: + hass.data[DATA_CONDITION][condi] = cond + + async_add_devices([BrWeather(data, config)]) # schedule the first update in 1 minute from now: yield from data.schedule_update(1) @@ -60,10 +91,10 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None): class BrWeather(WeatherEntity): """Representation of a weather condition.""" - def __init__(self, data, forecast, stationname=None): + def __init__(self, data, config): """Initialise the platform with a data instance and station name.""" - self._stationname = stationname - self._forecast = forecast + self._stationname = config.get(CONF_NAME, None) + self._forecast = config.get(CONF_FORECAST) self._data = data @property @@ -79,17 +110,32 @@ class BrWeather(WeatherEntity): @property def condition(self): - """Return the name of the sensor.""" - return self._data.condition + """Return the current condition.""" + from buienradar.buienradar import (CONDCODE) + if self._data and self._data.condition: + ccode = self._data.condition.get(CONDCODE) + if ccode: + conditions = self.hass.data.get(DATA_CONDITION) + if conditions: + return conditions.get(ccode) + + @property + def entity_picture(self): + """Return the entity picture to use in the frontend, if any.""" + from buienradar.buienradar import (IMAGE) + + if self._data and self._data.condition: + return self._data.condition.get(IMAGE, None) + return None @property def temperature(self): - """Return the name of the sensor.""" + """Return the current temperature.""" return self._data.temperature @property def pressure(self): - """Return the name of the sensor.""" + """Return the current pressure.""" return self._data.pressure @property @@ -97,14 +143,19 @@ class BrWeather(WeatherEntity): """Return the name of the sensor.""" return self._data.humidity + @property + def visibility(self): + """Return the current visibility.""" + return self._data.visibility + @property def wind_speed(self): - """Return the name of the sensor.""" + """Return the current windspeed.""" return self._data.wind_speed @property def wind_bearing(self): - """Return the name of the sensor.""" + """Return the current wind bearing (degrees).""" return self._data.wind_bearing @property @@ -114,6 +165,25 @@ class BrWeather(WeatherEntity): @property def forecast(self): - """Return the forecast.""" + """Return the forecast array.""" + from buienradar.buienradar import (CONDITION, CONDCODE, DATETIME, + MIN_TEMP, MAX_TEMP) + if self._forecast: - return self._data.forecast + fcdata_out = [] + cond = self.hass.data[DATA_CONDITION] + if self._data.forecast: + for data_in in self._data.forecast: + # remap keys from external library to + # keys understood by the weather component: + data_out = {} + condcode = data_in.get(CONDITION, []).get(CONDCODE) + + data_out[ATTR_FORECAST_TIME] = data_in.get(DATETIME) + data_out[ATTR_FORECAST_CONDITION] = cond[condcode] + data_out[ATTR_FORECAST_TEMP_LOW] = data_in.get(MIN_TEMP) + data_out[ATTR_FORECAST_TEMP] = data_in.get(MAX_TEMP) + + fcdata_out.append(data_out) + + return fcdata_out diff --git a/requirements_all.txt b/requirements_all.txt index 0d8d5f55843..59ca0a67bb1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -128,7 +128,7 @@ broadlink==0.5 # homeassistant.components.sensor.buienradar # homeassistant.components.weather.buienradar -buienradar==0.8 +buienradar==0.9 # homeassistant.components.notify.ciscospark ciscosparkapi==0.4.2 From f1142638455dd7f4409c4971ae47ba559708189f Mon Sep 17 00:00:00 2001 From: karlkar Date: Wed, 16 Aug 2017 09:29:42 +0200 Subject: [PATCH 31/93] Pushbullet, fix multiple messages sent when url param is set (#9006) --- homeassistant/components/notify/pushbullet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notify/pushbullet.py b/homeassistant/components/notify/pushbullet.py index 8ac2bd06dad..bbc7c18ffab 100644 --- a/homeassistant/components/notify/pushbullet.py +++ b/homeassistant/components/notify/pushbullet.py @@ -91,7 +91,7 @@ class PushBulletNotificationService(BaseNotificationService): # Backward compatibility, notify all devices in own account if url: self.pushbullet.push_link(title, url, body=message) - if filepath and self.hass.config.is_allowed_path(filepath): + elif filepath and self.hass.config.is_allowed_path(filepath): with open(filepath, "rb") as fileh: filedata = self.pushbullet.upload_file(fileh, filepath) self.pushbullet.push_file(title=title, body=message, From 95663f8126f13d4ec39affcc6f3d5048ead5e63d Mon Sep 17 00:00:00 2001 From: Dan Cinnamon Date: Wed, 16 Aug 2017 05:08:15 -0500 Subject: [PATCH 32/93] =?UTF-8?q?Update=20to=20pyenvisalink=202.2,=20and?= =?UTF-8?q?=20remove=20range=20validation=20on=20zonedump=20i=E2=80=A6=20(?= =?UTF-8?q?#8981)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update to pyenvisalink 2.2, and remove range validation on zonedump interval. * Keep using default timer dump variable, only remove minimum check. * Fix lint issue * Indentation issue --- homeassistant/components/envisalink.py | 8 ++++---- requirements_all.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/envisalink.py b/homeassistant/components/envisalink.py index 87b6163282a..5ffd97ef0e3 100644 --- a/homeassistant/components/envisalink.py +++ b/homeassistant/components/envisalink.py @@ -16,7 +16,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send -REQUIREMENTS = ['pyenvisalink==2.1'] +REQUIREMENTS = ['pyenvisalink==2.2'] _LOGGER = logging.getLogger(__name__) @@ -74,9 +74,9 @@ CONFIG_SCHEMA = vol.Schema({ vol.All(vol.Coerce(int), vol.Range(min=3, max=4)), vol.Optional(CONF_EVL_KEEPALIVE, default=DEFAULT_KEEPALIVE): vol.All(vol.Coerce(int), vol.Range(min=15)), - vol.Optional(CONF_ZONEDUMP_INTERVAL, - default=DEFAULT_ZONEDUMP_INTERVAL): - vol.All(vol.Coerce(int), vol.Range(min=15)), + vol.Optional( + CONF_ZONEDUMP_INTERVAL, + default=DEFAULT_ZONEDUMP_INTERVAL): vol.Coerce(int), }), }, extra=vol.ALLOW_EXTRA) diff --git a/requirements_all.txt b/requirements_all.txt index 59ca0a67bb1..0a14bc0d38f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -576,7 +576,7 @@ pyeight==0.0.7 pyemby==1.4 # homeassistant.components.envisalink -pyenvisalink==2.1 +pyenvisalink==2.2 # homeassistant.components.sensor.fido pyfido==1.0.1 From b75ce4f1b2efde2152b8960e5edf090e0916f807 Mon Sep 17 00:00:00 2001 From: Michael Hertig Date: Wed, 16 Aug 2017 21:28:51 +0200 Subject: [PATCH 33/93] Fix #9010 - Swiss Public Transportation shows departure time in the past (#9011) --- homeassistant/components/sensor/swiss_public_transport.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/swiss_public_transport.py b/homeassistant/components/sensor/swiss_public_transport.py index aa0be36b075..0febd8c95bc 100644 --- a/homeassistant/components/sensor/swiss_public_transport.py +++ b/homeassistant/components/sensor/swiss_public_transport.py @@ -136,7 +136,7 @@ class PublicTransportData(object): 'fields[]=connections/from/departureTimestamp/&' + 'fields[]=connections/', timeout=10) - connections = response.json()['connections'][:2] + connections = response.json()['connections'][1:3] try: self.times = [ From 3765f882c771979e6f6356b93ed9ed5e47586ad9 Mon Sep 17 00:00:00 2001 From: BioSehnsucht Date: Wed, 16 Aug 2017 18:26:30 -0500 Subject: [PATCH 34/93] Add HipChat notify service. (#8918) * Add HipChat notify service. * Change HipChat notify service to use python-simple-hipchat-v2. * Change HipChat notify service to use hipnotify * Change HipChat notify service to remove redundant validation --- .coveragerc | 1 + homeassistant/components/notify/hipchat.py | 102 +++++++++++++++++++++ requirements_all.txt | 3 + 3 files changed, 106 insertions(+) create mode 100644 homeassistant/components/notify/hipchat.py diff --git a/.coveragerc b/.coveragerc index 3cbb942508b..8297f7823d5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -384,6 +384,7 @@ omit = homeassistant/components/notify/free_mobile.py homeassistant/components/notify/gntp.py homeassistant/components/notify/group.py + homeassistant/components/notify/hipchat.py homeassistant/components/notify/instapush.py homeassistant/components/notify/kodi.py homeassistant/components/notify/lannouncer.py diff --git a/homeassistant/components/notify/hipchat.py b/homeassistant/components/notify/hipchat.py new file mode 100644 index 00000000000..5d69ce97d61 --- /dev/null +++ b/homeassistant/components/notify/hipchat.py @@ -0,0 +1,102 @@ +""" +HipChat platform for notify component. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/notify.hipchat/ +""" +import logging + +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_TARGET, ATTR_DATA, + PLATFORM_SCHEMA, BaseNotificationService) +from homeassistant.const import ( + CONF_TOKEN) +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['hipnotify==1.0.8'] + +_LOGGER = logging.getLogger(__name__) + +CONF_COLOR = 'color' +CONF_ROOM = 'room' +CONF_NOTIFY = 'notify' +CONF_FORMAT = 'format' +CONF_HOST = 'host' + +VALID_COLORS = {'yellow', 'green', 'red', 'purple', 'gray', 'random'} +VALID_FORMATS = {'text', 'html'} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Required(CONF_ROOM): vol.Coerce(int), + vol.Optional(CONF_COLOR, default='yellow'): vol.In(VALID_COLORS), + vol.Optional(CONF_NOTIFY, default=False): cv.boolean, + vol.Optional(CONF_FORMAT, default='text'): vol.In(VALID_FORMATS), + vol.Optional(CONF_HOST, default='https://api.hipchat.com/'): cv.string, +}) + + +def get_service(hass, config, discovery_info=None): + """Get the HipChat notification service.""" + return HipchatNotificationService( + config[CONF_TOKEN], + config[CONF_ROOM], + config[CONF_COLOR], + config[CONF_NOTIFY], + config[CONF_FORMAT], + config[CONF_HOST]) + + +class HipchatNotificationService(BaseNotificationService): + """Implement the notification service for HipChat.""" + + def __init__(self, token, default_room, default_color, default_notify, + default_format, host): + """Initialize the service.""" + self._token = token + self._default_room = default_room + self._default_color = default_color + self._default_notify = default_notify + self._default_format = default_format + self._host = host + + self._rooms = {} + self._get_room(self._default_room) + + def _get_room(self, room): + """Get Room object, creating it if necessary.""" + from hipnotify import Room + if room not in self._rooms: + self._rooms[room] = Room(token=self._token, + room_id=room, + endpoint_url=self._host) + return self._rooms[room] + + def send_message(self, message="", **kwargs): + """Send a message.""" + color = self._default_color + notify = self._default_notify + message_format = self._default_format + + if kwargs.get(ATTR_DATA) is not None: + data = kwargs.get(ATTR_DATA) + if ((data.get(CONF_COLOR) is not None) + and (data.get(CONF_COLOR) in VALID_COLORS)): + color = data.get(CONF_COLOR) + if ((data.get(CONF_NOTIFY) is not None) + and isinstance(data.get(CONF_NOTIFY), bool)): + notify = data.get(CONF_NOTIFY) + if ((data.get(CONF_FORMAT) is not None) + and (data.get(CONF_FORMAT) in VALID_FORMATS)): + message_format = data.get(CONF_FORMAT) + + targets = kwargs.get(ATTR_TARGET, [self._default_room]) + + for target in targets: + room = self._get_room(target) + room.notify(msg=message, + color=color, + notify=notify, + message_format=message_format) diff --git a/requirements_all.txt b/requirements_all.txt index 0a14bc0d38f..34a1482508b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -283,6 +283,9 @@ heatmiserV3==0.9.1 # homeassistant.components.switch.hikvisioncam hikvision==0.4 +# homeassistant.components.notify.hipchat +hipnotify==1.0.8 + # homeassistant.components.binary_sensor.workday holidays==0.8.1 From 55234a7fa32c6229df2b51debe7e8ce955e4be38 Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 17 Aug 2017 00:51:03 -0400 Subject: [PATCH 35/93] Update onkyo-eiscp to 1.2.3 (#9019) --- homeassistant/components/media_player/onkyo.py | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_player/onkyo.py b/homeassistant/components/media_player/onkyo.py index 927de799ae9..c49df1405d5 100644 --- a/homeassistant/components/media_player/onkyo.py +++ b/homeassistant/components/media_player/onkyo.py @@ -14,7 +14,7 @@ from homeassistant.components.media_player import ( from homeassistant.const import (STATE_OFF, STATE_ON, CONF_HOST, CONF_NAME) import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['onkyo-eiscp==1.1'] +REQUIREMENTS = ['onkyo-eiscp==1.2.3'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 34a1482508b..9cfaed98866 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -435,7 +435,7 @@ oauth2client==4.0.0 oemthermostat==1.1 # homeassistant.components.media_player.onkyo -onkyo-eiscp==1.1 +onkyo-eiscp==1.2.3 # homeassistant.components.camera.onvif onvif-py3==0.1.3 From 427d7ee1fc430e94041a5888b47ff8e4867f41ba Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Thu, 17 Aug 2017 22:39:20 +0200 Subject: [PATCH 36/93] Check if album image(s) exist in spotify (#9024) * Check if album image(s) exist in spotify * Actually set the image to None * Simplified using ternary operator --- homeassistant/components/media_player/spotify.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/media_player/spotify.py b/homeassistant/components/media_player/spotify.py index 80d18b8eea8..239b13a6292 100644 --- a/homeassistant/components/media_player/spotify.py +++ b/homeassistant/components/media_player/spotify.py @@ -185,7 +185,8 @@ class SpotifyMediaPlayer(MediaPlayerDevice): self._artist = ', '.join([artist.get('name') for artist in item.get('artists')]) self._uri = current.get('uri') - self._image_url = item.get('album').get('images')[0].get('url') + images = item.get('album').get('images') + self._image_url = images[0].get('url') if images else None # Playing state self._state = STATE_PAUSED if current.get('is_playing'): From c278209c7b70024ae8ad4780f6f3fd7d39c8849d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 18 Aug 2017 00:51:52 +0200 Subject: [PATCH 37/93] Update ffmpeg to 1.7 to fix severals problems (#9029) * Update ffmpeg to 1.7 to fix severals problems * Update ffmpeg.py * Update requirements_test_all.txt --- homeassistant/components/ffmpeg.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ffmpeg.py b/homeassistant/components/ffmpeg.py index 45bd651ad95..887d07e5855 100644 --- a/homeassistant/components/ffmpeg.py +++ b/homeassistant/components/ffmpeg.py @@ -19,7 +19,7 @@ from homeassistant.helpers.dispatcher import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ['ha-ffmpeg==1.5'] +REQUIREMENTS = ['ha-ffmpeg==1.7'] DOMAIN = 'ffmpeg' diff --git a/requirements_all.txt b/requirements_all.txt index 9cfaed98866..0bb785cc0ef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -269,7 +269,7 @@ gps3==0.33.3 gstreamer-player==1.1.0 # homeassistant.components.ffmpeg -ha-ffmpeg==1.5 +ha-ffmpeg==1.7 # homeassistant.components.media_player.philips_js ha-philipsjs==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 68ce17c04f2..74717aa7d7b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -49,7 +49,7 @@ fuzzywuzzy==0.15.1 gTTS-token==1.1.1 # homeassistant.components.ffmpeg -ha-ffmpeg==1.5 +ha-ffmpeg==1.7 # homeassistant.components.mqtt.server hbmqtt==0.8 From b282167f264535971247ca9dc639af7262499059 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 17 Aug 2017 23:19:35 -0700 Subject: [PATCH 38/93] Add state_with_unit property to state objects in templates (#9014) * Wrap state objects in templates * Fix tests * Fix bugs * Lint * Remove invalid state warning --- homeassistant/helpers/template.py | 50 +++++++++++++++++++++++++++---- tests/helpers/test_template.py | 36 ++++++++++++++++++++-- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 6c74c49424e..aa6ca186a8e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -10,7 +10,8 @@ from jinja2 import contextfilter from jinja2.sandbox import ImmutableSandboxedEnvironment from homeassistant.const import ( - STATE_UNKNOWN, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL) + STATE_UNKNOWN, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL, + ATTR_UNIT_OF_MEASUREMENT) from homeassistant.core import State from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper @@ -181,8 +182,10 @@ class AllStates(object): def __iter__(self): """Return all states.""" - return iter(sorted(self._hass.states.async_all(), - key=lambda state: state.entity_id)) + return iter( + _wrap_state(state) for state in + sorted(self._hass.states.async_all(), + key=lambda state: state.entity_id)) def __call__(self, entity_id): """Return the states.""" @@ -200,7 +203,8 @@ class DomainStates(object): def __getattr__(self, name): """Return the states.""" - return self._hass.states.get('{}.{}'.format(self._domain, name)) + return _wrap_state( + self._hass.states.get('{}.{}'.format(self._domain, name))) def __iter__(self): """Return the iteration over all the states.""" @@ -210,6 +214,42 @@ class DomainStates(object): key=lambda state: state.entity_id)) +class TemplateState(State): + """Class to represent a state object in a template.""" + + # Inheritance is done so functions that check against State keep working + # pylint: disable=super-init-not-called + def __init__(self, state): + """Initialize template state.""" + self._state = state + + @property + def state_with_unit(self): + """Return the state concatenated with the unit if available.""" + state = object.__getattribute__(self, '_state') + unit = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + if unit is None: + return state.state + return "{} {}".format(state.state, unit) + + def __getattribute__(self, name): + """Return an attribute of the state.""" + if name in TemplateState.__dict__: + return object.__getattribute__(self, name) + else: + return getattr(object.__getattribute__(self, '_state'), name) + + def __repr__(self): + """Representation of Template State.""" + rep = object.__getattribute__(self, '_state').__repr__() + return '