From 69bdce768c1e4d5c36d2fed909db6971731fdd89 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 3 Oct 2019 11:19:02 +0000 Subject: [PATCH 001/639] Bump version 0.101.0dev0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7d8a68a9707..9baa4a1f71c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 100 +MINOR_VERSION = 101 PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) From 2307cac942d4f5b1cef5bdf4a11d9baa5160791c Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Oct 2019 07:26:19 -0500 Subject: [PATCH 002/639] Add unique_id to cert_expiry (#27140) * Add unique_id to cert_expiry * Simplify ID --- homeassistant/components/cert_expiry/sensor.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index b564cff7338..2cd5c9abc8e 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -67,6 +67,11 @@ class SSLCertificate(Entity): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return a unique id for the sensor.""" + return f"{self.server_name}:{self.server_port}" + @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" From bb45bdd8dd937a9fbbb21def1da93299498a9290 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Oct 2019 10:39:14 -0500 Subject: [PATCH 003/639] Fix update on cert_expiry startup (#27137) * Don't force extra update on startup * Skip on entity add instead * Conditional update based on HA state * Only force entity state update when postponed * Clean up state updating * Delay YAML import --- .../components/cert_expiry/sensor.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 2cd5c9abc8e..2d578ef2c3b 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -15,6 +15,7 @@ from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_START, ) +from homeassistant.core import callback from homeassistant.helpers.entity import Entity from .const import DOMAIN, DEFAULT_NAME, DEFAULT_PORT @@ -35,18 +36,26 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up certificate expiry sensor.""" - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + + @callback + def do_import(_): + """Process YAML import after HA is fully started.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + ) ) - ) + + # Delay to avoid validation during setup in case we're checking our own cert. + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_import) async def async_setup_entry(hass, entry, async_add_entities): """Add cert-expiry entry.""" async_add_entities( [SSLCertificate(entry.title, entry.data[CONF_HOST], entry.data[CONF_PORT])], - True, + False, + # Don't update in case we're checking our own cert. ) return True @@ -89,17 +98,22 @@ class SSLCertificate(Entity): @property def available(self): - """Icon to use in the frontend, if any.""" + """Return the availability of the sensor.""" return self._available async def async_added_to_hass(self): """Once the entity is added we should update to get the initial data loaded.""" + @callback def do_update(_): """Run the update method when the start event was fired.""" - self.update() + self.async_schedule_update_ha_state(True) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) + if self.hass.is_running: + self.async_schedule_update_ha_state(True) + else: + # Delay until HA is fully started in case we're checking our own cert. + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, do_update) def update(self): """Fetch the certificate information.""" From 9902209ad2ad9b7e92731533bb6a1e68aa3d808c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 22:17:58 +0200 Subject: [PATCH 004/639] Add above and below to sensor trigger extra_fields (#27160) --- homeassistant/components/sensor/device_trigger.py | 6 +++++- tests/components/sensor/test_device_trigger.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 1074236eedf..00a05f4e590 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -140,6 +140,10 @@ async def async_get_trigger_capabilities(hass, trigger): """List trigger capabilities.""" return { "extra_fields": vol.Schema( - {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + { + vol.Optional(CONF_ABOVE): vol.Coerce(float), + vol.Optional(CONF_BELOW): vol.Coerce(float), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } ) } diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 1dc41f5bffa..fcf3c474f46 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -86,7 +86,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_capabilities = { "extra_fields": [ - {"name": "for", "optional": True, "type": "positive_time_period_dict"} + {"name": "above", "optional": True, "type": "float"}, + {"name": "below", "optional": True, "type": "float"}, + {"name": "for", "optional": True, "type": "positive_time_period_dict"}, ] } triggers = await async_get_device_automations(hass, "trigger", device_entry.id) From 565302ed34822fe48270455fdf6723699f74a7c2 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 3 Oct 2019 22:23:25 +0200 Subject: [PATCH 005/639] Improve device tracker tests (#27159) --- tests/components/unifi/test_device_tracker.py | 264 +++++++++--------- 1 file changed, 137 insertions(+), 127 deletions(-) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 3a2b37487af..8e05d8a1dd1 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -4,12 +4,7 @@ from copy import copy from datetime import timedelta -from asynctest import Mock - -import pytest - -from aiounifi.clients import Clients, ClientsAll -from aiounifi.devices import Devices +from asynctest import patch from homeassistant import config_entries from homeassistant.components import unifi @@ -107,64 +102,63 @@ ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") -@pytest.fixture -def mock_controller(hass): - """Mock a UniFi Controller.""" - hass.data[UNIFI_CONFIG] = {} - hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() - controller = unifi.UniFiController(hass, None) - controller.wireless_clients = set() - - controller.api = Mock() - controller.mock_requests = [] - - controller.mock_client_responses = deque() - controller.mock_device_responses = deque() - controller.mock_client_all_responses = deque() - - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - controller.mock_requests.append(kwargs) - if path == "s/{site}/stat/sta": - return controller.mock_client_responses.popleft() - if path == "s/{site}/stat/device": - return controller.mock_device_responses.popleft() - if path == "s/{site}/rest/user": - return controller.mock_client_all_responses.popleft() - return None - - controller.api.clients = Clients({}, mock_request) - controller.api.devices = Devices({}, mock_request) - controller.api.clients_all = ClientsAll({}, mock_request) - - return controller - - -async def setup_controller(hass, mock_controller, options={}): - """Load the UniFi switch platform with the provided controller.""" - hass.config.components.add(unifi.DOMAIN) - hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller} +async def setup_unifi_integration( + hass, config, options, clients_response, devices_response, clients_all_response +): + """Create the UniFi controller.""" + hass.data[UNIFI_CONFIG] = [] + hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) config_entry = config_entries.ConfigEntry( - 1, - unifi.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - entry_id=1, + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, options=options, - ) - hass.config_entries._entries.append(config_entry) - mock_controller.config_entry = config_entry - - await mock_controller.async_update() - await hass.config_entries.async_forward_entry_setup( - config_entry, device_tracker.DOMAIN + entry_id=1, ) + sites = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "viewer"}} + + mock_client_responses = deque() + mock_client_responses.append(clients_response) + + mock_device_responses = deque() + mock_device_responses.append(devices_response) + + mock_client_all_responses = deque() + mock_client_all_responses.append(clients_all_response) + + mock_requests = [] + + async def mock_request(self, method, path, json=None): + mock_requests.append({"method": method, "path": path, "json": json}) + print(mock_requests, mock_client_responses, mock_device_responses) + if path == "s/{site}/stat/sta" and mock_client_responses: + return mock_client_responses.popleft() + if path == "s/{site}/stat/device" and mock_device_responses: + return mock_device_responses.popleft() + if path == "s/{site}/rest/user" and mock_client_all_responses: + return mock_client_all_responses.popleft() + return {} + + with patch("aiounifi.Controller.login", return_value=True), patch( + "aiounifi.Controller.sites", return_value=sites + ), patch("aiounifi.Controller.request", new=mock_request): + await unifi.async_setup_entry(hass, config_entry) await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + controller_id = unifi.get_controller_id_from_config_entry(config_entry) + controller = hass.data[unifi.DOMAIN][controller_id] + + controller.mock_client_responses = mock_client_responses + controller.mock_device_responses = mock_device_responses + controller.mock_client_all_responses = mock_client_all_responses + + return controller async def test_platform_manually_configured(hass): @@ -178,24 +172,30 @@ async def test_platform_manually_configured(hass): assert unifi.DOMAIN not in hass.data -async def test_no_clients(hass, mock_controller): +async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" - mock_controller.mock_client_responses.append({}) - mock_controller.mock_device_responses.append({}) + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + clients_response={}, + devices_response={}, + clients_all_response={}, + ) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 assert len(hass.states.async_all()) == 2 -async def test_tracked_devices(hass, mock_controller): +async def test_tracked_devices(hass): """Test the update_items function with some clients.""" - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2, CLIENT_3]) - mock_controller.mock_device_responses.append([DEVICE_1, DEVICE_2]) - options = {CONF_SSID_FILTER: ["ssid"]} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={CONF_SSID_FILTER: ["ssid"]}, + clients_response=[CLIENT_1, CLIENT_2, CLIENT_3], + devices_response=[DEVICE_1, DEVICE_2], + clients_all_response={}, + ) assert len(hass.states.async_all()) == 5 client_1 = hass.states.get("device_tracker.client_1") @@ -217,9 +217,9 @@ async def test_tracked_devices(hass, mock_controller): client_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) device_1_copy = copy(DEVICE_1) device_1_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_copy]) - mock_controller.mock_device_responses.append([device_1_copy]) - await mock_controller.async_update() + controller.mock_client_responses.append([client_1_copy]) + controller.mock_device_responses.append([device_1_copy]) + await controller.async_update() await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") @@ -230,19 +230,17 @@ async def test_tracked_devices(hass, mock_controller): device_1_copy = copy(DEVICE_1) device_1_copy["disabled"] = True - mock_controller.mock_client_responses.append({}) - mock_controller.mock_device_responses.append([device_1_copy]) - await mock_controller.async_update() + controller.mock_client_responses.append({}) + controller.mock_device_responses.append([device_1_copy]) + await controller.async_update() await hass.async_block_till_done() device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == STATE_UNAVAILABLE - mock_controller.config_entry.add_update_listener( - mock_controller.async_options_updated - ) + controller.config_entry.add_update_listener(controller.async_options_updated) hass.config_entries.async_update_entry( - mock_controller.config_entry, + controller.config_entry, options={ CONF_SSID_FILTER: [], CONF_TRACK_WIRED_CLIENTS: False, @@ -258,18 +256,22 @@ async def test_tracked_devices(hass, mock_controller): assert device_1 is None -async def test_wireless_client_go_wired_issue(hass, mock_controller): +async def test_wireless_client_go_wired_issue(hass): """Test the solution to catch wireless device go wired UniFi issue. UniFi has a known issue that when a wireless device goes away it sometimes gets marked as wired. """ client_1_client = copy(CLIENT_1) client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_client]) - mock_controller.mock_device_responses.append({}) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + clients_response=[client_1_client], + devices_response={}, + clients_all_response={}, + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") @@ -278,9 +280,9 @@ async def test_wireless_client_go_wired_issue(hass, mock_controller): client_1_client["is_wired"] = True client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_client]) - mock_controller.mock_device_responses.append({}) - await mock_controller.async_update() + controller.mock_client_responses.append([client_1_client]) + controller.mock_device_responses.append({}) + await controller.async_update() await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") @@ -288,31 +290,27 @@ async def test_wireless_client_go_wired_issue(hass, mock_controller): client_1_client["is_wired"] = False client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) - mock_controller.mock_client_responses.append([client_1_client]) - mock_controller.mock_device_responses.append({}) - await mock_controller.async_update() + controller.mock_client_responses.append([client_1_client]) + controller.mock_device_responses.append({}) + await controller.async_update() await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" -async def test_restoring_client(hass, mock_controller): +async def test_restoring_client(hass): """Test the update_items function with some clients.""" - mock_controller.mock_client_responses.append([CLIENT_2]) - mock_controller.mock_device_responses.append({}) - mock_controller.mock_client_all_responses.append([CLIENT_1]) - options = {unifi.CONF_BLOCK_CLIENT: True} - config_entry = config_entries.ConfigEntry( - 1, - unifi.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - entry_id=1, + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, + options={}, + entry_id=1, ) registry = await entity_registry.async_get_registry(hass) @@ -331,22 +329,30 @@ async def test_restoring_client(hass, mock_controller): config_entry=config_entry, ) - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 3 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.CONF_BLOCK_CLIENT: True}, + clients_response=[CLIENT_2], + devices_response={}, + clients_all_response=[CLIENT_1], + ) assert len(hass.states.async_all()) == 4 device_1 = hass.states.get("device_tracker.client_1") assert device_1 is not None -async def test_dont_track_clients(hass, mock_controller): +async def test_dont_track_clients(hass): """Test dont track clients config works.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([DEVICE_1]) - options = {unifi.controller.CONF_TRACK_CLIENTS: False} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.controller.CONF_TRACK_CLIENTS: False}, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response={}, + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") @@ -357,14 +363,16 @@ async def test_dont_track_clients(hass, mock_controller): assert device_1.state == "not_home" -async def test_dont_track_devices(hass, mock_controller): +async def test_dont_track_devices(hass): """Test dont track devices config works.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([DEVICE_1]) - options = {unifi.controller.CONF_TRACK_DEVICES: False} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.controller.CONF_TRACK_DEVICES: False}, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response={}, + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") @@ -375,14 +383,16 @@ async def test_dont_track_devices(hass, mock_controller): assert device_1 is None -async def test_dont_track_wired_clients(hass, mock_controller): +async def test_dont_track_wired_clients(hass): """Test dont track wired clients config works.""" - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) - mock_controller.mock_device_responses.append({}) - options = {unifi.controller.CONF_TRACK_WIRED_CLIENTS: False} - - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 2 + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False}, + clients_response=[CLIENT_1, CLIENT_2], + devices_response={}, + clients_all_response={}, + ) assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") From af81878d08f55be6a14ebc2f746f53a3b6ad0513 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Thu, 3 Oct 2019 16:28:02 -0400 Subject: [PATCH 006/639] Add PowerLevelController for fan to alexa (#27158) * Implement AlexaPowerLevelController * Implement AlexaPowerLevelController Tests --- .../components/alexa/capabilities.py | 35 ++++++++++ homeassistant/components/alexa/const.py | 7 +- homeassistant/components/alexa/entities.py | 2 + homeassistant/components/alexa/handlers.py | 70 +++++++++++++++++++ tests/components/alexa/test_smart_home.py | 22 ++++++ 5 files changed, 135 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index b8bd3841a78..fca63adab0e 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -614,3 +614,38 @@ class AlexaThermostatController(AlexaCapibility): return None return {"value": temp, "scale": API_TEMP_UNITS[unit]} + + +class AlexaPowerLevelController(AlexaCapibility): + """Implements Alexa.PowerLevelController. + + https://developer.amazon.com/docs/device-apis/alexa-powerlevelcontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.PowerLevelController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "powerLevel"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "powerLevel": + raise UnsupportedProperty(name) + + if self.entity.domain == fan.DOMAIN: + speed = self.entity.attributes.get(fan.ATTR_SPEED) + + return PERCENTAGE_FAN_MAP.get(speed, None) + + return None diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index 83c7da41c16..cd0cb85a0a5 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -62,7 +62,12 @@ API_THERMOSTAT_MODES = OrderedDict( ) API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"} -PERCENTAGE_FAN_MAP = {fan.SPEED_LOW: 33, fan.SPEED_MEDIUM: 66, fan.SPEED_HIGH: 100} +PERCENTAGE_FAN_MAP = { + fan.SPEED_OFF: 0, + fan.SPEED_LOW: 33, + fan.SPEED_MEDIUM: 66, + fan.SPEED_HIGH: 100, +} class Cause: diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 55b5878f667..63231f71447 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -43,6 +43,7 @@ from .capabilities import ( AlexaPercentageController, AlexaPlaybackController, AlexaPowerController, + AlexaPowerLevelController, AlexaSceneController, AlexaSpeaker, AlexaStepSpeaker, @@ -344,6 +345,7 @@ class FanCapabilities(AlexaEntity): supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & fan.SUPPORT_SET_SPEED: yield AlexaPercentageController(self.entity) + yield AlexaPowerLevelController(self.entity) yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index c72101460c4..3cb61675f92 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -779,3 +779,73 @@ async def async_api_set_thermostat_mode(hass, config, directive, context): async def async_api_reportstate(hass, config, directive, context): """Process a ReportState request.""" return directive.response(name="StateReport") + + +@HANDLERS.register(("Alexa.PowerLevelController", "SetPowerLevel")) +async def async_api_set_power_level(hass, config, directive, context): + """Process a SetPowerLevel request.""" + entity = directive.entity + percentage = int(directive.payload["powerLevel"]) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_SPEED + speed = "off" + + if percentage <= 33: + speed = "low" + elif percentage <= 66: + speed = "medium" + else: + speed = "high" + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.PowerLevelController", "AdjustPowerLevel")) +async def async_api_adjust_power_level(hass, config, directive, context): + """Process an AdjustPowerLevel request.""" + entity = directive.entity + percentage_delta = int(directive.payload["powerLevelDelta"]) + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + current = 0 + + if entity.domain == fan.DOMAIN: + service = fan.SERVICE_SET_SPEED + speed = entity.attributes.get(fan.ATTR_SPEED) + + if speed == "off": + current = 0 + elif speed == "low": + current = 33 + elif speed == "medium": + current = 66 + else: + current = 100 + + # set percentage + percentage = max(0, percentage_delta + current) + speed = "off" + + if percentage <= 33: + speed = "low" + elif percentage <= 66: + speed = "medium" + else: + speed = "high" + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + return directive.response() diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index e5e5b8ab7ae..78ce2963eaf 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -340,6 +340,7 @@ async def test_variable_fan(hass): appliance, "Alexa.PercentageController", "Alexa.PowerController", + "Alexa.PowerLevelController", "Alexa.EndpointHealth", ) @@ -364,6 +365,27 @@ async def test_variable_fan(hass): "speed", ) + call, _ = await assert_request_calls_service( + "Alexa.PowerLevelController", + "SetPowerLevel", + "fan#test_2", + "fan.set_speed", + hass, + payload={"powerLevel": "50"}, + ) + assert call.data["speed"] == "medium" + + await assert_percentage_changes( + hass, + [("high", "-5"), ("high", "5"), ("low", "-80")], + "Alexa.PowerLevelController", + "AdjustPowerLevel", + "fan#test_2", + "powerLevelDelta", + "fan.set_speed", + "speed", + ) + async def test_lock(hass): """Test lock discovery.""" From 2f251104e3f01aaad8d972966b5c717ecf0449dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 3 Oct 2019 23:28:12 +0300 Subject: [PATCH 007/639] update broadlink library (#27157) --- homeassistant/components/broadlink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index d77c32966b1..c2c128909cc 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -3,7 +3,7 @@ "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", "requirements": [ - "broadlink==0.11.1" + "broadlink==0.12.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index fd44b46c64b..dcb114c1896 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -308,7 +308,7 @@ boto3==1.9.233 braviarc-homeassistant==0.3.7.dev0 # homeassistant.components.broadlink -broadlink==0.11.1 +broadlink==0.12.0 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 From 4733fea4163ee6e71e9824ad5ffc184caadcfed1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 3 Oct 2019 22:28:53 +0200 Subject: [PATCH 008/639] Adds fields to light.toggle service description (#27155) --- homeassistant/components/light/services.yaml | 84 +++++++++++++++----- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index ef944d75efc..97186f56a8f 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -5,22 +5,22 @@ turn_on: fields: entity_id: description: Name(s) of entities to turn on - example: 'light.kitchen' + example: "light.kitchen" transition: description: Duration in seconds it takes to get to next state example: 60 rgb_color: description: Color for the light in RGB-format. - example: '[255, 100, 100]' + example: "[255, 100, 100]" color_name: description: A human readable color name. - example: 'red' + example: "red" hs_color: description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. - example: '[300, 70]' + example: "[300, 70]" xy_color: description: Color for the light in XY-format. - example: '[0.52, 0.43]' + example: "[0.52, 0.43]" color_temp: description: Color temperature for the light in mireds. example: 250 @@ -29,7 +29,7 @@ turn_on: example: 4000 white_value: description: Number between 0..255 indicating level of white. - example: '250' + example: "250" brightness: description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. example: 120 @@ -55,7 +55,7 @@ turn_off: fields: entity_id: description: Name(s) of entities to turn off. - example: 'light.kitchen' + example: "light.kitchen" transition: description: Duration in seconds it takes to get to next state. example: 60 @@ -68,23 +68,67 @@ turn_off: toggle: description: Toggles a light. fields: - '...': - description: All turn_on parameters can be used. + entity_id: + description: Name(s) of entities to turn on + example: "light.kitchen" + transition: + description: Duration in seconds it takes to get to next state + example: 60 + rgb_color: + description: Color for the light in RGB-format. + example: "[255, 100, 100]" + color_name: + description: A human readable color name. + example: "red" + hs_color: + description: Color for the light in hue/sat format. Hue is 0-360 and Sat is 0-100. + example: "[300, 70]" + xy_color: + description: Color for the light in XY-format. + example: "[0.52, 0.43]" + color_temp: + description: Color temperature for the light in mireds. + example: 250 + kelvin: + description: Color temperature for the light in Kelvin. + example: 4000 + white_value: + description: Number between 0..255 indicating level of white. + example: "250" + brightness: + description: Number between 0..255 indicating brightness, where 0 turns the light off, 1 is the minimum brightness and 255 is the maximum brightness supported by the light. + example: 120 + brightness_pct: + description: Number between 0..100 indicating percentage of full brightness, where 0 turns the light off, 1 is the minimum brightness and 100 is the maximum brightness supported by the light. + example: 47 + profile: + description: Name of a light profile to use. + example: relax + flash: + description: If the light should flash. + values: + - short + - long + effect: + description: Light effect. + values: + - colorloop + - random lifx_set_state: description: Set a color/brightness and possibliy turn the light on/off. fields: entity_id: description: Name(s) of entities to set a state on. - example: 'light.garage' - '...': + example: "light.garage" + "...": description: All turn_on parameters can be used to specify a color. infrared: description: Automatic infrared level (0..255) when light brightness is low. example: 255 zones: description: List of zone numbers to affect (8 per LIFX Z, starts at 0). - example: '[0,5]' + example: "[0,5]" transition: description: Duration in seconds it takes to get to the final state. example: 10 @@ -97,19 +141,19 @@ lifx_effect_pulse: fields: entity_id: description: Name(s) of entities to run the effect on. - example: 'light.kitchen' + example: "light.kitchen" mode: - description: 'Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid.' + description: "Decides how colors are changed. Possible values: blink, breathe, ping, strobe, solid." example: strobe brightness: description: Number between 0..255 indicating brightness of the temporary color. example: 120 color_name: description: A human readable color name. - example: 'red' + example: "red" rgb_color: description: The temporary color in RGB-format. - example: '[255, 100, 100]' + example: "[255, 100, 100]" period: description: Duration of the effect in seconds (default 1.0). example: 3 @@ -125,7 +169,7 @@ lifx_effect_colorloop: fields: entity_id: description: Name(s) of entities to run the effect on. - example: 'light.disco1, light.disco2, light.disco3' + example: "light.disco1, light.disco2, light.disco3" brightness: description: Number between 0 and 255 indicating brightness of the effect. Leave this out to maintain the current brightness of each participating light. example: 120 @@ -147,14 +191,14 @@ lifx_effect_stop: fields: entity_id: description: Name(s) of entities to stop effects on. Leave out to stop effects everywhere. - example: 'light.bedroom' + example: "light.bedroom" xiaomi_miio_set_scene: description: Set a fixed scene. fields: entity_id: description: Name of the light entity. - example: 'light.xiaomi_miio' + example: "light.xiaomi_miio" scene: description: Number of the fixed scene, between 1 and 4. example: 1 @@ -164,7 +208,7 @@ xiaomi_miio_set_delayed_turn_off: fields: entity_id: description: Name of the light entity. - example: 'light.xiaomi_miio' + example: "light.xiaomi_miio" time_period: description: Time period for the delayed turn off. example: "5, '0:05', {'minutes': 5}" From cda7692f2449590b6bbb275fb094cef7b76f7095 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 22:29:57 +0200 Subject: [PATCH 009/639] Add support for `for` to binary_sensor, light and switch device conditions (#27153) * Add support for `for` to binary_sensor, light and switch device conditions * Fix typing * Fixup * Fixup --- .../binary_sensor/device_condition.py | 14 ++- .../binary_sensor/device_trigger.py | 2 +- .../components/device_automation/__init__.py | 19 ++++ .../device_automation/toggle_entity.py | 14 ++- .../components/light/device_condition.py | 5 + .../components/light/device_trigger.py | 4 +- .../components/sensor/device_trigger.py | 2 +- .../components/switch/device_condition.py | 5 + .../components/switch/device_trigger.py | 4 +- .../binary_sensor/test_device_condition.py | 97 ++++++++++++++++- .../components/device_automation/test_init.py | 97 +++++++++++++++++ .../components/light/test_device_condition.py | 96 +++++++++++++++++ .../switch/test_device_condition.py | 100 +++++++++++++++++- 13 files changed, 447 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index 1749ea91c5b..d686ef412c1 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -4,7 +4,7 @@ import voluptuous as vol from homeassistant.core import HomeAssistant from homeassistant.components.device_automation.const import CONF_IS_OFF, CONF_IS_ON -from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_TYPE +from homeassistant.const import ATTR_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FOR, CONF_TYPE from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.entity_registry import ( async_entries_for_device, @@ -188,6 +188,7 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In(IS_OFF + IS_ON), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) @@ -244,5 +245,16 @@ def async_condition_from_config( condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], condition.CONF_STATE: stat, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] return condition.state_from_config(state_config, config_validation) + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 89fd9add69a..e05713b5c67 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -240,7 +240,7 @@ async def async_get_triggers(hass, device_id): return triggers -async def async_get_trigger_capabilities(hass, trigger): +async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index fa6deac40ba..9d0a5a72a47 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -59,6 +59,9 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( websocket_device_automation_list_triggers ) + hass.components.websocket_api.async_register_command( + websocket_device_automation_get_condition_capabilities + ) hass.components.websocket_api.async_register_command( websocket_device_automation_get_trigger_capabilities ) @@ -206,6 +209,22 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): connection.send_result(msg["id"], triggers) +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/condition/capabilities", + vol.Required("condition"): dict, + } +) +async def websocket_device_automation_get_condition_capabilities(hass, connection, msg): + """Handle request for device condition capabilities.""" + condition = msg["condition"] + capabilities = await _async_get_device_automation_capabilities( + hass, "condition", condition + ) + connection.send_result(msg["id"], capabilities) + + @websocket_api.async_response @websocket_api.websocket_command( { diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index c9588c1efa7..47953dc5e81 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -80,6 +80,7 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): cv.entity_id, vol.Required(CONF_TYPE): vol.In([CONF_IS_OFF, CONF_IS_ON]), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) @@ -132,6 +133,8 @@ def async_condition_from_config( condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], condition.CONF_STATE: stat, } + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] return condition.state_from_config(state_config, config_validation) @@ -213,7 +216,16 @@ async def async_get_triggers( return await _async_get_automations(hass, device_id, ENTITY_TRIGGERS, domain) -async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } + + +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: """List trigger capabilities.""" return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index 4abf34e6661..86f5761ddf5 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -27,3 +27,8 @@ def async_condition_from_config( async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return await toggle_entity.async_get_condition_capabilities(hass, config) diff --git a/homeassistant/components/light/device_trigger.py b/homeassistant/components/light/device_trigger.py index 5bd5d83e1c0..432d24d3c14 100644 --- a/homeassistant/components/light/device_trigger.py +++ b/homeassistant/components/light/device_trigger.py @@ -32,6 +32,6 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) -async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: """List trigger capabilities.""" - return await toggle_entity.async_get_trigger_capabilities(hass, trigger) + return await toggle_entity.async_get_trigger_capabilities(hass, config) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 00a05f4e590..5ec6a57ffb4 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -136,7 +136,7 @@ async def async_get_triggers(hass, device_id): return triggers -async def async_get_trigger_capabilities(hass, trigger): +async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 5825a3ba91a..f3d5903bcf3 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -27,3 +27,8 @@ def async_condition_from_config( async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) + + +async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List condition capabilities.""" + return await toggle_entity.async_get_condition_capabilities(hass, config) diff --git a/homeassistant/components/switch/device_trigger.py b/homeassistant/components/switch/device_trigger.py index 22a016e49b9..7f0458b3e9f 100644 --- a/homeassistant/components/switch/device_trigger.py +++ b/homeassistant/components/switch/device_trigger.py @@ -32,6 +32,6 @@ async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: return await toggle_entity.async_get_triggers(hass, device_id, DOMAIN) -async def async_get_trigger_capabilities(hass: HomeAssistant, trigger: dict) -> dict: +async def async_get_trigger_capabilities(hass: HomeAssistant, config: dict) -> dict: """List trigger capabilities.""" - return await toggle_entity.async_get_trigger_capabilities(hass, trigger) + return await toggle_entity.async_get_trigger_capabilities(hass, config) diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index b5502d8fe3d..34cf4030a50 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -1,5 +1,7 @@ """The test for binary_sensor device automation.""" +from datetime import timedelta import pytest +from unittest.mock import patch from homeassistant.components.binary_sensor import DOMAIN, DEVICE_CLASSES from homeassistant.components.binary_sensor.device_condition import ENTITY_CONDITIONS @@ -7,6 +9,7 @@ from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, @@ -14,6 +17,7 @@ from tests.common import ( mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -71,6 +75,28 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a binary_sensor condition.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -131,7 +157,6 @@ async def test_if_state(hass, calls): assert len(calls) == 0 hass.bus.async_fire("test_event1") - hass.bus.async_fire("test_event2") await hass.async_block_till_done() assert len(calls) == 1 assert calls[0].data["some"] == "is_on event - test_event1" @@ -142,3 +167,73 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_if_fires_on_for_condition(hass, calls): + """Test for firing if condition is on with delay.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=10) + point3 = point2 + timedelta(seconds=10) + + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = point1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_not_bat_low", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ("platform", "event.event_type") + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 10 secs into the future + mock_utcnow.return_value = point2 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 20 secs into the future + mock_utcnow.return_value = point3 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index fa78ae94416..1af4b541a92 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -170,6 +170,103 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert _same_lists(triggers, expected_triggers) +async def test_websocket_get_condition_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get the expected condition capabilities for a light through websocket.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create("light", "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/list", + "device_id": device_entry.id, + } + ) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + conditions = msg["result"] + + id = 2 + for condition in conditions: + await client.send_json( + { + "id": id, + "type": "device_automation/condition/capabilities", + "condition": condition, + } + ) + msg = await client.receive_json() + assert msg["id"] == id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + id = id + 1 + + +async def test_websocket_get_bad_condition_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no condition capabilities for a non existing domain.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/capabilities", + "condition": {"domain": "beer"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + +async def test_websocket_get_no_condition_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no condition capabilities for a domain with no device condition capabilities.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/condition/capabilities", + "condition": {"domain": "deconz"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + async def test_websocket_get_trigger_capabilities( hass, hass_ws_client, device_reg, entity_reg ): diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index 8009fbd6337..a9f4adddfab 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -1,11 +1,14 @@ """The test for light device automation.""" +from datetime import timedelta import pytest +from unittest.mock import patch from homeassistant.components.light import DOMAIN from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, @@ -13,6 +16,7 @@ from tests.common import ( mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -63,6 +67,28 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a light condition.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -134,3 +160,73 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_if_fires_on_for_condition(hass, calls): + """Test for firing if condition is on with delay.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=10) + point3 = point2 + timedelta(seconds=10) + + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = point1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ("platform", "event.event_type") + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 10 secs into the future + mock_utcnow.return_value = point2 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 20 secs into the future + mock_utcnow.return_value = point3 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_off event - test_event1" diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index e2ce5a373d2..e673527fada 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -1,20 +1,22 @@ """The test for switch device automation.""" +from datetime import timedelta import pytest +from unittest.mock import patch from homeassistant.components.switch import DOMAIN from homeassistant.const import STATE_ON, STATE_OFF, CONF_PLATFORM from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation -from homeassistant.components.device_automation import ( - _async_get_device_automations as async_get_device_automations, -) from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, async_mock_service, mock_device_registry, mock_registry, + async_get_device_automations, + async_get_device_automation_capabilities, ) @@ -65,6 +67,28 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a switch condition.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state(hass, calls): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -136,3 +160,73 @@ async def test_if_state(hass, calls): await hass.async_block_till_done() assert len(calls) == 2 assert calls[1].data["some"] == "is_off event - test_event2" + + +async def test_if_fires_on_for_condition(hass, calls): + """Test for firing if condition is on with delay.""" + point1 = dt_util.utcnow() + point2 = point1 + timedelta(seconds=10) + point3 = point2 + timedelta(seconds=10) + + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + ent1, ent2, ent3 = platform.ENTITIES + + with patch("homeassistant.core.dt_util.utcnow") as mock_utcnow: + mock_utcnow.return_value = point1 + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": ent1.entity_id, + "type": "is_off", + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "is_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ("platform", "event.event_type") + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(ent1.entity_id).state == STATE_ON + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 10 secs into the future + mock_utcnow.return_value = point2 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(ent1.entity_id, STATE_OFF) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + # Time travel 20 secs into the future + mock_utcnow.return_value = point3 + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_off event - test_event1" From 89ebc17fb139c4ded65fc717a094d8a9038f052c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 22:30:59 +0200 Subject: [PATCH 010/639] Only generate device trigger for sensor with unit (#27152) --- .../components/sensor/device_trigger.py | 11 ++++++++-- .../components/sensor/test_device_trigger.py | 4 +++- .../custom_components/test/sensor.py | 22 ++++++++++++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 5ec6a57ffb4..377b202db18 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -5,6 +5,7 @@ import homeassistant.components.automation.numeric_state as numeric_state_automa from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA from homeassistant.const import ( ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, CONF_ABOVE, CONF_BELOW, CONF_ENTITY_ID, @@ -113,8 +114,14 @@ async def async_get_triggers(hass, device_id): for entry in entries: device_class = DEVICE_CLASS_NONE state = hass.states.get(entry.entity_id) - if state: - device_class = state.attributes.get(ATTR_DEVICE_CLASS) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + continue + + device_class = state.attributes.get(ATTR_DEVICE_CLASS) templates = ENTITY_TRIGGERS.get( device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index fcf3c474f46..45452dc84a0 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -2,7 +2,7 @@ from datetime import timedelta import pytest -from homeassistant.components.sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.sensor import DOMAIN from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM from homeassistant.setup import async_setup_component @@ -19,6 +19,7 @@ from tests.common import ( async_get_device_automations, async_get_device_automation_capabilities, ) +from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES @pytest.fixture @@ -70,6 +71,7 @@ async def test_get_triggers(hass, device_reg, entity_reg): } for device_class in DEVICE_CLASSES for trigger in ENTITY_TRIGGERS[device_class] + if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert triggers == expected_triggers diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py index c25be28fdb0..651ee17bd65 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/testing_config/custom_components/test/sensor.py @@ -3,10 +3,24 @@ Provide a mock sensor platform. Call init before using it in your tests to ensure clean test data. """ -from homeassistant.components.sensor import DEVICE_CLASSES +import homeassistant.components.sensor as sensor from tests.common import MockEntity +DEVICE_CLASSES = list(sensor.DEVICE_CLASSES) +DEVICE_CLASSES.append("none") + +UNITS_OF_MEASUREMENT = { + sensor.DEVICE_CLASS_BATTERY: "%", # % of battery that is left + sensor.DEVICE_CLASS_HUMIDITY: "%", # % of humidity in the air + sensor.DEVICE_CLASS_ILLUMINANCE: "lm", # current light level (lx/lm) + sensor.DEVICE_CLASS_SIGNAL_STRENGTH: "dB", # signal strength (dB/dBm) + sensor.DEVICE_CLASS_TEMPERATURE: "C", # temperature (C/F) + sensor.DEVICE_CLASS_TIMESTAMP: "hh:mm:ss", # timestamp (ISO8601) + sensor.DEVICE_CLASS_PRESSURE: "hPa", # pressure (hPa/mbar) + sensor.DEVICE_CLASS_POWER: "kW", # power (W/kW) +} + ENTITIES = {} @@ -22,6 +36,7 @@ def init(empty=False): name=f"{device_class} sensor", unique_id=f"unique_{device_class}", device_class=device_class, + unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), ) for device_class in DEVICE_CLASSES } @@ -42,3 +57,8 @@ class MockSensor(MockEntity): def device_class(self): """Return the class of this sensor.""" return self._handle("device_class") + + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of this sensor.""" + return self._handle("unit_of_measurement") From adab228012e445750abe6a7ca1a6e3c834832fcb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 3 Oct 2019 18:50:15 -0500 Subject: [PATCH 011/639] Unload cert_expiry config entries (#27150) * Allow cert_expiry unloading * Update codeowners --- CODEOWNERS | 2 +- homeassistant/components/cert_expiry/__init__.py | 8 ++++++++ homeassistant/components/cert_expiry/manifest.json | 5 ++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 4e7b0a0cd2a..418eb745ecb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -49,7 +49,7 @@ homeassistant/components/broadlink/* @danielhiversen homeassistant/components/brunt/* @eavanvalkenburg homeassistant/components/bt_smarthub/* @jxwolstenholme homeassistant/components/buienradar/* @mjj4791 @ties -homeassistant/components/cert_expiry/* @cereal2nd +homeassistant/components/cert_expiry/* @Cereal2nd @jjlawren homeassistant/components/cisco_ios/* @fbradyirl homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index 7c7efea7333..f5078219809 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -15,3 +15,11 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, "sensor") ) return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_unload(entry, "sensor") + ) + return True diff --git a/homeassistant/components/cert_expiry/manifest.json b/homeassistant/components/cert_expiry/manifest.json index 97f72f2ad11..48816809bbd 100644 --- a/homeassistant/components/cert_expiry/manifest.json +++ b/homeassistant/components/cert_expiry/manifest.json @@ -5,5 +5,8 @@ "requirements": [], "config_flow": true, "dependencies": [], - "codeowners": ["@cereal2nd"] + "codeowners": [ + "@Cereal2nd", + "@jjlawren" + ] } From f2c5c249d29e54121fa3e8e79d4ccceb712c7933 Mon Sep 17 00:00:00 2001 From: Dan Cinnamon Date: Thu, 3 Oct 2019 19:15:52 -0500 Subject: [PATCH 012/639] Envisalink startup reconnect (#27063) * Added retry capability to the component initialization. * Removed extra chars * Black formatting. * Removed issue with block upon setup. Now setup will only fail if auth failed to the device. --- homeassistant/components/envisalink/__init__.py | 7 +++++-- homeassistant/components/envisalink/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 76d6a7e369c..6cdedf89744 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -141,9 +141,12 @@ async def async_setup(hass, config): @callback def connection_fail_callback(data): """Network failure callback.""" - _LOGGER.error("Could not establish a connection with the Envisalink") + _LOGGER.error( + "Could not establish a connection with the Envisalink- retrying..." + ) if not sync_connect.done(): - sync_connect.set_result(False) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_envisalink) + sync_connect.set_result(True) @callback def connection_success_callback(data): diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 6c5405c75ea..3cee270f099 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -3,7 +3,7 @@ "name": "Envisalink", "documentation": "https://www.home-assistant.io/integrations/envisalink", "requirements": [ - "pyenvisalink==3.8" + "pyenvisalink==4.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index dcb114c1896..208eeac899b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1168,7 +1168,7 @@ pyeight==0.1.1 pyemby==1.6 # homeassistant.components.envisalink -pyenvisalink==3.8 +pyenvisalink==4.0 # homeassistant.components.ephember pyephember==0.2.0 From 85947591c5ffc0fa04b9ffd734f67ed9d5153237 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 4 Oct 2019 00:32:16 +0000 Subject: [PATCH 013/639] [ci skip] Translation update --- .../components/deconz/.translations/it.json | 1 + .../components/deconz/.translations/no.json | 1 + .../deconz/.translations/zh-Hant.json | 1 + .../components/plex/.translations/it.json | 3 ++- .../plex/.translations/zh-Hant.json | 3 ++- .../components/sensor/.translations/ca.json | 24 +++++++++++++++++ .../components/sensor/.translations/da.json | 26 +++++++++++++++++++ .../components/sensor/.translations/en.json | 26 +++++++++++++++++++ .../components/sensor/.translations/it.json | 26 +++++++++++++++++++ .../components/sensor/.translations/no.json | 26 +++++++++++++++++++ .../sensor/.translations/zh-Hant.json | 26 +++++++++++++++++++ .../soma/.translations/zh-Hant.json | 13 ++++++++++ .../components/zha/.translations/da.json | 3 +++ 13 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/sensor/.translations/ca.json create mode 100644 homeassistant/components/sensor/.translations/da.json create mode 100644 homeassistant/components/sensor/.translations/en.json create mode 100644 homeassistant/components/sensor/.translations/it.json create mode 100644 homeassistant/components/sensor/.translations/no.json create mode 100644 homeassistant/components/sensor/.translations/zh-Hant.json create mode 100644 homeassistant/components/soma/.translations/zh-Hant.json diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 7a2b8832864..1f0b344a32d 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte", "remote_button_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte", "remote_button_rotated": "Pulsante ruotato \"{subtype}\"", + "remote_button_rotation_stopped": "La rotazione dei pulsanti \"{subtype}\" si \u00e8 arrestata", "remote_button_short_press": "Pulsante \"{subtype}\" premuto", "remote_button_short_release": "Pulsante \"{subtype}\" rilasciato", "remote_button_triple_press": "Pulsante \"{subtype}\" cliccato tre volte", diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index c7079fd6219..f779f0918fe 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", "remote_button_quintuple_press": "\" {subtype} \" - knappen femdobbelt klikket", "remote_button_rotated": "Knappen roterte \" {subtype} \"", + "remote_button_rotation_stopped": "Knappe rotasjon \"{under type}\" stoppet", "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\" {subtype} \"-knappen trippel klikket", diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index bd47a637761..2ad613cde68 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" \u6309\u9215\u56db\u9023\u9ede\u64ca", "remote_button_quintuple_press": "\"{subtype}\" \u6309\u9215\u4e94\u9023\u9ede\u64ca", "remote_button_rotated": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215", + "remote_button_rotation_stopped": "\u65cb\u8f49 \"{subtype}\" \u6309\u9215\u5df2\u505c\u6b62", "remote_button_short_press": "\"{subtype}\" \u6309\u9215\u5df2\u6309\u4e0b", "remote_button_short_release": "\"{subtype}\" \u6309\u9215\u5df2\u91cb\u653e", "remote_button_triple_press": "\"{subtype}\" \u6309\u9215\u4e09\u9023\u9ede\u64ca", diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 3c28f1d25f9..99a6d13e0d4 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -5,6 +5,7 @@ "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", "already_in_progress": "Plex \u00e8 in fase di configurazione", "invalid_import": "La configurazione importata non \u00e8 valida", + "token_request_timeout": "Timeout per l'ottenimento del token", "unknown": "Non riuscito per motivo sconosciuto" }, "error": { @@ -36,7 +37,7 @@ "manual_setup": "Configurazione manuale", "token": "Token Plex" }, - "description": "Immettere un token Plex per la configurazione automatica.", + "description": "Continuare ad autorizzare plex.tv o configurare manualmente un server.", "title": "Collegare il server Plex" } }, diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index 5f6d0c41c13..a0a033651a5 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -5,6 +5,7 @@ "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", + "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" }, "error": { @@ -36,7 +37,7 @@ "manual_setup": "\u624b\u52d5\u8a2d\u5b9a", "token": "Plex \u5bc6\u9470" }, - "description": "\u8acb\u8f38\u5165 Plex \u5bc6\u9470\u4ee5\u9032\u884c\u81ea\u52d5\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", + "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" } }, diff --git a/homeassistant/components/sensor/.translations/ca.json b/homeassistant/components/sensor/.translations/ca.json new file mode 100644 index 00000000000..59db5a62f86 --- /dev/null +++ b/homeassistant/components/sensor/.translations/ca.json @@ -0,0 +1,24 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Nivell de bateria de {entity_name}", + "is_humidity": "Humitat de {entity_name}", + "is_illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "is_pressure": "Pressi\u00f3 de {entity_name}", + "is_signal_strength": "For\u00e7a del senyal de {entity_name}", + "is_temperature": "Temperatura de {entity_name}", + "is_timestamp": "Marca de temps de {entity_name}", + "is_value": "Valor de {entity_name}" + }, + "trigger_type": { + "battery_level": "Nivell de bateria de {entity_name}", + "humidity": "Humitat de {entity_name}", + "illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "pressure": "Pressi\u00f3 de {entity_name}", + "signal_strength": "For\u00e7a del senyal de {entity_name}", + "temperature": "Temperatura de {entity_name}", + "timestamp": "Marca de temps de {entity_name}", + "value": "Valor de {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/da.json b/homeassistant/components/sensor/.translations/da.json new file mode 100644 index 00000000000..df9b9935dc1 --- /dev/null +++ b/homeassistant/components/sensor/.translations/da.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} batteriniveau", + "is_humidity": "{entity_name} fugtighed", + "is_illuminance": "{entity_name} belysningsstyrke", + "is_power": "{entity_name} str\u00f8m", + "is_pressure": "{entity_name} tryk", + "is_signal_strength": "{entity_name} signalstyrke", + "is_temperature": "{entity_name} temperatur", + "is_timestamp": "{entity_name} tidsstempel", + "is_value": "{entity_name} v\u00e6rdi" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniveau", + "humidity": "{entity_name} fugtighed", + "illuminance": "{entity_name} belysningsstyrke", + "power": "{entity_name} str\u00f8m", + "pressure": "{entity_name} tryk", + "signal_strength": "{entity_name} signalstyrke", + "temperature": "{entity_name} temperatur", + "timestamp": "{entity_name} tidsstempel", + "value": "{entity_name} v\u00e6rdi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/en.json b/homeassistant/components/sensor/.translations/en.json new file mode 100644 index 00000000000..7bbbe660feb --- /dev/null +++ b/homeassistant/components/sensor/.translations/en.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} battery level", + "is_humidity": "{entity_name} humidity", + "is_illuminance": "{entity_name} illuminance", + "is_power": "{entity_name} power", + "is_pressure": "{entity_name} pressure", + "is_signal_strength": "{entity_name} signal strength", + "is_temperature": "{entity_name} temperature", + "is_timestamp": "{entity_name} timestamp", + "is_value": "{entity_name} value" + }, + "trigger_type": { + "battery_level": "{entity_name} battery level", + "humidity": "{entity_name} humidity", + "illuminance": "{entity_name} illuminance", + "power": "{entity_name} power", + "pressure": "{entity_name} pressure", + "signal_strength": "{entity_name} signal strength", + "temperature": "{entity_name} temperature", + "timestamp": "{entity_name} timestamp", + "value": "{entity_name} value" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/it.json b/homeassistant/components/sensor/.translations/it.json new file mode 100644 index 00000000000..07b20245c16 --- /dev/null +++ b/homeassistant/components/sensor/.translations/it.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "Livello della batteria di {entity_name}", + "is_humidity": "Umidit\u00e0 di {entity_name}", + "is_illuminance": "Illuminazione di {entity_name}", + "is_power": "Potenza di {entity_name}", + "is_pressure": "Pressione di {entity_name}", + "is_signal_strength": "Potenza del segnale di {entity_name}", + "is_temperature": "Temperatura di {entity_name}", + "is_timestamp": "Data di {entity_name}", + "is_value": "Valore di {entity_name}" + }, + "trigger_type": { + "battery_level": "Livello della batteria di {entity_name}", + "humidity": "Umidit\u00e0 di {entity_name}", + "illuminance": "Illuminazione di {entity_name}", + "power": "Potenza di {entity_name}", + "pressure": "Pressione di {entity_name}", + "signal_strength": "Potenza del segnale di {entity_name}", + "temperature": "Temperatura di {entity_name}", + "timestamp": "Data di {entity_name}", + "value": "Valore di {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json new file mode 100644 index 00000000000..5f5eeaacd11 --- /dev/null +++ b/homeassistant/components/sensor/.translations/no.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} batteriniv\u00e5", + "is_humidity": "{entity_name} fuktighet", + "is_illuminance": "{entity_name} belysningsstyrke", + "is_power": "{entity_name} str\u00f8m", + "is_pressure": "{entity_name} trykk", + "is_signal_strength": "{entity_name} signalstyrke", + "is_temperature": "{entity_name} temperatur", + "is_timestamp": "{entity_name} tidsstempel", + "is_value": "{entity_name} verdi" + }, + "trigger_type": { + "battery_level": "{entity_name} batteriniv\u00e5", + "humidity": "{entity_name} fuktighet", + "illuminance": "{entity_name} belysningsstyrke", + "power": "{entity_name} str\u00f8m", + "pressure": "{entity_name} trykk", + "signal_strength": "{entity_name} signalstyrke", + "temperature": "{entity_name} temperatur", + "timestamp": "{entity_name} tidsstempel", + "value": "{entity_name} verdi" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/zh-Hant.json b/homeassistant/components/sensor/.translations/zh-Hant.json new file mode 100644 index 00000000000..af97681ee76 --- /dev/null +++ b/homeassistant/components/sensor/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} \u96fb\u91cf", + "is_humidity": "{entity_name} \u6fd5\u5ea6", + "is_illuminance": "{entity_name} \u7167\u5ea6", + "is_power": "{entity_name} \u96fb\u529b", + "is_pressure": "{entity_name} \u58d3\u529b", + "is_signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", + "is_temperature": "{entity_name} \u6eab\u5ea6", + "is_timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", + "is_value": "{entity_name} \u503c" + }, + "trigger_type": { + "battery_level": "{entity_name} \u96fb\u91cf", + "humidity": "{entity_name} \u6fd5\u5ea6", + "illuminance": "{entity_name} \u7167\u5ea6", + "power": "{entity_name} \u96fb\u529b", + "pressure": "{entity_name} \u58d3\u529b", + "signal_strength": "{entity_name} \u8a0a\u865f\u5f37\u5ea6", + "temperature": "{entity_name} \u6eab\u5ea6", + "timestamp": "{entity_name} \u6642\u9593\u6a19\u8a18", + "value": "{entity_name} \u503c" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json new file mode 100644 index 00000000000..3d28389ff91 --- /dev/null +++ b/homeassistant/components/soma/.translations/zh-Hant.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44 Soma \u5e33\u865f\u3002", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", + "missing_configuration": "Soma \u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/da.json b/homeassistant/components/zha/.translations/da.json index 0b800ecd80a..39f254ac9af 100644 --- a/homeassistant/components/zha/.translations/da.json +++ b/homeassistant/components/zha/.translations/da.json @@ -29,7 +29,10 @@ "button_3": "Tredje knap", "button_4": "Fjerde knap", "button_5": "Femte knap", + "button_6": "Sjette knap", "close": "Luk", + "dim_down": "D\u00e6mp ned", + "dim_up": "D\u00e6mp op", "left": "Venstre", "open": "\u00c5ben", "right": "H\u00f8jre" From c6b08b28b2e07d02d3c6d1e3279dad36295c8a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20Gonz=C3=A1lez=20Calleja?= Date: Fri, 4 Oct 2019 02:44:07 +0200 Subject: [PATCH 014/639] Fix homekit temperaturesensor round (#27047) * Fix homekit temperature sensor for round with one decimal * Removing unnecesary operations * Adapting tests for new temperature_to_homekit() result precision --- homeassistant/components/homekit/type_sensors.py | 2 +- homeassistant/components/homekit/util.py | 2 +- tests/components/homekit/test_type_thermostats.py | 6 +++--- tests/components/homekit/test_util.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 87c8d5247a5..a1450518e0c 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -96,7 +96,7 @@ class TemperatureSensor(HomeAccessory): temperature = temperature_to_homekit(temperature, unit) self.char_temp.set_value(temperature) _LOGGER.debug( - "%s: Current temperature set to %d°C", self.entity_id, temperature + "%s: Current temperature set to %.1f°C", self.entity_id, temperature ) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index d60c94d420d..608c9a974e5 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -235,7 +235,7 @@ def convert_to_float(state): def temperature_to_homekit(temperature, unit): """Convert temperature to Celsius for HomeKit.""" - return round(temp_util.convert(temperature, unit, TEMP_CELSIUS) * 2) / 2 + return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) def temperature_to_states(temperature, unit): diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index d967d325561..8ad46e489d6 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -96,10 +96,10 @@ async def test_thermostat(hass, hk_driver, cls, events): }, ) await hass.async_block_till_done() - assert acc.char_target_temp.value == 22.0 + assert acc.char_target_temp.value == 22.2 assert acc.char_current_heat_cool.value == 1 assert acc.char_target_heat_cool.value == 1 - assert acc.char_current_temp.value == 18.0 + assert acc.char_current_temp.value == 17.8 assert acc.char_display_units.value == 0 hass.states.async_set( @@ -432,7 +432,7 @@ async def test_thermostat_fahrenheit(hass, hk_driver, cls, events): ) await hass.async_block_till_done() assert acc.get_temperature_range() == (7.0, 35.0) - assert acc.char_heating_thresh_temp.value == 20.0 + assert acc.char_heating_thresh_temp.value == 20.1 assert acc.char_cooling_thresh_temp.value == 24.0 assert acc.char_current_temp.value == 23.0 assert acc.char_target_temp.value == 22.0 diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 923cbaca42f..8898f988f9a 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -173,7 +173,7 @@ def test_convert_to_float(): def test_temperature_to_homekit(): """Test temperature conversion from HA to HomeKit.""" assert temperature_to_homekit(20.46, TEMP_CELSIUS) == 20.5 - assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.5 + assert temperature_to_homekit(92.1, TEMP_FAHRENHEIT) == 33.4 def test_temperature_to_states(): From d36d123cf7c64f4c48be5a65a704a512b99248da Mon Sep 17 00:00:00 2001 From: Hugh Eaves Date: Thu, 3 Oct 2019 21:01:06 -0400 Subject: [PATCH 015/639] Support zone expanders in alarmdecoder (#27167) --- homeassistant/components/alarmdecoder/__init__.py | 4 ++-- homeassistant/components/alarmdecoder/binary_sensor.py | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index e0ff80ae9fa..61cb0effe53 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -174,7 +174,7 @@ def setup(hass, config): hass.helpers.dispatcher.dispatcher_send(SIGNAL_ZONE_RESTORE, zone) def handle_rel_message(sender, message): - """Handle relay message from AlarmDecoder.""" + """Handle relay or zone expander message from AlarmDecoder.""" hass.helpers.dispatcher.dispatcher_send(SIGNAL_REL_MESSAGE, message) controller = False @@ -195,7 +195,7 @@ def setup(hass, config): controller.on_zone_fault += zone_fault_callback controller.on_zone_restore += zone_restore_callback controller.on_close += handle_closed_connection - controller.on_relay_changed += handle_rel_message + controller.on_expander_message += handle_rel_message hass.data[DATA_AD] = controller diff --git a/homeassistant/components/alarmdecoder/binary_sensor.py b/homeassistant/components/alarmdecoder/binary_sensor.py index bbcc4fd6eae..dc3f16b7d22 100644 --- a/homeassistant/components/alarmdecoder/binary_sensor.py +++ b/homeassistant/components/alarmdecoder/binary_sensor.py @@ -151,10 +151,15 @@ class AlarmDecoderBinarySensor(BinarySensorDevice): self.schedule_update_ha_state() def _rel_message_callback(self, message): - """Update relay state.""" + """Update relay / expander state.""" + if self._relay_addr == message.address and self._relay_chan == message.channel: _LOGGER.debug( - "Relay %d:%d value:%d", message.address, message.channel, message.value + "%s %d:%d value:%d", + "Relay" if message.type == message.RELAY else "ZoneExpander", + message.address, + message.channel, + message.value, ) self._state = message.value self.schedule_update_ha_state() From f50036772196a1a520f19137be28504ca9edf03f Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 4 Oct 2019 01:10:26 +0100 Subject: [PATCH 016/639] Handle all single zone thermostats (#27168) --- homeassistant/components/evohome/climate.py | 17 ++++++++--------- .../components/evohome/water_heater.py | 4 +++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index e5c8c6af14b..7df2db1b17e 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -75,21 +75,20 @@ async def async_setup_platform( loc_idx = broker.params[CONF_LOCATION_IDX] _LOGGER.debug( - "Found Location/Controller, id=%s [%s], name=%s (location_idx=%s)", - broker.tcs.systemId, + "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)", broker.tcs.modelType, + broker.tcs.systemId, broker.tcs.location.name, loc_idx, ) - # special case of RoundThermostat (is single zone) - if broker.config["zones"][0]["modelType"] == "RoundModulation": + # special case of RoundModulation/RoundWireless (is a single zone system) + if broker.config["zones"][0]["zoneType"] == "Thermostat": zone = list(broker.tcs.zones.values())[0] _LOGGER.debug( - "Found %s, id=%s [%s], name=%s", - zone.zoneType, - zone.zoneId, + "Found the Thermostat (%s), id=%s, name=%s", zone.modelType, + zone.zoneId, zone.name, ) @@ -101,10 +100,10 @@ async def async_setup_platform( zones = [] for zone in broker.tcs.zones.values(): _LOGGER.debug( - "Found %s, id=%s [%s], name=%s", + "Found a %s (%s), id=%s, name=%s", zone.zoneType, - zone.zoneId, zone.modelType, + zone.zoneId, zone.name, ) zones.append(EvoZone(broker, zone)) diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index b65665eb2c9..37bdcd82afc 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -34,7 +34,9 @@ async def async_setup_platform( broker = hass.data[DOMAIN]["broker"] _LOGGER.debug( - "Found %s, id: %s", broker.tcs.hotwater.zone_type, broker.tcs.hotwater.zoneId + "Found the DHW Controller (%s), id: %s", + broker.tcs.hotwater.zone_type, + broker.tcs.hotwater.zoneId, ) evo_dhw = EvoDHW(broker, broker.tcs.hotwater) From 98eaecf61dfd4a18a2b76401d56228666647981f Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Fri, 4 Oct 2019 02:31:45 -0400 Subject: [PATCH 017/639] Add device registry support to ecobee integration (#27109) * Add manufacturer const * Add device_info to binary sensor * Add device info to climate * Add device info to sensor * Add device info to weather * Add constant for device info * Fix log messages * Use guard clauses --- .../components/ecobee/binary_sensor.py | 40 ++++++++++++++++++- homeassistant/components/ecobee/climate.py | 25 +++++++++++- homeassistant/components/ecobee/const.py | 14 +++++++ homeassistant/components/ecobee/sensor.py | 40 ++++++++++++++++++- homeassistant/components/ecobee/weather.py | 26 +++++++++++- 5 files changed, 141 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 68d8a88df47..97ba94aaa70 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -4,7 +4,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, ) -from .const import DOMAIN +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -52,6 +52,44 @@ class EcobeeBinarySensor(BinarySensorDevice): return f"{sensor['code']}-{self.device_class}" return f"{sensor['id']}-{self.device_class}" + @property + def device_info(self): + """Return device information for this sensor.""" + identifier = None + model = None + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue + if "code" in sensor: + identifier = sensor["code"] + model = "ecobee Room Sensor" + else: + thermostat = self.data.ecobee.get_thermostat(self.index) + identifier = thermostat["identifier"] + try: + model = ( + f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" + ) + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + thermostat["name"], + thermostat["modelNumber"], + ) + break + + if identifier is not None and model is not None: + return { + "identifiers": {(DOMAIN, identifier)}, + "name": self.sensor_name, + "manufacturer": MANUFACTURER, + "model": model, + } + return None + @property def is_on(self): """Return the status of the sensor.""" diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index f930282ba7b..491fd8d686a 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -36,7 +36,7 @@ from homeassistant.const import ( from homeassistant.util.temperature import convert import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, _LOGGER +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER from .util import ecobee_date, ecobee_time ATTR_COOL_TEMP = "cool_temp" @@ -310,6 +310,29 @@ class Thermostat(ClimateDevice): """Return a unique identifier for this ecobee thermostat.""" return self.thermostat["identifier"] + @property + def device_info(self): + """Return device information for this ecobee thermostat.""" + try: + model = f"{ECOBEE_MODEL_TO_NAME[self.thermostat['modelNumber']]} Thermostat" + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + self.name, + self.thermostat["modelNumber"], + ) + return None + + return { + "identifiers": {(DOMAIN, self.thermostat["identifier"])}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": model, + } + @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index c3a23099b8a..411f5ddeeeb 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -9,4 +9,18 @@ DATA_ECOBEE_CONFIG = "ecobee_config" CONF_INDEX = "index" CONF_REFRESH_TOKEN = "refresh_token" +ECOBEE_MODEL_TO_NAME = { + "idtSmart": "ecobee Smart", + "idtEms": "ecobee Smart EMS", + "siSmart": "ecobee Si Smart", + "siEms": "ecobee Si EMS", + "athenaSmart": "ecobee3 Smart", + "athenaEms": "ecobee3 EMS", + "corSmart": "Carrier/Bryant Cor", + "nikeSmart": "ecobee3 lite Smart", + "nikeEms": "ecobee3 lite EMS", +} + ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] + +MANUFACTURER = "ecobee" diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 8cf9af0e3b4..27a8ae3ef07 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -8,7 +8,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity import Entity -from .const import DOMAIN +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER SENSOR_TYPES = { "temperature": ["Temperature", TEMP_FAHRENHEIT], @@ -63,6 +63,44 @@ class EcobeeSensor(Entity): return f"{sensor['code']}-{self.device_class}" return f"{sensor['id']}-{self.device_class}" + @property + def device_info(self): + """Return device information for this sensor.""" + identifier = None + model = None + for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue + if "code" in sensor: + identifier = sensor["code"] + model = "ecobee Room Sensor" + else: + thermostat = self.data.ecobee.get_thermostat(self.index) + identifier = thermostat["identifier"] + try: + model = ( + f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" + ) + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + thermostat["name"], + thermostat["modelNumber"], + ) + break + + if identifier is not None and model is not None: + return { + "identifiers": {(DOMAIN, identifier)}, + "name": self.sensor_name, + "manufacturer": MANUFACTURER, + "model": model, + } + return None + @property def device_class(self): """Return the device class of the sensor.""" diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 6175405638e..53e9842aae7 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -13,7 +13,7 @@ from homeassistant.components.weather import ( ) from homeassistant.const import TEMP_FAHRENHEIT -from .const import DOMAIN +from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER ATTR_FORECAST_TEMP_HIGH = "temphigh" ATTR_FORECAST_PRESSURE = "pressure" @@ -66,6 +66,30 @@ class EcobeeWeather(WeatherEntity): """Return a unique identifier for the weather platform.""" return self.data.ecobee.get_thermostat(self._index)["identifier"] + @property + def device_info(self): + """Return device information for the ecobee weather platform.""" + thermostat = self.data.ecobee.get_thermostat(self._index) + try: + model = f"{ECOBEE_MODEL_TO_NAME[thermostat['modelNumber']]} Thermostat" + except KeyError: + _LOGGER.error( + "Model number for ecobee thermostat %s not recognized. " + "Please visit this link and provide the following information: " + "https://github.com/home-assistant/home-assistant/issues/27172 " + "Unrecognized model number: %s", + thermostat["name"], + thermostat["modelNumber"], + ) + return None + + return { + "identifiers": {(DOMAIN, thermostat["identifier"])}, + "name": self.name, + "manufacturer": MANUFACTURER, + "model": model, + } + @property def condition(self): """Return the current condition.""" From 4b4a290f718ca05d06b64368d9feba7ac9385fb1 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Fri, 4 Oct 2019 01:37:30 -0700 Subject: [PATCH 018/639] WAQI add unique ID and availability (#27086) * WAQI add unique ID and availability * Review comments * Fix unique ID * Fix unique ID --- homeassistant/components/waqi/sensor.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index dbfe6de1a60..9f3c3ffc13e 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -128,6 +128,16 @@ class WaqiSensor(Entity): return self._data.get("aqi") return None + @property + def available(self): + """Return sensor availability.""" + return self._data is not None + + @property + def unique_id(self): + """Return unique ID.""" + return self.uid + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" From 8ba4ee1012fcdbfe7b238ef1ac4de34e09adf641 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 4 Oct 2019 13:58:29 +0200 Subject: [PATCH 019/639] Add Airly integration (#26375) * Add Airly integration * Update .coveragerc file * Remove AVAILABLE_CONDITIONS and fix device_class * Don't create client on every update * Rename client to session * Rename state_attributes to device_state_attributes * Remove log latitude and longitude * Fix try...except * Change latitude and longitude to HA defaults * _show_config_form doesn't need coroutine * Simplify config_flow errors handlig * Preetier * Remove unnecessary condition * Change sensor platform to air_quality * Remove PM1 * Make unique_id more unique * Remove , * Add tests for config_flow * Move conf to CONFIG * Remove domain from unique_id * Change the way update of attrs * Language and attrs * Fix attrs * Add aiohttp error handling * Throttle as decorator * Suggested change * Suggested change * Invert condition * Cleaning * Add tests * Polish no sesnor error handling * Better strings * Fix test_invalid_api_key * Fix documentation url * Remove unnecessary test * Remove language option * Fix test_invalid_api_key once again * Sort imports * Remove splits in strings --- .coveragerc | 3 + CODEOWNERS | 1 + homeassistant/components/airly/__init__.py | 21 + homeassistant/components/airly/air_quality.py | 201 ++ homeassistant/components/airly/config_flow.py | 114 ++ homeassistant/components/airly/const.py | 4 + homeassistant/components/airly/manifest.json | 9 + homeassistant/components/airly/strings.json | 22 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/airly/__init__.py | 1 + tests/components/airly/test_config_flow.py | 93 + tests/fixtures/airly_no_station.json | 642 ++++++ tests/fixtures/airly_valid_station.json | 1726 +++++++++++++++++ 16 files changed, 2845 insertions(+) create mode 100644 homeassistant/components/airly/__init__.py create mode 100644 homeassistant/components/airly/air_quality.py create mode 100644 homeassistant/components/airly/config_flow.py create mode 100644 homeassistant/components/airly/const.py create mode 100644 homeassistant/components/airly/manifest.json create mode 100644 homeassistant/components/airly/strings.json create mode 100644 tests/components/airly/__init__.py create mode 100644 tests/components/airly/test_config_flow.py create mode 100644 tests/fixtures/airly_no_station.json create mode 100644 tests/fixtures/airly_valid_station.json diff --git a/.coveragerc b/.coveragerc index aa8f2d8c03d..25ee023ac84 100644 --- a/.coveragerc +++ b/.coveragerc @@ -19,6 +19,9 @@ omit = homeassistant/components/adguard/switch.py homeassistant/components/ads/* homeassistant/components/aftership/sensor.py + homeassistant/components/airly/__init__.py + homeassistant/components/airly/air_quality.py + homeassistant/components/airly/const.py homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/cover.py homeassistant/components/alarm_control_panel/manual_mqtt.py diff --git a/CODEOWNERS b/CODEOWNERS index 418eb745ecb..8073020712d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -14,6 +14,7 @@ homeassistant/scripts/check_config.py @kellerza # Integrations homeassistant/components/adguard/* @frenck +homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya homeassistant/components/alarm_control_panel/* @colinodell homeassistant/components/alexa/* @home-assistant/cloud diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py new file mode 100644 index 00000000000..56b3477ac89 --- /dev/null +++ b/homeassistant/components/airly/__init__.py @@ -0,0 +1,21 @@ +"""The Airly component.""" +from homeassistant.core import Config, HomeAssistant + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Set up configured Airly.""" + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Airly as config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") + ) + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality") + return True diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py new file mode 100644 index 00000000000..a8ec82ab304 --- /dev/null +++ b/homeassistant/components/airly/air_quality.py @@ -0,0 +1,201 @@ +"""Support for the Airly service.""" +import asyncio +import logging +from datetime import timedelta + +import async_timeout +from aiohttp.client_exceptions import ClientConnectorError +from airly import Airly +from airly.exceptions import AirlyError + +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.components.air_quality import ( + AirQualityEntity, + ATTR_AQI, + ATTR_PM_10, + ATTR_PM_2_5, +) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import Throttle + +from .const import NO_AIRLY_SENSORS + +_LOGGER = logging.getLogger(__name__) + +ATTRIBUTION = "Data provided by Airly" + +ATTR_API_ADVICE = "ADVICE" +ATTR_API_CAQI = "CAQI" +ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION" +ATTR_API_CAQI_LEVEL = "LEVEL" +ATTR_API_PM10 = "PM10" +ATTR_API_PM10_LIMIT = "PM10_LIMIT" +ATTR_API_PM10_PERCENT = "PM10_PERCENT" +ATTR_API_PM25 = "PM25" +ATTR_API_PM25_LIMIT = "PM25_LIMIT" +ATTR_API_PM25_PERCENT = "PM25_PERCENT" + +LABEL_ADVICE = "advice" +LABEL_AQI_LEVEL = f"{ATTR_AQI}_level" +LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit" +LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit" +LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit" +LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a Airly entities from a config_entry.""" + api_key = config_entry.data[CONF_API_KEY] + name = config_entry.data[CONF_NAME] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + + websession = async_get_clientsession(hass) + + data = AirlyData(websession, api_key, latitude, longitude) + + async_add_entities([AirlyAirQuality(data, name)], True) + + +def round_state(func): + """Round state.""" + + def _decorator(self): + res = func(self) + if isinstance(res, float): + return round(res) + return res + + return _decorator + + +class AirlyAirQuality(AirQualityEntity): + """Define an Airly air_quality.""" + + def __init__(self, airly, name): + """Initialize.""" + self.airly = airly + self.data = airly.data + self._name = name + self._pm_2_5 = None + self._pm_10 = None + self._aqi = None + self._icon = "mdi:blur" + self._attrs = {} + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + @round_state + def air_quality_index(self): + """Return the air quality index.""" + return self._aqi + + @property + @round_state + def particulate_matter_2_5(self): + """Return the particulate matter 2.5 level.""" + return self._pm_2_5 + + @property + @round_state + def particulate_matter_10(self): + """Return the particulate matter 10 level.""" + return self._pm_10 + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def state(self): + """Return the CAQI description.""" + return self.data[ATTR_API_CAQI_DESCRIPTION] + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self.airly.latitude}-{self.airly.longitude}" + + @property + def available(self): + """Return True if entity is available.""" + return bool(self.airly.data) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + self._attrs[LABEL_ADVICE] = self.data[ATTR_API_ADVICE] + self._attrs[LABEL_AQI_LEVEL] = self.data[ATTR_API_CAQI_LEVEL] + self._attrs[LABEL_PM_2_5_LIMIT] = self.data[ATTR_API_PM25_LIMIT] + self._attrs[LABEL_PM_2_5_PERCENT] = round(self.data[ATTR_API_PM25_PERCENT]) + self._attrs[LABEL_PM_10_LIMIT] = self.data[ATTR_API_PM10_LIMIT] + self._attrs[LABEL_PM_10_PERCENT] = round(self.data[ATTR_API_PM10_PERCENT]) + return self._attrs + + async def async_update(self): + """Get the data from Airly.""" + await self.airly.async_update() + + self._pm_10 = self.data[ATTR_API_PM10] + self._pm_2_5 = self.data[ATTR_API_PM25] + self._aqi = self.data[ATTR_API_CAQI] + + +class AirlyData: + """Define an object to hold sensor data.""" + + def __init__(self, session, api_key, latitude, longitude): + """Initialize.""" + self.latitude = latitude + self.longitude = longitude + self.airly = Airly(api_key, session) + self.data = {} + + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update Airly data.""" + + try: + with async_timeout.timeout(10): + measurements = self.airly.create_measurements_session_point( + self.latitude, self.longitude + ) + await measurements.update() + + values = measurements.current["values"] + index = measurements.current["indexes"][0] + standards = measurements.current["standards"] + + if index["description"] == NO_AIRLY_SENSORS: + _LOGGER.error("Can't retrieve data: no Airly sensors in this area") + return + for value in values: + self.data[value["name"]] = value["value"] + for standard in standards: + self.data[f"{standard['pollutant']}_LIMIT"] = standard["limit"] + self.data[f"{standard['pollutant']}_PERCENT"] = standard["percent"] + self.data[ATTR_API_CAQI] = index["value"] + self.data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ") + self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"] + self.data[ATTR_API_ADVICE] = index["advice"] + _LOGGER.debug("Data retrieved from Airly") + except ( + ValueError, + AirlyError, + asyncio.TimeoutError, + ClientConnectorError, + ) as error: + _LOGGER.error(error) + self.data = {} diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py new file mode 100644 index 00000000000..b361930fa7d --- /dev/null +++ b/homeassistant/components/airly/config_flow.py @@ -0,0 +1,114 @@ +"""Adds config flow for Airly.""" +import async_timeout +import voluptuous as vol +from airly import Airly +from airly.exceptions import AirlyError + +import homeassistant.helpers.config_validation as cv +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DEFAULT_NAME, DOMAIN, NO_AIRLY_SENSORS + + +@callback +def configured_instances(hass): + """Return a set of configured Airly instances.""" + return set( + entry.data[CONF_NAME] for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Airly.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize.""" + self._errors = {} + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + self._errors = {} + + websession = async_get_clientsession(self.hass) + + if user_input is not None: + if user_input[CONF_NAME] in configured_instances(self.hass): + self._errors[CONF_NAME] = "name_exists" + api_key_valid = await self._test_api_key(websession, user_input["api_key"]) + if not api_key_valid: + self._errors["base"] = "auth" + else: + location_valid = await self._test_location( + websession, + user_input["api_key"], + user_input["latitude"], + user_input["longitude"], + ) + if not location_valid: + self._errors["base"] = "wrong_location" + + if not self._errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + + return self._show_config_form( + name=DEFAULT_NAME, + api_key="", + latitude=self.hass.config.latitude, + longitude=self.hass.config.longitude, + ) + + def _show_config_form(self, name=None, api_key=None, latitude=None, longitude=None): + """Show the configuration form to edit data.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY, default=api_key): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Optional(CONF_NAME, default=name): str, + } + ), + errors=self._errors, + ) + + async def _test_api_key(self, client, api_key): + """Return true if api_key is valid.""" + + with async_timeout.timeout(10): + airly = Airly(api_key, client) + measurements = airly.create_measurements_session_point( + latitude=52.24131, longitude=20.99101 + ) + try: + await measurements.update() + except AirlyError: + return False + return True + + async def _test_location(self, client, api_key, latitude, longitude): + """Return true if location is valid.""" + + with async_timeout.timeout(10): + airly = Airly(api_key, client) + measurements = airly.create_measurements_session_point( + latitude=latitude, longitude=longitude + ) + + await measurements.update() + current = measurements.current + if current["indexes"][0]["description"] == NO_AIRLY_SENSORS: + return False + return True diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py new file mode 100644 index 00000000000..5313ba0e494 --- /dev/null +++ b/homeassistant/components/airly/const.py @@ -0,0 +1,4 @@ +"""Constants for Airly integration.""" +DEFAULT_NAME = "Airly" +DOMAIN = "airly" +NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." diff --git a/homeassistant/components/airly/manifest.json b/homeassistant/components/airly/manifest.json new file mode 100644 index 00000000000..1859f084bf1 --- /dev/null +++ b/homeassistant/components/airly/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "airly", + "name": "Airly", + "documentation": "https://www.home-assistant.io/integrations/airly", + "dependencies": [], + "codeowners": ["@bieniu"], + "requirements": ["airly==0.0.2"], + "config_flow": true +} diff --git a/homeassistant/components/airly/strings.json b/homeassistant/components/airly/strings.json new file mode 100644 index 00000000000..116b6df83e6 --- /dev/null +++ b/homeassistant/components/airly/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "Airly", + "step": { + "user": { + "title": "Airly", + "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "data": { + "name": "Name of the integration", + "api_key": "Airly API key", + "latitude": "Latitude", + "longitude": "Longitude" + } + } + }, + "error": { + "name_exists": "Name already exists.", + "wrong_location": "No Airly measuring stations in this area.", + "auth": "API key is not correct." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 21f57934e95..7557fc32b40 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -7,6 +7,7 @@ To update, run python3 -m script.hassfest FLOWS = [ "adguard", + "airly", "ambiclimate", "ambient_station", "axis", diff --git a/requirements_all.txt b/requirements_all.txt index 208eeac899b..5731158cd2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -184,6 +184,9 @@ aiounifi==11 # homeassistant.components.wwlln aiowwlln==2.0.2 +# homeassistant.components.airly +airly==0.0.2 + # homeassistant.components.aladdin_connect aladdin_connect==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9bc9870be10..1e948ded8a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -76,6 +76,9 @@ aiounifi==11 # homeassistant.components.wwlln aiowwlln==2.0.2 +# homeassistant.components.airly +airly==0.0.2 + # homeassistant.components.ambiclimate ambiclimate==0.2.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e35a83bd24d..a6f2584e256 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -54,6 +54,7 @@ TEST_REQUIREMENTS = ( "aioswitcher", "aiounifi", "aiowwlln", + "airly", "ambiclimate", "androidtv", "apns2", diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py new file mode 100644 index 00000000000..f31dfb7712d --- /dev/null +++ b/tests/components/airly/__init__.py @@ -0,0 +1 @@ +"""Tests for Airly.""" diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py new file mode 100644 index 00000000000..8b615b34c2a --- /dev/null +++ b/tests/components/airly/test_config_flow.py @@ -0,0 +1,93 @@ +"""Define tests for the Airly config flow.""" +import json + +from airly.exceptions import AirlyError +from asynctest import patch + +from homeassistant import data_entry_flow +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.components.airly import config_flow +from homeassistant.components.airly.const import DOMAIN + +from tests.common import load_fixture, MockConfigEntry + +CONFIG = { + CONF_NAME: "abcd", + CONF_API_KEY: "foo", + CONF_LATITUDE: 123, + CONF_LONGITUDE: 456, +} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_invalid_api_key(hass): + """Test that errors are shown when API key is invalid.""" + with patch( + "airly._private._RequestsHandler.get", + side_effect=AirlyError(403, {"message": "Invalid authentication credentials"}), + ): + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {"base": "auth"} + + +async def test_invalid_location(hass): + """Test that errors are shown when location is invalid.""" + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_no_station.json")), + ): + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {"base": "wrong_location"} + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_valid_station.json")), + ): + MockConfigEntry(domain=DOMAIN, data=CONFIG).add_to_hass(hass) + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["errors"] == {CONF_NAME: "name_exists"} + + +async def test_create_entry(hass): + """Test that the user step works.""" + + with patch( + "airly._private._RequestsHandler.get", + return_value=json.loads(load_fixture("airly_valid_station.json")), + ): + flow = config_flow.AirlyFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=CONFIG) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONFIG[CONF_NAME] + assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] + assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] + assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] diff --git a/tests/fixtures/airly_no_station.json b/tests/fixtures/airly_no_station.json new file mode 100644 index 00000000000..cc64934938f --- /dev/null +++ b/tests/fixtures/airly_no_station.json @@ -0,0 +1,642 @@ +{ + "current": { + "fromDateTime": "2019-10-02T05:53:00.608Z", + "tillDateTime": "2019-10-02T06:53:00.608Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, + "history": [{ + "fromDateTime": "2019-10-01T06:00:00.000Z", + "tillDateTime": "2019-10-01T07:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T07:00:00.000Z", + "tillDateTime": "2019-10-01T08:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T08:00:00.000Z", + "tillDateTime": "2019-10-01T09:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T09:00:00.000Z", + "tillDateTime": "2019-10-01T10:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T10:00:00.000Z", + "tillDateTime": "2019-10-01T11:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T11:00:00.000Z", + "tillDateTime": "2019-10-01T12:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T12:00:00.000Z", + "tillDateTime": "2019-10-01T13:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T13:00:00.000Z", + "tillDateTime": "2019-10-01T14:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T14:00:00.000Z", + "tillDateTime": "2019-10-01T15:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T15:00:00.000Z", + "tillDateTime": "2019-10-01T16:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T16:00:00.000Z", + "tillDateTime": "2019-10-01T17:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T17:00:00.000Z", + "tillDateTime": "2019-10-01T18:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T18:00:00.000Z", + "tillDateTime": "2019-10-01T19:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T19:00:00.000Z", + "tillDateTime": "2019-10-01T20:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T20:00:00.000Z", + "tillDateTime": "2019-10-01T21:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T21:00:00.000Z", + "tillDateTime": "2019-10-01T22:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T22:00:00.000Z", + "tillDateTime": "2019-10-01T23:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-01T23:00:00.000Z", + "tillDateTime": "2019-10-02T00:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T00:00:00.000Z", + "tillDateTime": "2019-10-02T01:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T01:00:00.000Z", + "tillDateTime": "2019-10-02T02:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T02:00:00.000Z", + "tillDateTime": "2019-10-02T03:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T03:00:00.000Z", + "tillDateTime": "2019-10-02T04:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T04:00:00.000Z", + "tillDateTime": "2019-10-02T05:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T05:00:00.000Z", + "tillDateTime": "2019-10-02T06:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }], + "forecast": [{ + "fromDateTime": "2019-10-02T06:00:00.000Z", + "tillDateTime": "2019-10-02T07:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T07:00:00.000Z", + "tillDateTime": "2019-10-02T08:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T08:00:00.000Z", + "tillDateTime": "2019-10-02T09:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T09:00:00.000Z", + "tillDateTime": "2019-10-02T10:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T10:00:00.000Z", + "tillDateTime": "2019-10-02T11:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T11:00:00.000Z", + "tillDateTime": "2019-10-02T12:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T12:00:00.000Z", + "tillDateTime": "2019-10-02T13:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T13:00:00.000Z", + "tillDateTime": "2019-10-02T14:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T14:00:00.000Z", + "tillDateTime": "2019-10-02T15:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T15:00:00.000Z", + "tillDateTime": "2019-10-02T16:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T16:00:00.000Z", + "tillDateTime": "2019-10-02T17:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T17:00:00.000Z", + "tillDateTime": "2019-10-02T18:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T18:00:00.000Z", + "tillDateTime": "2019-10-02T19:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T19:00:00.000Z", + "tillDateTime": "2019-10-02T20:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T20:00:00.000Z", + "tillDateTime": "2019-10-02T21:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T21:00:00.000Z", + "tillDateTime": "2019-10-02T22:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T22:00:00.000Z", + "tillDateTime": "2019-10-02T23:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-02T23:00:00.000Z", + "tillDateTime": "2019-10-03T00:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T00:00:00.000Z", + "tillDateTime": "2019-10-03T01:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T01:00:00.000Z", + "tillDateTime": "2019-10-03T02:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T02:00:00.000Z", + "tillDateTime": "2019-10-03T03:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T03:00:00.000Z", + "tillDateTime": "2019-10-03T04:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T04:00:00.000Z", + "tillDateTime": "2019-10-03T05:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }, { + "fromDateTime": "2019-10-03T05:00:00.000Z", + "tillDateTime": "2019-10-03T06:00:00.000Z", + "values": [], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": null, + "level": "UNKNOWN", + "description": "There are no Airly sensors in this area yet.", + "advice": null, + "color": "#999999" + }], + "standards": [] + }] +} \ No newline at end of file diff --git a/tests/fixtures/airly_valid_station.json b/tests/fixtures/airly_valid_station.json new file mode 100644 index 00000000000..656c62c04c2 --- /dev/null +++ b/tests/fixtures/airly_valid_station.json @@ -0,0 +1,1726 @@ +{ + "current": { + "fromDateTime": "2019-10-02T05:54:57.204Z", + "tillDateTime": "2019-10-02T06:54:57.204Z", + "values": [{ + "name": "PM1", + "value": 9.23 + }, { + "name": "PM25", + "value": 13.71 + }, { + "name": "PM10", + "value": 18.58 + }, { + "name": "PRESSURE", + "value": 1000.87 + }, { + "name": "HUMIDITY", + "value": 92.84 + }, { + "name": "TEMPERATURE", + "value": 14.23 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.85, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 54.84 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 37.17 + }] + }, + "history": [{ + "fromDateTime": "2019-10-01T06:00:00.000Z", + "tillDateTime": "2019-10-01T07:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 5.95 + }, { + "name": "PM25", + "value": 8.54 + }, { + "name": "PM10", + "value": 11.46 + }, { + "name": "PRESSURE", + "value": 1009.61 + }, { + "name": "HUMIDITY", + "value": 97.6 + }, { + "name": "TEMPERATURE", + "value": 9.71 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 14.24, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green equals clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 34.18 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 22.91 + }] + }, { + "fromDateTime": "2019-10-01T07:00:00.000Z", + "tillDateTime": "2019-10-01T08:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 4.2 + }, { + "name": "PM25", + "value": 5.88 + }, { + "name": "PM10", + "value": 7.88 + }, { + "name": "PRESSURE", + "value": 1009.13 + }, { + "name": "HUMIDITY", + "value": 90.84 + }, { + "name": "TEMPERATURE", + "value": 12.65 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.81, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 23.53 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 15.75 + }] + }, { + "fromDateTime": "2019-10-01T08:00:00.000Z", + "tillDateTime": "2019-10-01T09:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 3.63 + }, { + "name": "PM25", + "value": 5.56 + }, { + "name": "PM10", + "value": 7.71 + }, { + "name": "PRESSURE", + "value": 1008.27 + }, { + "name": "HUMIDITY", + "value": 84.61 + }, { + "name": "TEMPERATURE", + "value": 15.57 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.26, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 22.23 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 15.42 + }] + }, { + "fromDateTime": "2019-10-01T09:00:00.000Z", + "tillDateTime": "2019-10-01T10:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.9 + }, { + "name": "PM25", + "value": 3.93 + }, { + "name": "PM10", + "value": 5.24 + }, { + "name": "PRESSURE", + "value": 1007.57 + }, { + "name": "HUMIDITY", + "value": 79.52 + }, { + "name": "TEMPERATURE", + "value": 16.57 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 6.56, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deep! The air is clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 15.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.48 + }] + }, { + "fromDateTime": "2019-10-01T10:00:00.000Z", + "tillDateTime": "2019-10-01T11:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.45 + }, { + "name": "PM25", + "value": 3.33 + }, { + "name": "PM10", + "value": 4.52 + }, { + "name": "PRESSURE", + "value": 1006.75 + }, { + "name": "HUMIDITY", + "value": 74.09 + }, { + "name": "TEMPERATURE", + "value": 16.95 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.55, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is grand today. ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 13.31 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 9.04 + }] + }, { + "fromDateTime": "2019-10-01T11:00:00.000Z", + "tillDateTime": "2019-10-01T12:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.0 + }, { + "name": "PM25", + "value": 2.93 + }, { + "name": "PM10", + "value": 3.98 + }, { + "name": "PRESSURE", + "value": 1005.71 + }, { + "name": "HUMIDITY", + "value": 69.06 + }, { + "name": "TEMPERATURE", + "value": 17.31 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 4.89, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green equals clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 11.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.96 + }] + }, { + "fromDateTime": "2019-10-01T12:00:00.000Z", + "tillDateTime": "2019-10-01T13:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 1.92 + }, { + "name": "PM25", + "value": 2.69 + }, { + "name": "PM10", + "value": 3.68 + }, { + "name": "PRESSURE", + "value": 1005.03 + }, { + "name": "HUMIDITY", + "value": 65.08 + }, { + "name": "TEMPERATURE", + "value": 17.47 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 4.49, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.77 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.36 + }] + }, { + "fromDateTime": "2019-10-01T13:00:00.000Z", + "tillDateTime": "2019-10-01T14:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 1.79 + }, { + "name": "PM25", + "value": 2.57 + }, { + "name": "PM10", + "value": 3.53 + }, { + "name": "PRESSURE", + "value": 1004.26 + }, { + "name": "HUMIDITY", + "value": 63.72 + }, { + "name": "TEMPERATURE", + "value": 17.91 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 4.29, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.29 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 7.06 + }] + }, { + "fromDateTime": "2019-10-01T14:00:00.000Z", + "tillDateTime": "2019-10-01T15:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 2.06 + }, { + "name": "PM25", + "value": 3.08 + }, { + "name": "PM10", + "value": 4.23 + }, { + "name": "PRESSURE", + "value": 1003.46 + }, { + "name": "HUMIDITY", + "value": 64.44 + }, { + "name": "TEMPERATURE", + "value": 17.84 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.14, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is grand today. ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 12.33 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 8.47 + }] + }, { + "fromDateTime": "2019-10-01T15:00:00.000Z", + "tillDateTime": "2019-10-01T16:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 3.17 + }, { + "name": "PM25", + "value": 4.61 + }, { + "name": "PM10", + "value": 6.25 + }, { + "name": "PRESSURE", + "value": 1003.18 + }, { + "name": "HUMIDITY", + "value": 65.32 + }, { + "name": "TEMPERATURE", + "value": 18.08 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 7.68, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 18.44 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 12.5 + }] + }, { + "fromDateTime": "2019-10-01T16:00:00.000Z", + "tillDateTime": "2019-10-01T17:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 4.17 + }, { + "name": "PM25", + "value": 5.91 + }, { + "name": "PM10", + "value": 8.06 + }, { + "name": "PRESSURE", + "value": 1003.05 + }, { + "name": "HUMIDITY", + "value": 66.14 + }, { + "name": "TEMPERATURE", + "value": 17.04 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.84, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 23.62 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 16.11 + }] + }, { + "fromDateTime": "2019-10-01T17:00:00.000Z", + "tillDateTime": "2019-10-01T18:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 6.4 + }, { + "name": "PM25", + "value": 10.93 + }, { + "name": "PM10", + "value": 15.7 + }, { + "name": "PRESSURE", + "value": 1002.85 + }, { + "name": "HUMIDITY", + "value": 68.31 + }, { + "name": "TEMPERATURE", + "value": 16.33 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 18.22, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.4 + }] + }, { + "fromDateTime": "2019-10-01T18:00:00.000Z", + "tillDateTime": "2019-10-01T19:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 4.79 + }, { + "name": "PM25", + "value": 7.41 + }, { + "name": "PM10", + "value": 10.31 + }, { + "name": "PRESSURE", + "value": 1002.52 + }, { + "name": "HUMIDITY", + "value": 69.88 + }, { + "name": "TEMPERATURE", + "value": 15.98 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 12.35, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 29.65 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 20.63 + }] + }, { + "fromDateTime": "2019-10-01T19:00:00.000Z", + "tillDateTime": "2019-10-01T20:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 5.99 + }, { + "name": "PM25", + "value": 9.45 + }, { + "name": "PM10", + "value": 13.22 + }, { + "name": "PRESSURE", + "value": 1002.32 + }, { + "name": "HUMIDITY", + "value": 70.47 + }, { + "name": "TEMPERATURE", + "value": 15.76 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 15.74, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deeply!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 37.78 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 26.44 + }] + }, { + "fromDateTime": "2019-10-01T20:00:00.000Z", + "tillDateTime": "2019-10-01T21:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.35 + }, { + "name": "PM25", + "value": 14.67 + }, { + "name": "PM10", + "value": 20.57 + }, { + "name": "PRESSURE", + "value": 1002.46 + }, { + "name": "HUMIDITY", + "value": 72.61 + }, { + "name": "TEMPERATURE", + "value": 15.47 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 24.45, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 58.68 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.13 + }] + }, { + "fromDateTime": "2019-10-01T21:00:00.000Z", + "tillDateTime": "2019-10-01T22:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.95 + }, { + "name": "PM25", + "value": 15.37 + }, { + "name": "PM10", + "value": 21.33 + }, { + "name": "PRESSURE", + "value": 1002.59 + }, { + "name": "HUMIDITY", + "value": 75.09 + }, { + "name": "TEMPERATURE", + "value": 15.17 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.62, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Take a breath!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 61.48 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.66 + }] + }, { + "fromDateTime": "2019-10-01T22:00:00.000Z", + "tillDateTime": "2019-10-01T23:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 10.16 + }, { + "name": "PM25", + "value": 15.78 + }, { + "name": "PM10", + "value": 21.97 + }, { + "name": "PRESSURE", + "value": 1002.59 + }, { + "name": "HUMIDITY", + "value": 77.68 + }, { + "name": "TEMPERATURE", + "value": 14.9 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 26.31, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Great air for a walk to the park!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 63.14 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 43.93 + }] + }, { + "fromDateTime": "2019-10-01T23:00:00.000Z", + "tillDateTime": "2019-10-02T00:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.86 + }, { + "name": "PM25", + "value": 15.14 + }, { + "name": "PM10", + "value": 21.07 + }, { + "name": "PRESSURE", + "value": 1002.49 + }, { + "name": "HUMIDITY", + "value": 79.86 + }, { + "name": "TEMPERATURE", + "value": 14.56 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.24, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Leave the mask at home today!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 60.57 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.14 + }] + }, { + "fromDateTime": "2019-10-02T00:00:00.000Z", + "tillDateTime": "2019-10-02T01:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.77 + }, { + "name": "PM25", + "value": 15.04 + }, { + "name": "PM10", + "value": 20.97 + }, { + "name": "PRESSURE", + "value": 1002.18 + }, { + "name": "HUMIDITY", + "value": 81.77 + }, { + "name": "TEMPERATURE", + "value": 14.13 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.07, + "level": "LOW", + "description": "Air is quite good.", + "advice": "Time for a walk with friends or activities with your family - because the air is clean!", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 60.18 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.94 + }] + }, { + "fromDateTime": "2019-10-02T01:00:00.000Z", + "tillDateTime": "2019-10-02T02:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 9.67 + }, { + "name": "PM25", + "value": 14.9 + }, { + "name": "PM10", + "value": 20.67 + }, { + "name": "PRESSURE", + "value": 1002.01 + }, { + "name": "HUMIDITY", + "value": 84.5 + }, { + "name": "TEMPERATURE", + "value": 13.7 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 24.84, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 59.62 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 41.33 + }] + }, { + "fromDateTime": "2019-10-02T02:00:00.000Z", + "tillDateTime": "2019-10-02T03:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 7.17 + }, { + "name": "PM25", + "value": 10.7 + }, { + "name": "PM10", + "value": 14.58 + }, { + "name": "PRESSURE", + "value": 1001.56 + }, { + "name": "HUMIDITY", + "value": 88.55 + }, { + "name": "TEMPERATURE", + "value": 13.44 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 17.83, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Catch your breath!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 42.8 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 29.17 + }] + }, { + "fromDateTime": "2019-10-02T03:00:00.000Z", + "tillDateTime": "2019-10-02T04:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 6.99 + }, { + "name": "PM25", + "value": 10.23 + }, { + "name": "PM10", + "value": 13.66 + }, { + "name": "PRESSURE", + "value": 1001.34 + }, { + "name": "HUMIDITY", + "value": 90.82 + }, { + "name": "TEMPERATURE", + "value": 13.3 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 17.05, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Perfect air for exercising! Go for it!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 40.91 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.33 + }] + }, { + "fromDateTime": "2019-10-02T04:00:00.000Z", + "tillDateTime": "2019-10-02T05:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 7.82 + }, { + "name": "PM25", + "value": 11.59 + }, { + "name": "PM10", + "value": 15.77 + }, { + "name": "PRESSURE", + "value": 1000.92 + }, { + "name": "HUMIDITY", + "value": 91.8 + }, { + "name": "TEMPERATURE", + "value": 13.34 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.32, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 46.36 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.54 + }] + }, { + "fromDateTime": "2019-10-02T05:00:00.000Z", + "tillDateTime": "2019-10-02T06:00:00.000Z", + "values": [{ + "name": "PM1", + "value": 10.16 + }, { + "name": "PM25", + "value": 15.35 + }, { + "name": "PM10", + "value": 21.45 + }, { + "name": "PRESSURE", + "value": 1000.82 + }, { + "name": "HUMIDITY", + "value": 92.15 + }, { + "name": "TEMPERATURE", + "value": 13.74 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 25.59, + "level": "LOW", + "description": "Air is quite good.", + "advice": "How about going for a walk?", + "color": "#D1CF1E" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 61.42 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 42.9 + }] + }], + "forecast": [{ + "fromDateTime": "2019-10-02T06:00:00.000Z", + "tillDateTime": "2019-10-02T07:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.28 + }, { + "name": "PM10", + "value": 18.37 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.14, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "It couldn't be better ;)", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 53.13 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.73 + }] + }, { + "fromDateTime": "2019-10-02T07:00:00.000Z", + "tillDateTime": "2019-10-02T08:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.19 + }, { + "name": "PM10", + "value": 15.65 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 18.65, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 44.76 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 31.31 + }] + }, { + "fromDateTime": "2019-10-02T08:00:00.000Z", + "tillDateTime": "2019-10-02T09:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 8.79 + }, { + "name": "PM10", + "value": 12.8 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 14.65, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe deep! The air is clean!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 35.15 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 25.59 + }] + }, { + "fromDateTime": "2019-10-02T09:00:00.000Z", + "tillDateTime": "2019-10-02T10:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 5.46 + }, { + "name": "PM10", + "value": 8.91 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 9.11, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 21.86 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 17.83 + }] + }, { + "fromDateTime": "2019-10-02T10:00:00.000Z", + "tillDateTime": "2019-10-02T11:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 2.26 + }, { + "name": "PM10", + "value": 5.02 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 9.06 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.05 + }] + }, { + "fromDateTime": "2019-10-02T11:00:00.000Z", + "tillDateTime": "2019-10-02T12:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 1.06 + }, { + "name": "PM10", + "value": 2.52 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 2.52, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 4.22 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 5.05 + }] + }, { + "fromDateTime": "2019-10-02T12:00:00.000Z", + "tillDateTime": "2019-10-02T13:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 0.48 + }, { + "name": "PM10", + "value": 1.94 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 1.94, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 1.94 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 3.89 + }] + }, { + "fromDateTime": "2019-10-02T13:00:00.000Z", + "tillDateTime": "2019-10-02T14:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 0.63 + }, { + "name": "PM10", + "value": 2.26 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 2.26, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Enjoy life!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 2.53 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 4.52 + }] + }, { + "fromDateTime": "2019-10-02T14:00:00.000Z", + "tillDateTime": "2019-10-02T15:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 1.47 + }, { + "name": "PM10", + "value": 3.39 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 3.39, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 5.87 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 6.78 + }] + }, { + "fromDateTime": "2019-10-02T15:00:00.000Z", + "tillDateTime": "2019-10-02T16:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 2.62 + }, { + "name": "PM10", + "value": 5.02 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 5.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Great air!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 10.5 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 10.05 + }] + }, { + "fromDateTime": "2019-10-02T16:00:00.000Z", + "tillDateTime": "2019-10-02T17:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 3.89 + }, { + "name": "PM10", + "value": 8.02 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 8.02, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 15.56 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 16.04 + }] + }, { + "fromDateTime": "2019-10-02T17:00:00.000Z", + "tillDateTime": "2019-10-02T18:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 6.26 + }, { + "name": "PM10", + "value": 11.41 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 11.41, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 25.05 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 22.83 + }] + }, { + "fromDateTime": "2019-10-02T18:00:00.000Z", + "tillDateTime": "2019-10-02T19:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 8.69 + }, { + "name": "PM10", + "value": 14.48 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 14.48, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Zero dust - zero worries!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 34.76 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 28.96 + }] + }, { + "fromDateTime": "2019-10-02T19:00:00.000Z", + "tillDateTime": "2019-10-02T20:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 10.78 + }, { + "name": "PM10", + "value": 16.86 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 17.97, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Zero dust - zero worries!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.13 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 33.72 + }] + }, { + "fromDateTime": "2019-10-02T20:00:00.000Z", + "tillDateTime": "2019-10-02T21:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 12.22 + }, { + "name": "PM10", + "value": 18.19 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 20.36, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 48.88 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.38 + }] + }, { + "fromDateTime": "2019-10-02T21:00:00.000Z", + "tillDateTime": "2019-10-02T22:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.06 + }, { + "name": "PM10", + "value": 18.62 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 21.77, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 52.25 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 37.24 + }] + }, { + "fromDateTime": "2019-10-02T22:00:00.000Z", + "tillDateTime": "2019-10-02T23:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.51 + }, { + "name": "PM10", + "value": 18.49 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.52, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "The air is great!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 54.06 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 36.98 + }] + }, { + "fromDateTime": "2019-10-02T23:00:00.000Z", + "tillDateTime": "2019-10-03T00:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.46 + }, { + "name": "PM10", + "value": 17.63 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 22.44, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 53.85 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 35.26 + }] + }, { + "fromDateTime": "2019-10-03T00:00:00.000Z", + "tillDateTime": "2019-10-03T01:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 13.05 + }, { + "name": "PM10", + "value": 16.36 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 21.74, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Catch your breath!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 52.19 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 32.73 + }] + }, { + "fromDateTime": "2019-10-03T01:00:00.000Z", + "tillDateTime": "2019-10-03T02:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 12.47 + }, { + "name": "PM10", + "value": 15.16 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 20.79, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Green, green, green!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 49.9 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 30.32 + }] + }, { + "fromDateTime": "2019-10-03T02:00:00.000Z", + "tillDateTime": "2019-10-03T03:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.99 + }, { + "name": "PM10", + "value": 14.07 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.98, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 47.94 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 28.14 + }] + }, { + "fromDateTime": "2019-10-03T03:00:00.000Z", + "tillDateTime": "2019-10-03T04:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.74 + }, { + "name": "PM10", + "value": 13.67 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.56, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Dear me, how wonderful!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 46.95 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.34 + }] + }, { + "fromDateTime": "2019-10-03T04:00:00.000Z", + "tillDateTime": "2019-10-03T05:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 11.44 + }, { + "name": "PM10", + "value": 13.51 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 19.06, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe to fill your lungs!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 45.74 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 27.02 + }] + }, { + "fromDateTime": "2019-10-03T05:00:00.000Z", + "tillDateTime": "2019-10-03T06:00:00.000Z", + "values": [{ + "name": "PM25", + "value": 10.88 + }, { + "name": "PM10", + "value": 13.38 + }], + "indexes": [{ + "name": "AIRLY_CAQI", + "value": 18.13, + "level": "VERY_LOW", + "description": "Great air here today!", + "advice": "Breathe as much as you can!", + "color": "#6BC926" + }], + "standards": [{ + "name": "WHO", + "pollutant": "PM25", + "limit": 25.0, + "percent": 43.52 + }, { + "name": "WHO", + "pollutant": "PM10", + "limit": 50.0, + "percent": 26.76 + }] + }] +} \ No newline at end of file From f169e84d2170aa4dca16fedbdcfd4c00c6e85449 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 4 Oct 2019 17:05:52 +0200 Subject: [PATCH 020/639] Update connect-box to fix issue with attrs (#27194) --- homeassistant/components/upc_connect/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/upc_connect/manifest.json b/homeassistant/components/upc_connect/manifest.json index 2cf463d1cf0..cd5d327f2c2 100644 --- a/homeassistant/components/upc_connect/manifest.json +++ b/homeassistant/components/upc_connect/manifest.json @@ -2,7 +2,7 @@ "domain": "upc_connect", "name": "Upc connect", "documentation": "https://www.home-assistant.io/integrations/upc_connect", - "requirements": ["connect-box==0.2.4"], + "requirements": ["connect-box==0.2.5"], "dependencies": [], "codeowners": ["@pvizeli"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5731158cd2f..9ce8b070b1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -359,7 +359,7 @@ colorlog==4.0.2 concord232==0.15 # homeassistant.components.upc_connect -connect-box==0.2.4 +connect-box==0.2.5 # homeassistant.components.eddystone_temperature # homeassistant.components.eq3btsmart From 9a5c1fbaedb7d8b20015c65037fb01b7d4c3e06f Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 4 Oct 2019 11:41:47 -0400 Subject: [PATCH 021/639] Add SecurityPanelController for alarm_control_panel to alexa (#27081) * Implemented Alexa.SecurityPanelController Interface for alarm_control_panel https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html * Implemented Tests for Alexa.SecurityPanelController Interface for alarm_control_panel * Added additional AuthorizationRequired error handling * Removed optional exitDelayInSeconds * Updating elif to if to please pylint * Adding self to code owners. * Adding self to code owners. * Added AlexaEndpointHealth Interface to alarm_control_panel entities. * Added additional entity tests. * Code reformatted with Black. * Updated alexa alarm_control_panel tests for more coverage. * Updated alexa alarm_control_panel tests for more coverage. Fixed Test. * Adding self to code owners. --- CODEOWNERS | 2 +- .../components/alexa/capabilities.py | 67 ++++++++++ homeassistant/components/alexa/entities.py | 17 +++ homeassistant/components/alexa/errors.py | 14 ++ homeassistant/components/alexa/handlers.py | 75 +++++++++++ homeassistant/components/alexa/manifest.json | 5 +- tests/components/alexa/test_capabilities.py | 35 +++++ tests/components/alexa/test_smart_home.py | 124 ++++++++++++++++++ 8 files changed, 337 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 8073020712d..935d68033e3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,7 +17,7 @@ homeassistant/components/adguard/* @frenck homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya homeassistant/components/alarm_control_panel/* @colinodell -homeassistant/components/alexa/* @home-assistant/cloud +homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/alpha_vantage/* @fabaff homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index fca63adab0e..7be3188fea1 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -5,6 +5,10 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_LOCKED, STATE_OFF, STATE_ON, @@ -13,6 +17,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) import homeassistant.components.climate.const as climate +from homeassistant.components.alarm_control_panel import ATTR_CODE_FORMAT, FORMAT_NUMBER from homeassistant.components import light, fan, cover import homeassistant.util.color as color_util import homeassistant.util.dt as dt_util @@ -79,6 +84,11 @@ class AlexaCapibility: """Applicable only to scenes.""" return None + @staticmethod + def configuration(): + """Applicable only to security control panel.""" + return [] + def serialize_discovery(self): """Serialize according to the Discovery API.""" result = { @@ -96,6 +106,11 @@ class AlexaCapibility: supports_deactivation = self.supports_deactivation() if supports_deactivation is not None: result["supportsDeactivation"] = supports_deactivation + + configuration = self.configuration() + if configuration: + result["configuration"] = configuration + return result def serialize_properties(self): @@ -649,3 +664,55 @@ class AlexaPowerLevelController(AlexaCapibility): return PERCENTAGE_FAN_MAP.get(speed, None) return None + + +class AlexaSecurityPanelController(AlexaCapibility): + """Implements Alexa.SecurityPanelController. + + https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html + """ + + def __init__(self, hass, entity): + """Initialize the entity.""" + super().__init__(entity) + self.hass = hass + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.SecurityPanelController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "armState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "armState": + raise UnsupportedProperty(name) + + arm_state = self.entity.state + if arm_state == STATE_ALARM_ARMED_HOME: + return "ARMED_STAY" + if arm_state == STATE_ALARM_ARMED_AWAY: + return "ARMED_AWAY" + if arm_state == STATE_ALARM_ARMED_NIGHT: + return "ARMED_NIGHT" + if arm_state == STATE_ALARM_ARMED_CUSTOM_BYPASS: + return "ARMED_STAY" + return "DISARMED" + + def configuration(self): + """Return supported authorization types.""" + code_format = self.entity.attributes.get(ATTR_CODE_FORMAT) + + if code_format == FORMAT_NUMBER: + return {"supportedAuthorizationTypes": [{"type": "FOUR_DIGIT_PIN"}]} + return [] diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 63231f71447..0f07e525fa9 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -14,6 +14,7 @@ from homeassistant.const import ( from homeassistant.util.decorator import Registry from homeassistant.components.climate import const as climate from homeassistant.components import ( + alarm_control_panel, alert, automation, binary_sensor, @@ -45,6 +46,7 @@ from .capabilities import ( AlexaPowerController, AlexaPowerLevelController, AlexaSceneController, + AlexaSecurityPanelController, AlexaSpeaker, AlexaStepSpeaker, AlexaTemperatureSensor, @@ -487,3 +489,18 @@ class BinarySensorCapabilities(AlexaEntity): return self.TYPE_CONTACT if attrs.get(ATTR_DEVICE_CLASS) == "motion": return self.TYPE_MOTION + + +@ENTITY_ADAPTERS.register(alarm_control_panel.DOMAIN) +class AlarmControlPanelCapabilities(AlexaEntity): + """Class to represent Alarm capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.SECURITY_PANEL] + + def interfaces(self): + """Yield the supported interfaces.""" + if not self.entity.attributes.get("code_arm_required"): + yield AlexaSecurityPanelController(self.hass, self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 8c2fa692267..8e32ed9c7ee 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -83,3 +83,17 @@ class AlexaBridgeUnreachableError(AlexaError): namespace = "Alexa" error_type = "BRIDGE_UNREACHABLE" + + +class AlexaSecurityPanelUnauthorizedError(AlexaError): + """Class to represent SecurityPanelController Unauthorized errors.""" + + namespace = "Alexa.SecurityPanelController" + error_type = "UNAUTHORIZED" + + +class AlexaSecurityPanelAuthorizationRequired(AlexaError): + """Class to represent SecurityPanelController AuthorizationRequired errors.""" + + namespace = "Alexa.SecurityPanelController" + error_type = "AUTHORIZATION_REQUIRED" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 3cb61675f92..bd07b71ca29 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -9,6 +9,11 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, + STATE_ALARM_DISARMED, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, SERVICE_LOCK, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, @@ -35,6 +40,8 @@ from .const import API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, from .entities import async_get_entities from .errors import ( AlexaInvalidValueError, + AlexaSecurityPanelAuthorizationRequired, + AlexaSecurityPanelUnauthorizedError, AlexaTempRangeError, AlexaUnsupportedThermostatModeError, ) @@ -849,3 +856,71 @@ async def async_api_adjust_power_level(hass, config, directive, context): ) return directive.response() + + +@HANDLERS.register(("Alexa.SecurityPanelController", "Arm")) +async def async_api_arm(hass, config, directive, context): + """Process a Security Panel Arm request.""" + entity = directive.entity + service = None + arm_state = directive.payload["armState"] + data = {ATTR_ENTITY_ID: entity.entity_id} + + if entity.state != STATE_ALARM_DISARMED: + msg = "You must disarm the system before you can set the requested arm state." + raise AlexaSecurityPanelAuthorizationRequired(msg) + + if arm_state == "ARMED_AWAY": + service = SERVICE_ALARM_ARM_AWAY + if arm_state == "ARMED_STAY": + service = SERVICE_ALARM_ARM_HOME + if arm_state == "ARMED_NIGHT": + service = SERVICE_ALARM_ARM_NIGHT + + await hass.services.async_call( + entity.domain, service, data, blocking=False, context=context + ) + + response = directive.response( + name="Arm.Response", namespace="Alexa.SecurityPanelController" + ) + + response.add_context_property( + { + "name": "armState", + "namespace": "Alexa.SecurityPanelController", + "value": arm_state, + } + ) + + return response + + +@HANDLERS.register(("Alexa.SecurityPanelController", "Disarm")) +async def async_api_disarm(hass, config, directive, context): + """Process a Security Panel Disarm request.""" + entity = directive.entity + data = {ATTR_ENTITY_ID: entity.entity_id} + + payload = directive.payload + if "authorization" in payload: + value = payload["authorization"]["value"] + if payload["authorization"]["type"] == "FOUR_DIGIT_PIN": + data["code"] = value + + if not await hass.services.async_call( + entity.domain, SERVICE_ALARM_DISARM, data, blocking=True, context=context + ): + msg = "Invalid Code" + raise AlexaSecurityPanelUnauthorizedError(msg) + + response = directive.response() + response.add_context_property( + { + "name": "armState", + "namespace": "Alexa.SecurityPanelController", + "value": "DISARMED", + } + ) + + return response diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index 9db7e270e61..ad0f1c33d49 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -4,5 +4,8 @@ "documentation": "https://www.home-assistant.io/integrations/alexa", "requirements": [], "dependencies": ["http"], - "codeowners": ["@home-assistant/cloud"] + "codeowners": [ + "@home-assistant/cloud", + "@ochlocracy" + ] } diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index d53f145e6ff..280a76dc3f0 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -8,6 +8,11 @@ from homeassistant.const import ( STATE_UNLOCKED, STATE_UNKNOWN, STATE_UNAVAILABLE, + STATE_ALARM_DISARMED, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, ) from homeassistant.components.climate import const as climate from homeassistant.components.alexa import smart_home @@ -527,3 +532,33 @@ async def test_temperature_sensor_climate(hass): properties.assert_equal( "Alexa.TemperatureSensor", "temperature", {"value": 34.0, "scale": "CELSIUS"} ) + + +async def test_report_alarm_control_panel_state(hass): + """Test SecurityPanelController implements armState property.""" + hass.states.async_set("alarm_control_panel.armed_away", STATE_ALARM_ARMED_AWAY, {}) + hass.states.async_set( + "alarm_control_panel.armed_custom_bypass", STATE_ALARM_ARMED_CUSTOM_BYPASS, {} + ) + hass.states.async_set("alarm_control_panel.armed_home", STATE_ALARM_ARMED_HOME, {}) + hass.states.async_set( + "alarm_control_panel.armed_night", STATE_ALARM_ARMED_NIGHT, {} + ) + hass.states.async_set("alarm_control_panel.disarmed", STATE_ALARM_DISARMED, {}) + + properties = await reported_properties(hass, "alarm_control_panel.armed_away") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY") + + properties = await reported_properties( + hass, "alarm_control_panel.armed_custom_bypass" + ) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY") + + properties = await reported_properties(hass, "alarm_control_panel.armed_home") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY") + + properties = await reported_properties(hass, "alarm_control_panel.armed_night") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_NIGHT") + + properties = await reported_properties(hass, "alarm_control_panel.disarmed") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 78ce2963eaf..78bdd8e0908 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1306,3 +1306,127 @@ async def test_endpoint_bad_health(hass): properties.assert_equal( "Alexa.EndpointHealth", "connectivity", {"value": "UNREACHABLE"} ) + + +async def test_alarm_control_panel_disarmed(hass): + """Test alarm_control_panel discovery.""" + device = ( + "alarm_control_panel.test_1", + "disarmed", + { + "friendly_name": "Test Alarm Control Panel 1", + "code_arm_required": False, + "code_format": "number", + "code": "1234", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "alarm_control_panel#test_1" + assert appliance["displayCategories"][0] == "SECURITY_PANEL" + assert appliance["friendlyName"] == "Test Alarm Control Panel 1" + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + ) + security_panel_capability = get_capability( + capabilities, "Alexa.SecurityPanelController" + ) + assert security_panel_capability is not None + configuration = security_panel_capability["configuration"] + assert {"type": "FOUR_DIGIT_PIN"} in configuration["supportedAuthorizationTypes"] + + properties = await reported_properties(hass, "alarm_control_panel#test_1") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_1", + "alarm_control_panel.alarm_arm_home", + hass, + response_type="Arm.Response", + payload={"armState": "ARMED_STAY"}, + ) + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_STAY") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_1", + "alarm_control_panel.alarm_arm_away", + hass, + response_type="Arm.Response", + payload={"armState": "ARMED_AWAY"}, + ) + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_1", + "alarm_control_panel.alarm_arm_night", + hass, + response_type="Arm.Response", + payload={"armState": "ARMED_NIGHT"}, + ) + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_NIGHT") + + +async def test_alarm_control_panel_armed(hass): + """Test alarm_control_panel discovery.""" + device = ( + "alarm_control_panel.test_2", + "armed_away", + { + "friendly_name": "Test Alarm Control Panel 2", + "code_arm_required": False, + "code_format": "FORMAT_NUMBER", + "code": "1234", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "alarm_control_panel#test_2" + assert appliance["displayCategories"][0] == "SECURITY_PANEL" + assert appliance["friendlyName"] == "Test Alarm Control Panel 2" + assert_endpoint_capabilities( + appliance, "Alexa.SecurityPanelController", "Alexa.EndpointHealth" + ) + + properties = await reported_properties(hass, "alarm_control_panel#test_2") + properties.assert_equal("Alexa.SecurityPanelController", "armState", "ARMED_AWAY") + + call, msg = await assert_request_calls_service( + "Alexa.SecurityPanelController", + "Disarm", + "alarm_control_panel#test_2", + "alarm_control_panel.alarm_disarm", + hass, + payload={"authorization": {"type": "FOUR_DIGIT_PIN", "value": "1234"}}, + ) + assert call.data["code"] == "1234" + properties = ReportedProperties(msg["context"]["properties"]) + properties.assert_equal("Alexa.SecurityPanelController", "armState", "DISARMED") + + msg = await assert_request_fails( + "Alexa.SecurityPanelController", + "Arm", + "alarm_control_panel#test_2", + "alarm_control_panel.alarm_arm_home", + hass, + payload={"armState": "ARMED_STAY"}, + ) + assert msg["event"]["payload"]["type"] == "AUTHORIZATION_REQUIRED" + + +async def test_alarm_control_panel_code_arm_required(hass): + """Test alarm_control_panel with code_arm_required discovery.""" + device = ( + "alarm_control_panel.test_3", + "disarmed", + {"friendly_name": "Test Alarm Control Panel 3", "code_arm_required": True}, + ) + await discovery_test(device, hass, expected_endpoints=0) From 3547b8691e019bc86e2ed61afd01e389c964ddc6 Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 4 Oct 2019 17:46:23 +0200 Subject: [PATCH 022/639] Add examples to lights service (#27192) --- homeassistant/components/light/services.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/light/services.yaml b/homeassistant/components/light/services.yaml index 97186f56a8f..9173f79f964 100644 --- a/homeassistant/components/light/services.yaml +++ b/homeassistant/components/light/services.yaml @@ -40,12 +40,14 @@ turn_on: description: Name of a light profile to use. example: relax flash: - description: If the light should flash. + description: If the light should flash. Valid values are short and long. + example: short values: - short - long effect: description: Light effect. + example: random values: - colorloop - random @@ -60,7 +62,8 @@ turn_off: description: Duration in seconds it takes to get to next state. example: 60 flash: - description: If the light should flash. + description: If the light should flash. Valid values are short and long. + example: short values: - short - long @@ -105,12 +108,14 @@ toggle: description: Name of a light profile to use. example: relax flash: - description: If the light should flash. + description: If the light should flash. Valid values are short and long. + example: short values: - short - long effect: description: Light effect. + example: random values: - colorloop - random From 45d4586bc2a8b77a1efee7bb187c63f84f0cc2d8 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 4 Oct 2019 16:54:16 +0100 Subject: [PATCH 023/639] Improve evohome debug logging (#27178) * add debug logging for schedule updates * add debug logging for schedules * change back to debug from warn --- homeassistant/components/evohome/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 14bf1223953..e9254c373d9 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -454,6 +454,8 @@ class EvoChild(EvoDevice): self._evo_device.schedule(), refresh=False ) + _LOGGER.debug("Schedule['%s'] = %s", self.name, self._schedule) + async def async_update(self) -> None: """Get the latest state data.""" next_sp_from = self._setpoints.get("next_sp_from", "2000-01-01T00:00:00+00:00") From b8f41dbb758aceaa1a4ab65d21ca4bd7544ff478 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Oct 2019 19:11:14 +0200 Subject: [PATCH 024/639] Add device condition support to sensor entities (#27163) * Add device condition support to sensor entities * Fix typing --- .../components/sensor/device_condition.py | 143 +++++++++ .../components/sensor/device_trigger.py | 3 +- .../sensor/test_device_condition.py | 289 ++++++++++++++++++ 3 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/sensor/device_condition.py create mode 100644 tests/components/sensor/test_device_condition.py diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py new file mode 100644 index 00000000000..76f1b3909ef --- /dev/null +++ b/homeassistant/components/sensor/device_condition.py @@ -0,0 +1,143 @@ +"""Provides device conditions for sensors.""" +from typing import List +import voluptuous as vol + +from homeassistant.core import HomeAssistant +import homeassistant.components.automation.numeric_state as numeric_state_automation +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + CONF_ABOVE, + CONF_BELOW, + CONF_ENTITY_ID, + CONF_FOR, + CONF_TYPE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, +) +from homeassistant.helpers.entity_registry import ( + async_entries_for_device, + async_get_registry, +) +from homeassistant.helpers import condition, config_validation as cv +from homeassistant.helpers.typing import ConfigType + +from . import DOMAIN + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_IS_BATTERY_LEVEL = "is_battery_level" +CONF_IS_HUMIDITY = "is_humidity" +CONF_IS_ILLUMINANCE = "is_illuminance" +CONF_IS_POWER = "is_power" +CONF_IS_PRESSURE = "is_pressure" +CONF_IS_SIGNAL_STRENGTH = "is_signal_strength" +CONF_IS_TEMPERATURE = "is_temperature" +CONF_IS_TIMESTAMP = "is_timestamp" +CONF_IS_VALUE = "is_value" + +ENTITY_CONDITIONS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], + DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_IS_HUMIDITY}], + DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_IS_POWER}], + DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_IS_PRESSURE}], + DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_IS_SIGNAL_STRENGTH}], + DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_IS_TEMPERATURE}], + DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_IS_TIMESTAMP}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_IS_VALUE}], +} + +CONDITION_SCHEMA = vol.All( + cv.DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In( + [ + CONF_IS_BATTERY_LEVEL, + CONF_IS_HUMIDITY, + CONF_IS_ILLUMINANCE, + CONF_IS_POWER, + CONF_IS_PRESSURE, + CONF_IS_SIGNAL_STRENGTH, + CONF_IS_TEMPERATURE, + CONF_IS_TIMESTAMP, + CONF_IS_VALUE, + ] + ), + vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions.""" + conditions: List[dict] = [] + entity_registry = await async_get_registry(hass) + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + continue + + if ATTR_DEVICE_CLASS in state.attributes: + device_class = state.attributes[ATTR_DEVICE_CLASS] + + templates = ENTITY_CONDITIONS.get( + device_class, ENTITY_CONDITIONS[DEVICE_CLASS_NONE] + ) + + conditions.extend( + ( + { + **template, + "condition": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for template in templates + ) + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Evaluate state based on configuration.""" + if config_validation: + config = CONDITION_SCHEMA(config) + numeric_state_config = { + numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE), + numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW), + numeric_state_automation.CONF_FOR: config.get(CONF_FOR), + } + + return condition.async_numeric_state_from_config( + numeric_state_config, config_validation + ) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 377b202db18..0d9e8f5af80 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -121,7 +121,8 @@ async def async_get_triggers(hass, device_id): if not state or not unit_of_measurement: continue - device_class = state.attributes.get(ATTR_DEVICE_CLASS) + if ATTR_DEVICE_CLASS in state.attributes: + device_class = state.attributes[ATTR_DEVICE_CLASS] templates = ENTITY_TRIGGERS.get( device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py new file mode 100644 index 00000000000..e28e487f4ef --- /dev/null +++ b/tests/components/sensor/test_device_condition.py @@ -0,0 +1,289 @@ +"""The test for sensor device automation.""" +import pytest + +from homeassistant.components.sensor import DOMAIN +from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS +from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) +from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": condition["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for condition in ENTITY_CONDITIONS[device_class] + if device_class != "none" + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert conditions == expected_conditions + + +async def test_if_state_not_above_below(hass, calls, caplog): + """Test for bad value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + } + ], + "action": {"service": "test.automation"}, + } + ] + }, + ) + assert "must contain at least one of below, above" in caplog.text + + +async def test_if_state_above(hass, calls): + """Test for value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + "above": 10, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "event - test_event1" + + +async def test_if_state_below(hass, calls): + """Test for value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + "below": 10, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "event - test_event1" + + +async def test_if_state_between(hass, calls): + """Test for value conditions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "is_battery_level", + "above": 10, + "below": 20, + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "{{ trigger.%s }}" + % "}} - {{ trigger.".join(("platform", "event.event_type")) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "event - test_event1" + + hass.states.async_set(sensor1.entity_id, 21) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_set(sensor1.entity_id, 19) + hass.bus.async_fire("test_event1") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "event - test_event1" From e27051aa61159940f3969d52cb75f12951c57ff4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 4 Oct 2019 19:17:57 +0200 Subject: [PATCH 025/639] Fix validation when automation is saved from frontend (#27195) --- homeassistant/components/automation/config.py | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index 581ce6b461d..ebbd1771e84 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -18,33 +18,40 @@ from . import CONF_ACTION, CONF_CONDITION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA async def async_validate_config_item(hass, config, full_config=None): """Validate config item.""" - try: - config = PLATFORM_SCHEMA(config) + config = PLATFORM_SCHEMA(config) - triggers = [] - for trigger in config[CONF_TRIGGER]: - trigger_platform = importlib.import_module( - "..{}".format(trigger[CONF_PLATFORM]), __name__ + triggers = [] + for trigger in config[CONF_TRIGGER]: + trigger_platform = importlib.import_module( + "..{}".format(trigger[CONF_PLATFORM]), __name__ + ) + if hasattr(trigger_platform, "async_validate_trigger_config"): + trigger = await trigger_platform.async_validate_trigger_config( + hass, trigger ) - if hasattr(trigger_platform, "async_validate_trigger_config"): - trigger = await trigger_platform.async_validate_trigger_config( - hass, trigger - ) - triggers.append(trigger) - config[CONF_TRIGGER] = triggers + triggers.append(trigger) + config[CONF_TRIGGER] = triggers - if CONF_CONDITION in config: - conditions = [] - for cond in config[CONF_CONDITION]: - cond = await condition.async_validate_condition_config(hass, cond) - conditions.append(cond) - config[CONF_CONDITION] = conditions + if CONF_CONDITION in config: + conditions = [] + for cond in config[CONF_CONDITION]: + cond = await condition.async_validate_condition_config(hass, cond) + conditions.append(cond) + config[CONF_CONDITION] = conditions - actions = [] - for action in config[CONF_ACTION]: - action = await script.async_validate_action_config(hass, action) - actions.append(action) - config[CONF_ACTION] = actions + actions = [] + for action in config[CONF_ACTION]: + action = await script.async_validate_action_config(hass, action) + actions.append(action) + config[CONF_ACTION] = actions + + return config + + +async def _try_async_validate_config_item(hass, config, full_config=None): + """Validate config item.""" + try: + config = await async_validate_config_item(hass, config, full_config) except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: async_log_exception(ex, DOMAIN, full_config or config, hass) return None @@ -57,7 +64,7 @@ async def async_validate_config(hass, config): automations = [] validated_automations = await asyncio.gather( *( - async_validate_config_item(hass, p_config, config) + _try_async_validate_config_item(hass, p_config, config) for _, p_config in config_per_platform(config, DOMAIN) ) ) From e5a2e188817532537af15e46ff8a7ccd0f80d0b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mayoral=20Mart=C3=ADnez?= Date: Fri, 4 Oct 2019 21:07:19 +0200 Subject: [PATCH 026/639] Fix template fan turn_on action (#27181) * Fix template fan turn_on action The turn_on action of a template fan should receive the 'speed' attribute in order to give the user the possibility of define the behaviour of this action as he desires Fixes #27176 * Format * Update fan.py --- homeassistant/components/template/fan.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/template/fan.py b/homeassistant/components/template/fan.py index 42790e618d9..606f18e5fe1 100644 --- a/homeassistant/components/template/fan.py +++ b/homeassistant/components/template/fan.py @@ -270,7 +270,7 @@ class TemplateFan(FanEntity): # pylint: disable=arguments-differ async def async_turn_on(self, speed: str = None) -> None: """Turn on the fan.""" - await self._on_script.async_run(context=self._context) + await self._on_script.async_run({ATTR_SPEED: speed}, context=self._context) self._state = STATE_ON if speed is not None: From 23686710b1c021198848c79e0c2c8798c4d29079 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 4 Oct 2019 13:49:51 -0700 Subject: [PATCH 027/639] Fix tests running in hass.io image (#27169) * Fix tests running in hass.io image * Real fix now * Only remove wheel links --- tests/test_requirements.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/test_requirements.py b/tests/test_requirements.py index b5574fe96fd..780b175778e 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -17,6 +17,13 @@ from homeassistant.requirements import ( from tests.common import get_test_home_assistant, MockModule, mock_integration +def env_without_wheel_links(): + """Return env without wheel links.""" + env = dict(os.environ) + env.pop("WHEEL_LINKS", None) + return env + + class TestRequirements: """Test the requirements module.""" @@ -36,6 +43,7 @@ class TestRequirements: @patch("homeassistant.util.package.is_virtual_env", return_value=True) @patch("homeassistant.util.package.is_docker_env", return_value=False) @patch("homeassistant.util.package.install_package", return_value=True) + @patch.dict(os.environ, env_without_wheel_links(), clear=True) def test_requirement_installed_in_venv( self, mock_install, mock_denv, mock_venv, mock_dirname ): @@ -55,6 +63,7 @@ class TestRequirements: @patch("homeassistant.util.package.is_virtual_env", return_value=False) @patch("homeassistant.util.package.is_docker_env", return_value=False) @patch("homeassistant.util.package.install_package", return_value=True) + @patch.dict(os.environ, env_without_wheel_links(), clear=True) def test_requirement_installed_in_deps( self, mock_install, mock_denv, mock_venv, mock_dirname ): @@ -136,7 +145,7 @@ async def test_install_with_wheels_index(hass): mock_dir.return_value = "ha_package_path" assert await setup.async_setup_component(hass, "comp", {}) assert "comp" in hass.config.components - print(mock_inst.call_args) + assert mock_inst.call_args == call( "hello==1.0.0", find_links="https://wheels.hass.io/test", @@ -154,11 +163,13 @@ async def test_install_on_docker(hass): "homeassistant.util.package.is_docker_env", return_value=True ), patch("homeassistant.util.package.install_package") as mock_inst, patch( "os.path.dirname" - ) as mock_dir: + ) as mock_dir, patch.dict( + os.environ, env_without_wheel_links(), clear=True + ): mock_dir.return_value = "ha_package_path" assert await setup.async_setup_component(hass, "comp", {}) assert "comp" in hass.config.components - print(mock_inst.call_args) + assert mock_inst.call_args == call( "hello==1.0.0", constraints=os.path.join("ha_package_path", CONSTRAINT_FILE), From de246fa7d88d8c163bdcacccc7f6dbee9bd2893d Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 4 Oct 2019 23:14:47 +0200 Subject: [PATCH 028/639] lock open service data (#27204) --- homeassistant/components/lock/services.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/lock/services.yaml b/homeassistant/components/lock/services.yaml index 0b4688c02a2..d17e00addd1 100644 --- a/homeassistant/components/lock/services.yaml +++ b/homeassistant/components/lock/services.yaml @@ -47,6 +47,16 @@ lock: description: An optional code to lock the lock with. example: 1234 +open: + description: Open all or specified locks. + fields: + entity_id: + description: Name of lock to open. + example: 'lock.front_door' + code: + description: An optional code to open the lock with. + example: 1234 + set_usercode: description: Set a usercode to lock. fields: From c62d1a77ecff5c809a72a44eab09d02c2e26b95c Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Fri, 4 Oct 2019 17:15:43 -0400 Subject: [PATCH 029/639] Fix ecobee binary sensor and sensor unique ids (#27208) * Fix sensor unique id * Fix binary sensor unique id --- homeassistant/components/ecobee/binary_sensor.py | 3 ++- homeassistant/components/ecobee/sensor.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 97ba94aaa70..3367f33a66f 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -50,7 +50,8 @@ class EcobeeBinarySensor(BinarySensorDevice): if sensor["name"] == self.sensor_name: if "code" in sensor: return f"{sensor['code']}-{self.device_class}" - return f"{sensor['id']}-{self.device_class}" + thermostat = self.data.ecobee.get_thermostat(self.index) + return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property def device_info(self): diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 27a8ae3ef07..1c47bc9b26d 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -61,7 +61,8 @@ class EcobeeSensor(Entity): if sensor["name"] == self.sensor_name: if "code" in sensor: return f"{sensor['code']}-{self.device_class}" - return f"{sensor['id']}-{self.device_class}" + thermostat = self.data.ecobee.get_thermostat(self.index) + return f"{thermostat['identifier']}-{sensor['id']}-{self.device_class}" @property def device_info(self): From 0be1269b2007c2cdf98c79a1007b3e9f7de01435 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 4 Oct 2019 23:21:19 +0200 Subject: [PATCH 030/639] Add acceleration sensor to Homematic IP Cloud (#27199) --- .../homematicip_cloud/binary_sensor.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 4ac4614379b..b3a21ba0b5c 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homematicip.aio.device import ( + AsyncAccelerationSensor, AsyncContactInterface, AsyncDevice, AsyncFullFlushContactInterface, @@ -28,6 +29,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_LIGHT, DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, + DEVICE_CLASS_MOVING, DEVICE_CLASS_OPENING, DEVICE_CLASS_PRESENCE, DEVICE_CLASS_SAFETY, @@ -42,6 +44,10 @@ from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP, ATTR_MODEL_TYP _LOGGER = logging.getLogger(__name__) +ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode" +ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position" +ATTR_ACCELERATION_SENSOR_SENSITIVITY = "acceleration_sensor_sensitivity" +ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE = "acceleration_sensor_trigger_angle" ATTR_LOW_BATTERY = "low_battery" ATTR_MOISTURE_DETECTED = "moisture_detected" ATTR_MOTION_DETECTED = "motion_detected" @@ -63,6 +69,13 @@ GROUP_ATTRIBUTES = { "waterlevelDetected": ATTR_WATER_LEVEL_DETECTED, } +SAM_DEVICE_ATTRIBUTES = { + "accelerationSensorNeutralPosition": ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION, + "accelerationSensorMode": ATTR_ACCELERATION_SENSOR_MODE, + "accelerationSensorSensitivity": ATTR_ACCELERATION_SENSOR_SENSITIVITY, + "accelerationSensorTriggerAngle": ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE, +} + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HomematicIP Cloud binary sensor devices.""" @@ -76,6 +89,8 @@ async def async_setup_entry( home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home devices = [] for device in home.devices: + if isinstance(device, AsyncAccelerationSensor): + devices.append(HomematicipAccelerationSensor(home, device)) if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): devices.append(HomematicipContactInterface(home, device)) if isinstance( @@ -118,6 +133,32 @@ async def async_setup_entry( async_add_entities(devices) +class HomematicipAccelerationSensor(HomematicipGenericDevice, BinarySensorDevice): + """Representation of a HomematicIP Cloud acceleration sensor.""" + + @property + def device_class(self) -> str: + """Return the class of this sensor.""" + return DEVICE_CLASS_MOVING + + @property + def is_on(self) -> bool: + """Return true if acceleration is detected.""" + return self._device.accelerationSensorTriggered + + @property + def device_state_attributes(self): + """Return the state attributes of the acceleration sensor.""" + state_attr = super().device_state_attributes + + for attr, attr_key in SAM_DEVICE_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value + + return state_attr + + class HomematicipContactInterface(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud contact interface.""" From cc4926afb1ff26e1fd65c366461be49e70d1f4bb Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 4 Oct 2019 23:29:53 +0200 Subject: [PATCH 031/639] lock_reproduce_state (#27203) --- .../components/lock/reproduce_state.py | 61 +++++++++++++++++++ tests/components/lock/test_reproduce_state.py | 53 ++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 homeassistant/components/lock/reproduce_state.py create mode 100644 tests/components/lock/test_reproduce_state.py diff --git a/homeassistant/components/lock/reproduce_state.py b/homeassistant/components/lock/reproduce_state.py new file mode 100644 index 00000000000..dc1bee85839 --- /dev/null +++ b/homeassistant/components/lock/reproduce_state.py @@ -0,0 +1,61 @@ +"""Reproduce an Lock state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_LOCKED, + STATE_UNLOCKED, + SERVICE_LOCK, + SERVICE_UNLOCK, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_LOCKED, STATE_UNLOCKED} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_LOCKED: + service = SERVICE_LOCK + elif state.state == STATE_UNLOCKED: + service = SERVICE_UNLOCK + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Lock states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/lock/test_reproduce_state.py b/tests/components/lock/test_reproduce_state.py new file mode 100644 index 00000000000..a9b61fa1219 --- /dev/null +++ b/tests/components/lock/test_reproduce_state.py @@ -0,0 +1,53 @@ +"""Test reproduce state for Lock.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Lock states.""" + hass.states.async_set("lock.entity_locked", "locked", {}) + hass.states.async_set("lock.entity_unlocked", "unlocked", {}) + + lock_calls = async_mock_service(hass, "lock", "lock") + unlock_calls = async_mock_service(hass, "lock", "unlock") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("lock.entity_locked", "locked"), + State("lock.entity_unlocked", "unlocked", {}), + ], + blocking=True, + ) + + assert len(lock_calls) == 0 + assert len(unlock_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("lock.entity_locked", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(lock_calls) == 0 + assert len(unlock_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("lock.entity_locked", "unlocked"), + State("lock.entity_unlocked", "locked"), + # Should not raise + State("lock.non_existing", "on"), + ], + blocking=True, + ) + + assert len(lock_calls) == 1 + assert lock_calls[0].domain == "lock" + assert lock_calls[0].data == {"entity_id": "lock.entity_unlocked"} + + assert len(unlock_calls) == 1 + assert unlock_calls[0].domain == "lock" + assert unlock_calls[0].data == {"entity_id": "lock.entity_locked"} From 9c96ec858a1506a60bcc7ee4031ba030efea530a Mon Sep 17 00:00:00 2001 From: Santobert Date: Fri, 4 Oct 2019 23:32:10 +0200 Subject: [PATCH 032/639] switch reproduce state (#27202) --- .../components/switch/reproduce_state.py | 61 +++++++++++++++++++ .../components/switch/test_reproduce_state.py | 50 +++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 homeassistant/components/switch/reproduce_state.py create mode 100644 tests/components/switch/test_reproduce_state.py diff --git a/homeassistant/components/switch/reproduce_state.py b/homeassistant/components/switch/reproduce_state.py new file mode 100644 index 00000000000..7ed1f70cb97 --- /dev/null +++ b/homeassistant/components/switch/reproduce_state.py @@ -0,0 +1,61 @@ +"""Reproduce an Switch state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Switch states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/switch/test_reproduce_state.py b/tests/components/switch/test_reproduce_state.py new file mode 100644 index 00000000000..4b6db84bfdd --- /dev/null +++ b/tests/components/switch/test_reproduce_state.py @@ -0,0 +1,50 @@ +"""Test reproduce state for Switch.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Switch states.""" + hass.states.async_set("switch.entity_off", "off", {}) + hass.states.async_set("switch.entity_on", "on", {}) + + turn_on_calls = async_mock_service(hass, "switch", "turn_on") + turn_off_calls = async_mock_service(hass, "switch", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [State("switch.entity_off", "off"), State("switch.entity_on", "on", {})], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("switch.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("switch.entity_on", "off"), + State("switch.entity_off", "on", {}), + # Should not raise + State("switch.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "switch" + assert turn_on_calls[0].data == {"entity_id": "switch.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "switch" + assert turn_off_calls[0].data == {"entity_id": "switch.entity_on"} From 7e7868f0a6d6baa710e93132861f03c59f85aaf8 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 5 Oct 2019 00:32:19 +0000 Subject: [PATCH 033/639] [ci skip] Translation update --- .../components/airly/.translations/da.json | 22 ++++++++++++++++ .../components/airly/.translations/en.json | 22 ++++++++++++++++ .../components/airly/.translations/lb.json | 20 ++++++++++++++ .../components/airly/.translations/no.json | 22 ++++++++++++++++ .../components/airly/.translations/pl.json | 22 ++++++++++++++++ .../components/deconz/.translations/lb.json | 1 + .../components/plex/.translations/lb.json | 1 + .../components/sensor/.translations/lb.json | 26 +++++++++++++++++++ .../components/soma/.translations/lb.json | 13 ++++++++++ 9 files changed, 149 insertions(+) create mode 100644 homeassistant/components/airly/.translations/da.json create mode 100644 homeassistant/components/airly/.translations/en.json create mode 100644 homeassistant/components/airly/.translations/lb.json create mode 100644 homeassistant/components/airly/.translations/no.json create mode 100644 homeassistant/components/airly/.translations/pl.json create mode 100644 homeassistant/components/sensor/.translations/lb.json create mode 100644 homeassistant/components/soma/.translations/lb.json diff --git a/homeassistant/components/airly/.translations/da.json b/homeassistant/components/airly/.translations/da.json new file mode 100644 index 00000000000..652cc46a7b3 --- /dev/null +++ b/homeassistant/components/airly/.translations/da.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-n\u00f8glen er ikke korrekt.", + "name_exists": "Navnet findes allerede.", + "wrong_location": "Ingen Airly m\u00e5lestationer i dette omr\u00e5de." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-n\u00f8gle", + "latitude": "Breddegrad", + "longitude": "L\u00e6ngdegrad", + "name": "Integrationens navn" + }, + "description": "Konfigurer Airly luftkvalitet integration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/en.json b/homeassistant/components/airly/.translations/en.json new file mode 100644 index 00000000000..83284aaeb7b --- /dev/null +++ b/homeassistant/components/airly/.translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API key is not correct.", + "name_exists": "Name already exists.", + "wrong_location": "No Airly measuring stations in this area." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API key", + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Name of the integration" + }, + "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/lb.json b/homeassistant/components/airly/.translations/lb.json new file mode 100644 index 00000000000..ca71c2e647f --- /dev/null +++ b/homeassistant/components/airly/.translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "auth": "Api Schl\u00ebssel ass net korrekt.", + "name_exists": "Numm g\u00ebtt et schonn" + }, + "step": { + "user": { + "data": { + "api_key": "Airly API Schl\u00ebssel", + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad", + "name": "Numm vun der Installatioun" + }, + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/no.json b/homeassistant/components/airly/.translations/no.json new file mode 100644 index 00000000000..70924bb7bf4 --- /dev/null +++ b/homeassistant/components/airly/.translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-n\u00f8kkelen er ikke korrekt.", + "name_exists": "Navnet finnes allerede.", + "wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "name": "Navn p\u00e5 integrasjonen" + }, + "description": "Sett opp Airly luftkvalitet integrering. For \u00e5 generere API-n\u00f8kkel g\u00e5 til https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/pl.json b/homeassistant/components/airly/.translations/pl.json new file mode 100644 index 00000000000..5d601b37591 --- /dev/null +++ b/homeassistant/components/airly/.translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Klucz API jest nieprawid\u0142owy.", + "name_exists": "Nazwa ju\u017c istnieje.", + "wrong_location": "Brak stacji pomiarowych Airly w tym rejonie." + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API Airly", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "name": "Nazwa integracji" + }, + "description": "Konfiguracja integracji Airly. By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 840bc8929a7..f5f41a28a32 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" Kn\u00e4ppche v\u00e9ier mol gedr\u00e9ckt", "remote_button_quintuple_press": "\"{subtype}\" Kn\u00e4ppche f\u00ebnnef mol gedr\u00e9ckt", "remote_button_rotated": "Kn\u00e4ppche gedr\u00e9int \"{subtype}\"", + "remote_button_rotation_stopped": "Kn\u00e4ppchen Rotatioun \"{subtype}\" gestoppt", "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss", "remote_button_triple_press": "\"{subtype}\" Kn\u00e4ppche dr\u00e4imol gedr\u00e9ckt", diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 1e6488784d4..1795ef6b6d3 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -5,6 +5,7 @@ "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert", "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", + "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", "unknown": "Onbekannte Feeler opgetrueden" }, "error": { diff --git a/homeassistant/components/sensor/.translations/lb.json b/homeassistant/components/sensor/.translations/lb.json new file mode 100644 index 00000000000..01a4e89c9f4 --- /dev/null +++ b/homeassistant/components/sensor/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} Batterie niveau", + "is_humidity": "{entity_name} Fiichtegkeet", + "is_illuminance": "{entity_name} Beliichtung", + "is_power": "{entity_name} Leeschtung", + "is_pressure": "{entity_name} Drock", + "is_signal_strength": "{entity_name} Signal St\u00e4erkt", + "is_temperature": "{entity_name} Temperatur", + "is_timestamp": "{entity_name} Z\u00e4itstempel", + "is_value": "{entity_name} W\u00e4ert" + }, + "trigger_type": { + "battery_level": "{entity_name} Batterie niveau", + "humidity": "{entity_name} Fiichtegkeet", + "illuminance": "{entity_name} Beliichtung", + "power": "{entity_name} Leeschtung", + "pressure": "{entity_name} Drock", + "signal_strength": "{entity_name} Signal St\u00e4erkt", + "temperature": "{entity_name} Temperatur", + "timestamp": "{entity_name} Z\u00e4itstempel", + "value": "{entity_name} W\u00e4ert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json new file mode 100644 index 00000000000..d8aba082537 --- /dev/null +++ b/homeassistant/components/soma/.translations/lb.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Dir k\u00ebnnt n\u00ebmmen een eenzegen Soma Kont konfigur\u00e9ieren.", + "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", + "missing_configuration": "D'Soma Komponent ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert." + }, + "title": "Soma" + } +} \ No newline at end of file From 2e49303401f18bd3b99e3ab68be7b6122e607b7e Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Fri, 4 Oct 2019 20:35:31 -0400 Subject: [PATCH 034/639] Add turn_on method to ecobee climate platform (#27103) * Add turn on method to ecobee climate * Update climate.py * Update value in async_update * Fix update in async_update * Simplify async_update * Fix lint complaining about log string * Cleanup inline if statement --- homeassistant/components/ecobee/climate.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 491fd8d686a..e29e2381008 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -264,6 +264,7 @@ class Thermostat(ClimateDevice): self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) self._name = self.thermostat["name"] self.vacation = None + self._last_active_hvac_mode = HVAC_MODE_AUTO self._operation_list = [] if self.thermostat["settings"]["heatStages"]: @@ -289,6 +290,8 @@ class Thermostat(ClimateDevice): else: await self.data.update() self.thermostat = self.data.ecobee.get_thermostat(self.thermostat_index) + if self.hvac_mode is not HVAC_MODE_OFF: + self._last_active_hvac_mode = self.hvac_mode @property def available(self): @@ -700,3 +703,12 @@ class Thermostat(ClimateDevice): vacation_name, ) self.data.ecobee.delete_vacation(self.thermostat_index, vacation_name) + + def turn_on(self): + """Set the thermostat to the last active HVAC mode.""" + _LOGGER.debug( + "Turning on ecobee thermostat %s in %s mode", + self.name, + self._last_active_hvac_mode, + ) + self.set_hvac_mode(self._last_active_hvac_mode) From 6ae908b883cb76f93a9d6303e8bab3d7a6619689 Mon Sep 17 00:00:00 2001 From: mvn23 Date: Sat, 5 Oct 2019 02:38:26 +0200 Subject: [PATCH 035/639] Add opentherm_gw config flow (#27148) * Add config flow support to opentherm_gw. Bump pyotgw to 0.5b0 (required for connection testing) Existing entries in configuration.yaml will be converted to config entries and ignored in future runs. * Fix not connecting to Gateway on startup. Pylint fixes. * Add tests for config flow. Remove non-essential options from config flow. Restructure config entry data. * Make sure gw_id is slugified --- .coveragerc | 5 +- .../opentherm_gw/.translations/en.json | 23 +++ .../opentherm_gw/.translations/nl.json | 23 +++ .../components/opentherm_gw/__init__.py | 70 +++++--- .../components/opentherm_gw/binary_sensor.py | 14 +- .../components/opentherm_gw/climate.py | 19 +- .../components/opentherm_gw/config_flow.py | 91 ++++++++++ .../components/opentherm_gw/const.py | 2 + .../components/opentherm_gw/manifest.json | 7 +- .../components/opentherm_gw/sensor.py | 15 +- .../components/opentherm_gw/strings.json | 23 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/opentherm_gw/__init__.py | 1 + .../opentherm_gw/test_config_flow.py | 163 ++++++++++++++++++ 17 files changed, 413 insertions(+), 50 deletions(-) create mode 100644 homeassistant/components/opentherm_gw/.translations/en.json create mode 100644 homeassistant/components/opentherm_gw/.translations/nl.json create mode 100644 homeassistant/components/opentherm_gw/config_flow.py create mode 100644 homeassistant/components/opentherm_gw/strings.json create mode 100644 tests/components/opentherm_gw/__init__.py create mode 100644 tests/components/opentherm_gw/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 25ee023ac84..a6f65430074 100644 --- a/.coveragerc +++ b/.coveragerc @@ -464,7 +464,10 @@ omit = homeassistant/components/openhome/media_player.py homeassistant/components/opensensemap/air_quality.py homeassistant/components/opensky/sensor.py - homeassistant/components/opentherm_gw/* + homeassistant/components/opentherm_gw/__init__.py + homeassistant/components/opentherm_gw/binary_sensor.py + homeassistant/components/opentherm_gw/climate.py + homeassistant/components/opentherm_gw/sensor.py homeassistant/components/openuv/__init__.py homeassistant/components/openuv/binary_sensor.py homeassistant/components/openuv/sensor.py diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json new file mode 100644 index 00000000000..65d7d9e92bb --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway already configured", + "id_exists": "Gateway id already exists", + "serial_error": "Error connecting to device", + "timeout": "Connection attempt timed out" + }, + "step": { + "init": { + "data": { + "device": "Path or URL", + "floor_temperature": "Floor climate temperature", + "id": "ID", + "name": "Name", + "precision": "Climate temperature precision" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json new file mode 100644 index 00000000000..ef3daafe4fe --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway is reeds geconfigureerd", + "id_exists": "Gateway id bestaat reeds", + "serial_error": "Kan niet verbinden met de Gateway", + "timeout": "Time-out van de verbinding" + }, + "step": { + "init": { + "data": { + "device": "Pad of URL", + "floor_temperature": "Thermostaat temperaturen naar beneden afronden", + "id": "ID", + "name": "Naam", + "precision": "Thermostaat temperatuur precisie" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index a32c375ac65..ba6de4c0bea 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -6,6 +6,7 @@ import pyotgw import pyotgw.vars as gw_vars import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR from homeassistant.components.climate import DOMAIN as COMP_CLIMATE from homeassistant.components.sensor import DOMAIN as COMP_SENSOR @@ -16,13 +17,13 @@ from homeassistant.const import ( ATTR_TEMPERATURE, ATTR_TIME, CONF_DEVICE, + CONF_ID, CONF_NAME, EVENT_HOMEASSISTANT_STOP, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, ) -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send import homeassistant.helpers.config_validation as cv @@ -36,6 +37,7 @@ from .const import ( CONF_PRECISION, DATA_GATEWAYS, DATA_OPENTHERM_GW, + DOMAIN, SERVICE_RESET_GATEWAY, SERVICE_SET_CLOCK, SERVICE_SET_CONTROL_SETPOINT, @@ -50,8 +52,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -DOMAIN = "opentherm_gw" - CLIMATE_SCHEMA = vol.Schema( { vol.Optional(CONF_PRECISION): vol.In( @@ -75,25 +75,38 @@ CONFIG_SCHEMA = vol.Schema( ) +async def async_setup_entry(hass, config_entry): + """Set up the OpenTherm Gateway component.""" + if DATA_OPENTHERM_GW not in hass.data: + hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}} + + gateway = OpenThermGatewayDevice(hass, config_entry) + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] = gateway + + # Schedule directly on the loop to avoid blocking HA startup. + hass.loop.create_task(gateway.connect_and_subscribe()) + + for comp in [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR]: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, comp) + ) + + register_services(hass) + return True + + async def async_setup(hass, config): """Set up the OpenTherm Gateway component.""" - conf = config[DOMAIN] - hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}} - for gw_id, cfg in conf.items(): - gateway = OpenThermGatewayDevice(hass, gw_id, cfg) - hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] = gateway - hass.async_create_task( - async_load_platform(hass, COMP_CLIMATE, DOMAIN, gw_id, config) - ) - hass.async_create_task( - async_load_platform(hass, COMP_BINARY_SENSOR, DOMAIN, gw_id, config) - ) - hass.async_create_task( - async_load_platform(hass, COMP_SENSOR, DOMAIN, gw_id, config) - ) - # Schedule directly on the loop to avoid blocking HA startup. - hass.loop.create_task(gateway.connect_and_subscribe(cfg[CONF_DEVICE])) - register_services(hass) + if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: + conf = config[DOMAIN] + for device_id, device_config in conf.items(): + device_config[CONF_ID] = device_id + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=device_config + ) + ) return True @@ -326,20 +339,21 @@ def register_services(hass): class OpenThermGatewayDevice: """OpenTherm Gateway device class.""" - def __init__(self, hass, gw_id, config): + def __init__(self, hass, config_entry): """Initialize the OpenTherm Gateway.""" self.hass = hass - self.gw_id = gw_id - self.name = config.get(CONF_NAME, gw_id) - self.climate_config = config[CONF_CLIMATE] + self.device_path = config_entry.data[CONF_DEVICE] + self.gw_id = config_entry.data[CONF_ID] + self.name = config_entry.data[CONF_NAME] + self.climate_config = config_entry.options self.status = {} - self.update_signal = f"{DATA_OPENTHERM_GW}_{gw_id}_update" + self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" self.gateway = pyotgw.pyotgw() - async def connect_and_subscribe(self, device_path): + async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" - await self.gateway.connect(self.hass.loop, device_path) - _LOGGER.debug("Connected to OpenTherm Gateway at %s", device_path) + await self.gateway.connect(self.hass.loop, self.device_path) + _LOGGER.debug("Connected to OpenTherm Gateway at %s", self.device_path) async def cleanup(event): """Reset overrides on the gateway.""" diff --git a/homeassistant/components/opentherm_gw/binary_sensor.py b/homeassistant/components/opentherm_gw/binary_sensor.py index 614829265e2..36867feda61 100644 --- a/homeassistant/components/opentherm_gw/binary_sensor.py +++ b/homeassistant/components/opentherm_gw/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice +from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import async_generate_entity_id @@ -12,18 +13,21 @@ from .const import BINARY_SENSOR_INFO, DATA_GATEWAYS, DATA_OPENTHERM_GW _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway binary sensors.""" - if discovery_info is None: - return - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] sensors = [] for var, info in BINARY_SENSOR_INFO.items(): device_class = info[0] friendly_name_format = info[1] sensors.append( - OpenThermBinarySensor(gw_dev, var, device_class, friendly_name_format) + OpenThermBinarySensor( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], + var, + device_class, + friendly_name_format, + ) ) + async_add_entities(sensors) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index fab028560bb..19763121e89 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -17,6 +17,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ( ATTR_TEMPERATURE, + CONF_ID, PRECISION_HALVES, PRECISION_TENTHS, PRECISION_WHOLE, @@ -33,12 +34,16 @@ _LOGGER = logging.getLogger(__name__) SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the opentherm_gw device.""" - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up an OpenTherm Gateway climate entity.""" + ents = [] + ents.append( + OpenThermClimate( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] + ) + ) - gateway = OpenThermClimate(gw_dev) - async_add_entities([gateway]) + async_add_entities(ents) class OpenThermClimate(ClimateDevice): @@ -48,7 +53,7 @@ class OpenThermClimate(ClimateDevice): """Initialize the device.""" self._gateway = gw_dev self.friendly_name = gw_dev.name - self.floor_temp = gw_dev.climate_config[CONF_FLOOR_TEMP] + self.floor_temp = gw_dev.climate_config.get(CONF_FLOOR_TEMP) self.temp_precision = gw_dev.climate_config.get(CONF_PRECISION) self._current_operation = None self._current_temperature = None @@ -62,7 +67,7 @@ class OpenThermClimate(ClimateDevice): async def async_added_to_hass(self): """Connect to the OpenTherm Gateway device.""" - _LOGGER.debug("Added device %s", self.friendly_name) + _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name) async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py new file mode 100644 index 00000000000..e1b68f1ae49 --- /dev/null +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -0,0 +1,91 @@ +"""OpenTherm Gateway config flow.""" +import asyncio +from serial import SerialException + +import pyotgw +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME + +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN + + +class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """OpenTherm Gateway Config Flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_init(self, info=None): + """Handle config flow initiation.""" + if info: + name = info[CONF_NAME] + device = info[CONF_DEVICE] + gw_id = cv.slugify(info.get(CONF_ID, name)) + + entries = [e.data for e in self.hass.config_entries.async_entries(DOMAIN)] + + if gw_id in [e[CONF_ID] for e in entries]: + return self._show_form({"base": "id_exists"}) + + if device in [e[CONF_DEVICE] for e in entries]: + return self._show_form({"base": "already_configured"}) + + async def test_connection(): + """Try to connect to the OpenTherm Gateway.""" + otgw = pyotgw.pyotgw() + status = await otgw.connect(self.hass.loop, device) + await otgw.disconnect() + return status.get(pyotgw.OTGW_ABOUT) + + try: + res = await asyncio.wait_for(test_connection(), timeout=10) + except asyncio.TimeoutError: + return self._show_form({"base": "timeout"}) + except SerialException: + return self._show_form({"base": "serial_error"}) + + if res: + return self._create_entry(gw_id, name, device) + + return self._show_form() + + async def async_step_user(self, info=None): + """Handle manual initiation of the config flow.""" + return await self.async_step_init(info) + + async def async_step_import(self, import_config): + """ + Import an OpenTherm Gateway device as a config entry. + + This flow is triggered by `async_setup` for configured devices. + """ + formatted_config = { + CONF_NAME: import_config.get(CONF_NAME, import_config[CONF_ID]), + CONF_DEVICE: import_config[CONF_DEVICE], + CONF_ID: import_config[CONF_ID], + } + return await self.async_step_init(info=formatted_config) + + def _show_form(self, errors=None): + """Show the config flow form with possible errors.""" + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Required(CONF_NAME): str, + vol.Required(CONF_DEVICE): str, + vol.Optional(CONF_ID): str, + } + ), + errors=errors or {}, + ) + + def _create_entry(self, gw_id, name, device): + """Create entry for the OpenTherm Gateway device.""" + return self.async_create_entry( + title=name, data={CONF_ID: gw_id, CONF_DEVICE: device, CONF_NAME: name} + ) diff --git a/homeassistant/components/opentherm_gw/const.py b/homeassistant/components/opentherm_gw/const.py index 60042b92867..bd9b372de33 100644 --- a/homeassistant/components/opentherm_gw/const.py +++ b/homeassistant/components/opentherm_gw/const.py @@ -18,6 +18,8 @@ DEVICE_CLASS_COLD = "cold" DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_PROBLEM = "problem" +DOMAIN = "opentherm_gw" + SERVICE_RESET_GATEWAY = "reset_gateway" SERVICE_SET_CLOCK = "set_clock" SERVICE_SET_CONTROL_SETPOINT = "set_control_setpoint" diff --git a/homeassistant/components/opentherm_gw/manifest.json b/homeassistant/components/opentherm_gw/manifest.json index 9c7f165c6df..a632096cd75 100644 --- a/homeassistant/components/opentherm_gw/manifest.json +++ b/homeassistant/components/opentherm_gw/manifest.json @@ -3,10 +3,11 @@ "name": "Opentherm Gateway", "documentation": "https://www.home-assistant.io/integrations/opentherm_gw", "requirements": [ - "pyotgw==0.4b4" + "pyotgw==0.5b0" ], "dependencies": [], "codeowners": [ "@mvn23" - ] -} + ], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/sensor.py b/homeassistant/components/opentherm_gw/sensor.py index 1449caf5def..c77a73cd180 100644 --- a/homeassistant/components/opentherm_gw/sensor.py +++ b/homeassistant/components/opentherm_gw/sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.sensor import ENTITY_ID_FORMAT +from homeassistant.const import CONF_ID from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity, async_generate_entity_id @@ -12,19 +13,23 @@ from .const import DATA_GATEWAYS, DATA_OPENTHERM_GW, SENSOR_INFO _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the OpenTherm Gateway sensors.""" - if discovery_info is None: - return - gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info] sensors = [] for var, info in SENSOR_INFO.items(): device_class = info[0] unit = info[1] friendly_name_format = info[2] sensors.append( - OpenThermSensor(gw_dev, var, device_class, unit, friendly_name_format) + OpenThermSensor( + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], + var, + device_class, + unit, + friendly_name_format, + ) ) + async_add_entities(sensors) diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json new file mode 100644 index 00000000000..a62a4625049 --- /dev/null +++ b/homeassistant/components/opentherm_gw/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "OpenTherm Gateway", + "step": { + "init": { + "title": "OpenTherm Gateway", + "data": { + "name": "Name", + "device": "Path or URL", + "id": "ID", + "precision": "Climate temperature precision", + "floor_temperature": "Floor climate temperature" + } + } + }, + "error": { + "already_configured": "Gateway already configured", + "id_exists": "Gateway id already exists", + "serial_error": "Error connecting to device", + "timeout": "Connection attempt timed out" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7557fc32b40..1eb08709741 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -45,6 +45,7 @@ FLOWS = [ "mqtt", "nest", "notion", + "opentherm_gw", "openuv", "owntracks", "plaato", diff --git a/requirements_all.txt b/requirements_all.txt index 9ce8b070b1e..9f0ee493fa1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1370,7 +1370,7 @@ pyoppleio==1.0.5 pyota==2.0.5 # homeassistant.components.opentherm_gw -pyotgw==0.4b4 +pyotgw==0.5b0 # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e948ded8a7..e8114352b04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -335,6 +335,9 @@ pynx584==0.4 # homeassistant.components.openuv pyopenuv==1.0.9 +# homeassistant.components.opentherm_gw +pyotgw==0.5b0 + # homeassistant.auth.mfa_modules.notify # homeassistant.auth.mfa_modules.totp # homeassistant.components.otp diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index a6f2584e256..70c81c66025 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -138,6 +138,7 @@ TEST_REQUIREMENTS = ( "pynws", "pynx584", "pyopenuv", + "pyotgw", "pyotp", "pyps4-homeassistant", "pyqwikswitch", diff --git a/tests/components/opentherm_gw/__init__.py b/tests/components/opentherm_gw/__init__.py new file mode 100644 index 00000000000..2dfe9267651 --- /dev/null +++ b/tests/components/opentherm_gw/__init__.py @@ -0,0 +1 @@ +"""Tests for the Opentherm Gateway integration.""" diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py new file mode 100644 index 00000000000..da80e2f9fbb --- /dev/null +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -0,0 +1,163 @@ +"""Test the Opentherm Gateway config flow.""" +import asyncio +from serial import SerialException +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME +from homeassistant.components.opentherm_gw.const import DOMAIN + +from pyotgw import OTGW_ABOUT +from tests.common import mock_coro + + +async def test_form_user(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Test Entry 1" + assert result2["data"] == { + CONF_NAME: "Test Entry 1", + CONF_DEVICE: "/dev/ttyUSB0", + CONF_ID: "test_entry_1", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_import(hass): + """Test import from existing config.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_ID: "legacy_gateway", CONF_DEVICE: "/dev/ttyUSB1"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "legacy_gateway" + assert result["data"] == { + CONF_NAME: "legacy_gateway", + CONF_DEVICE: "/dev/ttyUSB1", + CONF_ID: "legacy_gateway", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_duplicate_entries(hass): + """Test duplicate device or id errors.""" + flow1 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + flow2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + flow3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.opentherm_gw.async_setup", + return_value=mock_coro(True), + ) as mock_setup, patch( + "homeassistant.components.opentherm_gw.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry, patch( + "pyotgw.pyotgw.connect", + return_value=mock_coro({OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}), + ) as mock_pyotgw_connect, patch( + "pyotgw.pyotgw.disconnect", return_value=mock_coro(None) + ) as mock_pyotgw_disconnect: + result1 = await hass.config_entries.flow.async_configure( + flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + result2 = await hass.config_entries.flow.async_configure( + flow2["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB1"} + ) + result3 = await hass.config_entries.flow.async_configure( + flow3["flow_id"], {CONF_NAME: "Test Entry 2", CONF_DEVICE: "/dev/ttyUSB0"} + ) + assert result1["type"] == "create_entry" + assert result2["type"] == "form" + assert result2["errors"] == {"base": "id_exists"} + assert result3["type"] == "form" + assert result3["errors"] == {"base": "already_configured"} + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_pyotgw_connect.mock_calls) == 1 + assert len(mock_pyotgw_disconnect.mock_calls) == 1 + + +async def test_form_connection_timeout(hass): + """Test we handle connection timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyotgw.pyotgw.connect", side_effect=(asyncio.TimeoutError) + ) as mock_connect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "timeout"} + assert len(mock_connect.mock_calls) == 1 + + +async def test_form_connection_error(hass): + """Test we handle serial connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("pyotgw.pyotgw.connect", side_effect=(SerialException)) as mock_connect: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "serial_error"} + assert len(mock_connect.mock_calls) == 1 From bbd2078986a765c36b554680f21236365b4dbafa Mon Sep 17 00:00:00 2001 From: Zach Date: Fri, 4 Oct 2019 20:48:45 -0400 Subject: [PATCH 036/639] Add doods contains flags on areas to allow specifying overlap (#27035) * Add support for the contains flags on areas to allow specifying overlap vs contains * Remove draw_box * Add timeout option * Fix import for CONF_TIMEOUT * Change contains to covers --- .../components/doods/image_processing.py | 67 ++++++++++++++----- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/doods/image_processing.py b/homeassistant/components/doods/image_processing.py index 3eec85b3e53..02d7ce26f1c 100644 --- a/homeassistant/components/doods/image_processing.py +++ b/homeassistant/components/doods/image_processing.py @@ -7,6 +7,7 @@ import voluptuous as vol from PIL import Image, ImageDraw from pydoods import PyDOODS +from homeassistant.const import CONF_TIMEOUT from homeassistant.components.image_processing import ( CONF_CONFIDENCE, CONF_ENTITY_ID, @@ -31,6 +32,7 @@ CONF_AUTH_KEY = "auth_key" CONF_DETECTOR = "detector" CONF_LABELS = "labels" CONF_AREA = "area" +CONF_COVERS = "covers" CONF_TOP = "top" CONF_BOTTOM = "bottom" CONF_RIGHT = "right" @@ -43,6 +45,7 @@ AREA_SCHEMA = vol.Schema( vol.Optional(CONF_LEFT, default=0): cv.small_float, vol.Optional(CONF_RIGHT, default=1): cv.small_float, vol.Optional(CONF_TOP, default=0): cv.small_float, + vol.Optional(CONF_COVERS, default=True): cv.boolean, } ) @@ -50,7 +53,7 @@ LABEL_SCHEMA = vol.Schema( { vol.Required(CONF_NAME): cv.string, vol.Optional(CONF_AREA): AREA_SCHEMA, - vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), + vol.Optional(CONF_CONFIDENCE): vol.Range(min=0, max=100), } ) @@ -58,6 +61,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_URL): cv.string, vol.Required(CONF_DETECTOR): cv.string, + vol.Required(CONF_TIMEOUT, default=90): cv.positive_int, vol.Optional(CONF_AUTH_KEY, default=""): cv.string, vol.Optional(CONF_FILE_OUT, default=[]): vol.All(cv.ensure_list, [cv.template]), vol.Optional(CONF_CONFIDENCE, default=0.0): vol.Range(min=0, max=100), @@ -74,8 +78,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): url = config[CONF_URL] auth_key = config[CONF_AUTH_KEY] detector_name = config[CONF_DETECTOR] + timeout = config[CONF_TIMEOUT] - doods = PyDOODS(url, auth_key) + doods = PyDOODS(url, auth_key, timeout) response = doods.get_detectors() if not isinstance(response, dict): _LOGGER.warning("Could not connect to doods server: %s", url) @@ -140,6 +145,7 @@ class Doods(ImageProcessingEntity): # handle labels and specific detection areas labels = config[CONF_LABELS] self._label_areas = {} + self._label_covers = {} for label in labels: if isinstance(label, dict): label_name = label[CONF_NAME] @@ -147,14 +153,17 @@ class Doods(ImageProcessingEntity): _LOGGER.warning("Detector does not support label %s", label_name) continue - # Label Confidence - label_confidence = label[CONF_CONFIDENCE] + # If label confidence is not specified, use global confidence + label_confidence = label.get(CONF_CONFIDENCE) + if not label_confidence: + label_confidence = confidence if label_name not in dconfig or dconfig[label_name] > label_confidence: dconfig[label_name] = label_confidence # Label area label_area = label.get(CONF_AREA) self._label_areas[label_name] = [0, 0, 1, 1] + self._label_covers[label_name] = True if label_area: self._label_areas[label_name] = [ label_area[CONF_TOP], @@ -162,6 +171,7 @@ class Doods(ImageProcessingEntity): label_area[CONF_BOTTOM], label_area[CONF_RIGHT], ] + self._label_covers[label_name] = label_area[CONF_COVERS] else: if label not in detector["labels"] and label != "*": _LOGGER.warning("Detector does not support label %s", label) @@ -175,6 +185,7 @@ class Doods(ImageProcessingEntity): # Handle global detection area self._area = [0, 0, 1, 1] + self._covers = True area_config = config.get(CONF_AREA) if area_config: self._area = [ @@ -183,6 +194,7 @@ class Doods(ImageProcessingEntity): area_config[CONF_BOTTOM], area_config[CONF_RIGHT], ] + self._covers = area_config[CONF_COVERS] template.attach(hass, self._file_out) @@ -308,22 +320,41 @@ class Doods(ImageProcessingEntity): continue # Exclude matches outside global area definition - if ( - boxes[0] < self._area[0] - or boxes[1] < self._area[1] - or boxes[2] > self._area[2] - or boxes[3] > self._area[3] - ): - continue + if self._covers: + if ( + boxes[0] < self._area[0] + or boxes[1] < self._area[1] + or boxes[2] > self._area[2] + or boxes[3] > self._area[3] + ): + continue + else: + if ( + boxes[0] > self._area[2] + or boxes[1] > self._area[3] + or boxes[2] < self._area[0] + or boxes[3] < self._area[1] + ): + continue # Exclude matches outside label specific area definition - if self._label_areas and ( - boxes[0] < self._label_areas[label][0] - or boxes[1] < self._label_areas[label][1] - or boxes[2] > self._label_areas[label][2] - or boxes[3] > self._label_areas[label][3] - ): - continue + if self._label_areas.get(label): + if self._label_covers[label]: + if ( + boxes[0] < self._label_areas[label][0] + or boxes[1] < self._label_areas[label][1] + or boxes[2] > self._label_areas[label][2] + or boxes[3] > self._label_areas[label][3] + ): + continue + else: + if ( + boxes[0] > self._label_areas[label][2] + or boxes[1] > self._label_areas[label][3] + or boxes[2] < self._label_areas[label][0] + or boxes[3] < self._label_areas[label][1] + ): + continue if label not in matches: matches[label] = [] From 71a351605382136c5580a2eb8f46d5b24216ac64 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 4 Oct 2019 22:28:55 -0400 Subject: [PATCH 037/639] Guard against network errors for Dark Sky (#27141) * Guard against network errors for Dark Sky - Prevents network errors from throwing an exception during state updates for the Dark Sky weather component. * Implement `available` for Dark Sky component * unknown -> unavailable --- homeassistant/components/darksky/weather.py | 8 +++++++- tests/components/darksky/test_weather.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 5296f346626..dc5708d12a0 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -102,6 +102,11 @@ class DarkSkyWeather(WeatherEntity): self._ds_hourly = None self._ds_daily = None + @property + def available(self): + """Return if weather data is available from Dark Sky.""" + return self._ds_data is not None + @property def attribution(self): """Return the attribution.""" @@ -215,7 +220,8 @@ class DarkSkyWeather(WeatherEntity): self._dark_sky.update() self._ds_data = self._dark_sky.data - self._ds_currently = self._dark_sky.currently.d + currently = self._dark_sky.currently + self._ds_currently = currently.d if currently else {} self._ds_hourly = self._dark_sky.hourly self._ds_daily = self._dark_sky.daily diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py index ea28d3facb9..ca328f45839 100644 --- a/tests/components/darksky/test_weather.py +++ b/tests/components/darksky/test_weather.py @@ -6,6 +6,8 @@ from unittest.mock import patch import forecastio import requests_mock +from requests.exceptions import ConnectionError + from homeassistant.components import weather from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.setup import setup_component @@ -48,3 +50,16 @@ class TestDarkSky(unittest.TestCase): state = self.hass.states.get("weather.test") assert state.state == "sunny" + + @patch("forecastio.load_forecast", side_effect=ConnectionError()) + def test_failed_setup(self, mock_load_forecast): + """Test to ensure that a network error does not break component state.""" + + assert setup_component( + self.hass, + weather.DOMAIN, + {"weather": {"name": "test", "platform": "darksky", "api_key": "foo"}}, + ) + + state = self.hass.states.get("weather.test") + assert state.state == "unavailable" From 2e17ad86af660091591facc8cf682197bffb6d04 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 5 Oct 2019 11:59:34 +0200 Subject: [PATCH 038/639] Adds guards for missing information in call stack frames (#27217) --- homeassistant/helpers/config_validation.py | 3 ++- homeassistant/helpers/deprecation.py | 10 +++++++++- homeassistant/util/logging.py | 22 ++++++++++++++++++---- tests/helpers/test_config_validation.py | 5 ++++- tests/util/test_logging.py | 6 +----- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 2d1bb89d23a..4d5df8785e2 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -600,7 +600,8 @@ def deprecated( if module is not None: module_name = module.__name__ else: - # Unclear when it is None, but it happens, so let's guard. + # If Python is unable to access the sources files, the call stack frame + # will be missing information, so let's guard. # https://github.com/home-assistant/home-assistant/issues/24982 module_name = __name__ diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 881534b5bed..2a4fafde75b 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -54,7 +54,15 @@ def get_deprecated( and a warning is issued to the user. """ if old_name in config: - module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ # type: ignore + module = inspect.getmodule(inspect.stack()[1][0]) + if module is not None: + module_name = module.__name__ + else: + # If Python is unable to access the sources files, the call stack frame + # will be missing information, so let's guard. + # https://github.com/home-assistant/home-assistant/issues/24982 + module_name = __name__ + logger = logging.getLogger(module_name) logger.warning( "'%s' is deprecated. Please rename '%s' to '%s' in your " diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 99e606d2866..de04f23d9dd 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -130,7 +130,15 @@ def catch_log_exception( """Decorate a callback to catch and log exceptions.""" def log_exception(*args: Any) -> None: - module_name = inspect.getmodule(inspect.trace()[1][0]).__name__ # type: ignore + module = inspect.getmodule(inspect.stack()[1][0]) + if module is not None: + module_name = module.__name__ + else: + # If Python is unable to access the sources files, the call stack frame + # will be missing information, so let's guard. + # https://github.com/home-assistant/home-assistant/issues/24982 + module_name = __name__ + # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) @@ -178,9 +186,15 @@ def catch_log_coro_exception( try: return await target except Exception: # pylint: disable=broad-except - module_name = inspect.getmodule( # type: ignore - inspect.trace()[1][0] - ).__name__ + module = inspect.getmodule(inspect.stack()[1][0]) + if module is not None: + module_name = module.__name__ + else: + # If Python is unable to access the sources files, the frame + # will be missing information, so let's guard. + # https://github.com/home-assistant/home-assistant/issues/24982 + module_name = __name__ + # Do not print the wrapper in the traceback frames = len(inspect.trace()) - 1 exc_msg = traceback.format_exc(-frames) diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index e09f8cf57aa..1f5d6ddfc40 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -494,7 +494,10 @@ def test_deprecated_with_no_optionals(caplog, schema): test_data = {"mars": True} output = deprecated_schema(test_data.copy()) assert len(caplog.records) == 1 - assert caplog.records[0].name == __name__ + assert caplog.records[0].name in [ + __name__, + "homeassistant.helpers.config_validation", + ] assert ( "The 'mars' option (with value 'True') is deprecated, " "please remove it from your configuration" diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 414b246466c..d5f8eb4a2c7 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -72,12 +72,8 @@ async def test_async_create_catching_coro(hass, caplog): async def job(): raise Exception("This is a bad coroutine") - pass hass.async_create_task(logging_util.async_create_catching_coro(job())) await hass.async_block_till_done() assert "This is a bad coroutine" in caplog.text - assert ( - "hass.async_create_task(" - "logging_util.async_create_catching_coro(job()))" in caplog.text - ) + assert "in test_async_create_catching_coro" in caplog.text From a9073451f818d65e16f4c1c2dcf3952e8c23ac39 Mon Sep 17 00:00:00 2001 From: MagicalTrev89 <52410270+MagicalTrev89@users.noreply.github.com> Date: Sat, 5 Oct 2019 14:52:42 +0100 Subject: [PATCH 039/639] Add hive trv support (#27033) * TRV-Support * pyhive import update * Moved HVAC to new line * updated pyhiveapi version * Update for pylint errors * Fix Pylint Errors * Fixed Pylint 2 * removed whitespace * Black * Updates following review * updated phyhive to 0.2.19.3 * Corrected logic on TRV name * updated requirements as requested * Black run --- homeassistant/components/hive/__init__.py | 1 + homeassistant/components/hive/climate.py | 27 +++++++++++++++++++-- homeassistant/components/hive/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 3301097bab7..976821513b6 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -82,6 +82,7 @@ class HiveSession: switch = None weather = None attributes = None + trv = None def setup(hass, config): diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 1fb77ce6cb9..ed13e3019ce 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -8,6 +8,9 @@ from homeassistant.components.climate.const import ( PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + CURRENT_HVAC_HEAT, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -26,6 +29,12 @@ HASS_TO_HIVE_STATE = { HVAC_MODE_OFF: "OFF", } +HIVE_TO_HASS_HVAC_ACTION = { + "UNKNOWN": CURRENT_HVAC_OFF, + False: CURRENT_HVAC_IDLE, + True: CURRENT_HVAC_HEAT, +} + SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE SUPPORT_HVAC = [HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF] SUPPORT_PRESET = [PRESET_NONE, PRESET_BOOST] @@ -71,7 +80,11 @@ class HiveClimateEntity(HiveEntity, ClimateDevice): """Return the name of the Climate device.""" friendly_name = "Heating" if self.node_name is not None: - friendly_name = f"{self.node_name} {friendly_name}" + if self.device_type == "TRV": + friendly_name = self.node_name + else: + friendly_name = f"{self.node_name} {friendly_name}" + return friendly_name @property @@ -95,6 +108,13 @@ class HiveClimateEntity(HiveEntity, ClimateDevice): """ return HIVE_TO_HASS_STATE[self.session.heating.get_mode(self.node_id)] + @property + def hvac_action(self): + """Return current HVAC action.""" + return HIVE_TO_HASS_HVAC_ACTION[ + self.session.heating.operational_status(self.node_id, self.device_type) + ] + @property def temperature_unit(self): """Return the unit of measurement.""" @@ -123,7 +143,10 @@ class HiveClimateEntity(HiveEntity, ClimateDevice): @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" - if self.session.heating.get_boost(self.node_id) == "ON": + if ( + self.device_type == "Heating" + and self.session.heating.get_boost(self.node_id) == "ON" + ): return PRESET_BOOST return None diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 4164283f9f8..e87e3387a62 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,7 +3,7 @@ "name": "Hive", "documentation": "https://www.home-assistant.io/integrations/hive", "requirements": [ - "pyhiveapi==0.2.19.2" + "pyhiveapi==0.2.19.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9f0ee493fa1..112304cd48a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1229,7 +1229,7 @@ pyheos==0.6.0 pyhik==0.2.3 # homeassistant.components.hive -pyhiveapi==0.2.19.2 +pyhiveapi==0.2.19.3 # homeassistant.components.homematic pyhomematic==0.1.60 From a8567a746bceb01103d942bd0982a18bff041abc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 5 Oct 2019 16:16:08 +0200 Subject: [PATCH 040/639] UniFi - Improve switch tests (#27200) * Continue rewriting tests for UniFi --- tests/components/unifi/test_device_tracker.py | 1 - tests/components/unifi/test_switch.py | 427 +++++++++++------- 2 files changed, 255 insertions(+), 173 deletions(-) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 8e05d8a1dd1..d2cedb91d8d 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -135,7 +135,6 @@ async def setup_unifi_integration( async def mock_request(self, method, path, json=None): mock_requests.append({"method": method, "path": path, "json": json}) - print(mock_requests, mock_client_responses, mock_device_responses) if path == "s/{site}/stat/sta" and mock_client_responses: return mock_client_responses.popleft() if path == "s/{site}/stat/device" and mock_device_responses: diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 7ea5e0680b9..56a96b2b5b2 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1,14 +1,10 @@ """UniFi POE control platform tests.""" from collections import deque -from unittest.mock import Mock +from copy import deepcopy -import pytest - -from tests.common import mock_coro +from asynctest import Mock, patch import aiounifi -from aiounifi.clients import Clients, ClientsAll -from aiounifi.devices import Devices from homeassistant import config_entries from homeassistant.components import unifi @@ -35,6 +31,7 @@ CLIENT_1 = { "hostname": "client_1", "ip": "10.0.0.1", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:01", "name": "POE Client 1", "oui": "Producer", @@ -47,6 +44,7 @@ CLIENT_2 = { "hostname": "client_2", "ip": "10.0.0.2", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:02", "name": "POE Client 2", "oui": "Producer", @@ -59,6 +57,7 @@ CLIENT_3 = { "hostname": "client_3", "ip": "10.0.0.3", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:03", "name": "Non-POE Client 3", "oui": "Producer", @@ -71,6 +70,7 @@ CLIENT_4 = { "hostname": "client_4", "ip": "10.0.0.4", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:04", "name": "Non-POE Client 4", "oui": "Producer", @@ -83,6 +83,7 @@ CLOUDKEY = { "hostname": "client_1", "ip": "mock-host", "is_wired": True, + "last_seen": 1562600145, "mac": "10:00:00:00:00:01", "name": "Cloud key", "oui": "Producer", @@ -96,6 +97,7 @@ POE_SWITCH_CLIENTS = [ "hostname": "client_1", "ip": "10.0.0.1", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:01", "name": "POE Client 1", "oui": "Producer", @@ -108,6 +110,7 @@ POE_SWITCH_CLIENTS = [ "hostname": "client_2", "ip": "10.0.0.2", "is_wired": True, + "last_seen": 1562600145, "mac": "00:00:00:00:00:02", "name": "POE Client 2", "oui": "Producer", @@ -122,7 +125,8 @@ DEVICE_1 = { "device_id": "mock-id", "ip": "10.0.1.1", "mac": "00:00:00:00:01:01", - "type": "usw", + "last_seen": 1562600145, + "model": "US16P150", "name": "mock-name", "port_overrides": [], "port_table": [ @@ -179,6 +183,9 @@ DEVICE_1 = { "up": True, }, ], + "state": 1, + "type": "usw", + "version": "4.0.42.10433", } BLOCKED = { @@ -210,126 +217,170 @@ CONTROLLER_DATA = { CONF_PASSWORD: "mock-pswd", CONF_PORT: 1234, CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: True, + CONF_VERIFY_SSL: False, } ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") +SITES = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "admin"}} -@pytest.fixture -def mock_controller(hass): - """Mock a UniFi Controller.""" - hass.data[UNIFI_CONFIG] = {} - hass.data[UNIFI_WIRELESS_CLIENTS] = Mock() - controller = unifi.UniFiController(hass, None) - controller.wireless_clients = set() - controller._site_role = "admin" +async def setup_unifi_integration( + hass, + config, + options, + sites, + clients_response, + devices_response, + clients_all_response, +): + """Create the UniFi controller.""" + hass.data[UNIFI_CONFIG] = [] + hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) + config_entry = config_entries.ConfigEntry( + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, + options=options, + entry_id=1, + ) - controller.api = Mock() - controller.mock_requests = [] + mock_client_responses = deque() + mock_client_responses.append(clients_response) - controller.mock_client_responses = deque() - controller.mock_device_responses = deque() - controller.mock_client_all_responses = deque() + mock_device_responses = deque() + mock_device_responses.append(devices_response) - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - controller.mock_requests.append(kwargs) - if path == "s/{site}/stat/sta": - return controller.mock_client_responses.popleft() - if path == "s/{site}/stat/device": - return controller.mock_device_responses.popleft() - if path == "s/{site}/rest/user": - return controller.mock_client_all_responses.popleft() - return None + mock_client_all_responses = deque() + mock_client_all_responses.append(clients_all_response) - controller.api.clients = Clients({}, mock_request) - controller.api.devices = Devices({}, mock_request) - controller.api.clients_all = ClientsAll({}, mock_request) + mock_requests = [] + + async def mock_request(self, method, path, json=None): + mock_requests.append({"method": method, "path": path, "json": json}) + print(mock_requests, mock_client_responses, mock_device_responses) + if path == "s/{site}/stat/sta" and mock_client_responses: + return mock_client_responses.popleft() + if path == "s/{site}/stat/device" and mock_device_responses: + return mock_device_responses.popleft() + if path == "s/{site}/rest/user" and mock_client_all_responses: + return mock_client_all_responses.popleft() + return {} + + with patch("aiounifi.Controller.login", return_value=True), patch( + "aiounifi.Controller.sites", return_value=sites + ), patch("aiounifi.Controller.request", new=mock_request): + await unifi.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + controller_id = unifi.get_controller_id_from_config_entry(config_entry) + controller = hass.data[unifi.DOMAIN][controller_id] + + controller.mock_client_responses = mock_client_responses + controller.mock_device_responses = mock_device_responses + controller.mock_client_all_responses = mock_client_all_responses + controller.mock_requests = mock_requests return controller -async def setup_controller(hass, mock_controller, options={}): - """Load the UniFi switch platform with the provided controller.""" - hass.config.components.add(unifi.DOMAIN) - hass.data[unifi.DOMAIN] = {CONTROLLER_ID: mock_controller} - config_entry = config_entries.ConfigEntry( - 1, - unifi.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - entry_id=1, - system_options={}, - options=options, - ) - mock_controller.config_entry = config_entry - - await mock_controller.async_update() - await hass.config_entries.async_forward_entry_setup(config_entry, "switch") - # To flush out the service call to update the group - await hass.async_block_till_done() - - async def test_platform_manually_configured(hass): - """Test that we do not discover anything or try to set up a bridge.""" + """Test that we do not discover anything or try to set up a controller.""" assert ( await async_setup_component( - hass, switch.DOMAIN, {"switch": {"platform": "unifi"}} + hass, switch.DOMAIN, {switch.DOMAIN: {"platform": "unifi"}} ) is True ) assert unifi.DOMAIN not in hass.data -async def test_no_clients(hass, mock_controller): +async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" - mock_controller.mock_client_responses.append({}) - mock_controller.mock_device_responses.append({}) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert not hass.states.async_all() + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 -async def test_controller_not_client(hass, mock_controller): +async def test_controller_not_client(hass): """Test that the controller doesn't become a switch.""" - mock_controller.mock_client_responses.append([CLOUDKEY]) - mock_controller.mock_device_responses.append([DEVICE_1]) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert not hass.states.async_all() + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CLOUDKEY], + devices_response=[DEVICE_1], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 cloudkey = hass.states.get("switch.cloud_key") assert cloudkey is None -async def test_not_admin(hass, mock_controller): +async def test_not_admin(hass): """Test that switch platform only work on an admin account.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([]) + sites = deepcopy(SITES) + sites["Site name"]["role"] = "not admin" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=sites, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response=[], + ) - mock_controller._site_role = "viewer" - - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert len(hass.states.async_all()) == 0 + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 -async def test_switches(hass, mock_controller): +async def test_switches(hass): """Test the update_items function with some clients.""" - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_4]) - mock_controller.mock_device_responses.append([DEVICE_1]) - mock_controller.mock_client_all_responses.append([BLOCKED, UNBLOCKED, CLIENT_1]) - options = {unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]} + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CLIENT_1, CLIENT_4], + devices_response=[DEVICE_1], + clients_all_response=[BLOCKED, UNBLOCKED, CLIENT_1], + ) - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 4 + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 6 switch_1 = hass.states.get("switch.poe_client_1") assert switch_1 is not None @@ -353,25 +404,34 @@ async def test_switches(hass, mock_controller): assert unblocked.state == "on" -async def test_new_client_discovered(hass, mock_controller): +async def test_new_client_discovered(hass): """Test if 2nd update has a new client.""" - mock_controller.mock_client_responses.append([CLIENT_1]) - mock_controller.mock_device_responses.append([DEVICE_1]) + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CLIENT_1], + devices_response=[DEVICE_1], + clients_all_response=[], + ) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - assert len(hass.states.async_all()) == 2 + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 4 - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) - mock_controller.mock_device_responses.append([DEVICE_1]) + controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) + controller.mock_device_responses.append([DEVICE_1]) # Calling a service will trigger the updates to run await hass.services.async_call( "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True ) - assert len(mock_controller.mock_requests) == 5 - assert len(hass.states.async_all()) == 3 - assert mock_controller.mock_requests[2] == { + assert len(controller.mock_requests) == 6 + assert len(hass.states.async_all()) == 5 + assert controller.mock_requests[3] == { "json": { "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}] }, @@ -382,8 +442,8 @@ async def test_new_client_discovered(hass, mock_controller): await hass.services.async_call( "switch", "turn_on", {"entity_id": "switch.poe_client_1"}, blocking=True ) - assert len(mock_controller.mock_requests) == 7 - assert mock_controller.mock_requests[5] == { + assert len(controller.mock_requests) == 9 + assert controller.mock_requests[3] == { "json": { "port_overrides": [ {"port_idx": 1, "portconf_id": "1a1", "poe_mode": "auto"} @@ -398,66 +458,24 @@ async def test_new_client_discovered(hass, mock_controller): assert switch_2.state == "on" -async def test_failed_update_successful_login(hass, mock_controller): - """Running update can login when requested.""" - mock_controller.available = False - mock_controller.api.clients.update = Mock() - mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired - mock_controller.api.login = Mock() - mock_controller.api.login.return_value = mock_coro() - - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 0 - - assert mock_controller.available is True - - -async def test_failed_update_failed_login(hass, mock_controller): - """Running update can handle a failed login.""" - mock_controller.api.clients.update = Mock() - mock_controller.api.clients.update.side_effect = aiounifi.LoginRequired - mock_controller.api.login = Mock() - mock_controller.api.login.side_effect = aiounifi.AiounifiException - - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 0 - - assert mock_controller.available is False - - -async def test_failed_update_unreachable_controller(hass, mock_controller): - """Running update can handle a unreachable controller.""" - mock_controller.mock_client_responses.append([CLIENT_1, CLIENT_2]) - mock_controller.mock_device_responses.append([DEVICE_1]) - - await setup_controller(hass, mock_controller) - - mock_controller.api.clients.update = Mock() - mock_controller.api.clients.update.side_effect = aiounifi.AiounifiException - - # Calling a service will trigger the updates to run - await hass.services.async_call( - "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True - ) - - assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 3 - - assert mock_controller.available is False - - -async def test_ignore_multiple_poe_clients_on_same_port(hass, mock_controller): +async def test_ignore_multiple_poe_clients_on_same_port(hass): """Ignore when there are multiple POE driven clients on same port. If there is a non-UniFi switch powered by POE, clients will be transparently marked as having POE as well. """ - mock_controller.mock_client_responses.append(POE_SWITCH_CLIENTS) - mock_controller.mock_device_responses.append([DEVICE_1]) - await setup_controller(hass, mock_controller) - assert len(mock_controller.mock_requests) == 2 - # 1 All Lights group, 2 lights - assert len(hass.states.async_all()) == 0 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=POE_SWITCH_CLIENTS, + devices_response=[DEVICE_1], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 5 switch_1 = hass.states.get("switch.poe_client_1") switch_2 = hass.states.get("switch.poe_client_2") @@ -465,22 +483,18 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass, mock_controller): assert switch_2 is None -async def test_restoring_client(hass, mock_controller): +async def test_restoring_client(hass): """Test the update_items function with some clients.""" - mock_controller.mock_client_responses.append([CLIENT_2]) - mock_controller.mock_device_responses.append([DEVICE_1]) - mock_controller.mock_client_all_responses.append([CLIENT_1]) - options = {unifi.CONF_BLOCK_CLIENT: ["random mac"]} - config_entry = config_entries.ConfigEntry( - 1, - unifi.DOMAIN, - "Mock Title", - ENTRY_CONFIG, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - entry_id=1, + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=ENTRY_CONFIG, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, system_options={}, + options={}, + entry_id=1, ) registry = await entity_registry.async_get_registry(hass) @@ -499,9 +513,78 @@ async def test_restoring_client(hass, mock_controller): config_entry=config_entry, ) - await setup_controller(hass, mock_controller, options) - assert len(mock_controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 3 + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: ["random mac"], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[CLIENT_2], + devices_response=[DEVICE_1], + clients_all_response=[CLIENT_1], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 5 device_1 = hass.states.get("switch.client_1") assert device_1 is not None + + +async def test_failed_update_failed_login(hass): + """Running update can handle a failed login.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: ["random mac"], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 + + with patch.object( + controller.api.clients, "update", side_effect=aiounifi.LoginRequired + ), patch.object(controller.api, "login", side_effect=aiounifi.AiounifiException): + await controller.async_update() + await hass.async_block_till_done() + + assert controller.available is False + + +async def test_failed_update_successful_login(hass): + """Running update can login when requested.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: ["random mac"], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 + + with patch.object( + controller.api.clients, "update", side_effect=aiounifi.LoginRequired + ), patch.object(controller.api, "login", return_value=Mock(True)): + await controller.async_update() + await hass.async_block_till_done() + + assert controller.available is True From 25bfdbc8df383e801de977fa8ac1aba3fe1c3d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 5 Oct 2019 22:20:11 +0300 Subject: [PATCH 041/639] Require Python >= 3.6.1 (#27226) https://github.com/home-assistant/architecture/issues/278 --- homeassistant/bootstrap.py | 11 ----------- homeassistant/const.py | 2 +- tests/test_main.py | 29 ++++++++++++++++++++++++++++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 7c4ec731b49..ef294491141 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -97,17 +97,6 @@ async def async_from_config_dict( stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) - if sys.version_info[:3] < (3, 6, 1): - msg = ( - "Python 3.6.0 support is deprecated and will " - "be removed in the first release after October 2. Please " - "upgrade Python to 3.6.1 or higher." - ) - _LOGGER.warning(msg) - hass.components.persistent_notification.async_create( - msg, "Python version", "python_version" - ) - return hass diff --git a/homeassistant/const.py b/homeassistant/const.py index 9baa4a1f71c..e0f90834d94 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -4,7 +4,7 @@ MINOR_VERSION = 101 PATCH_VERSION = "0.dev0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) -REQUIRED_PYTHON_VER = (3, 6, 0) +REQUIRED_PYTHON_VER = (3, 6, 1) # Format for platform files PLATFORM_FORMAT = "{platform}.{domain}" diff --git a/tests/test_main.py b/tests/test_main.py index 509425ce418..29454d269af 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -2,6 +2,7 @@ from unittest.mock import patch, PropertyMock from homeassistant import __main__ as main +from homeassistant.const import REQUIRED_PYTHON_VER @patch("sys.exit") @@ -31,6 +32,32 @@ def test_validate_python(mock_exit): mock_exit.reset_mock() - with patch("sys.version_info", new_callable=PropertyMock(return_value=(3, 6, 0))): + with patch( + "sys.version_info", + new_callable=PropertyMock( + return_value=(REQUIRED_PYTHON_VER[0] - 1,) + REQUIRED_PYTHON_VER[1:] + ), + ): + main.validate_python() + assert mock_exit.called is True + + mock_exit.reset_mock() + + with patch( + "sys.version_info", new_callable=PropertyMock(return_value=REQUIRED_PYTHON_VER) + ): main.validate_python() assert mock_exit.called is False + + mock_exit.reset_mock() + + with patch( + "sys.version_info", + new_callable=PropertyMock( + return_value=(REQUIRED_PYTHON_VER[:2]) + (REQUIRED_PYTHON_VER[2] + 1,) + ), + ): + main.validate_python() + assert mock_exit.called is False + + mock_exit.reset_mock() From cc1cca0a14205018f2723ca29e7ab45898149ff4 Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 5 Oct 2019 21:31:51 +0200 Subject: [PATCH 042/639] automation_reproduce_state (#27222) --- .../components/automation/reproduce_state.py | 61 +++++++++++++++++++ .../automation/test_reproduce_state.py | 50 +++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 homeassistant/components/automation/reproduce_state.py create mode 100644 tests/components/automation/test_reproduce_state.py diff --git a/homeassistant/components/automation/reproduce_state.py b/homeassistant/components/automation/reproduce_state.py new file mode 100644 index 00000000000..553d6871087 --- /dev/null +++ b/homeassistant/components/automation/reproduce_state.py @@ -0,0 +1,61 @@ +"""Reproduce an Automation state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Automation states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/automation/test_reproduce_state.py b/tests/components/automation/test_reproduce_state.py new file mode 100644 index 00000000000..4f3fd735fc5 --- /dev/null +++ b/tests/components/automation/test_reproduce_state.py @@ -0,0 +1,50 @@ +"""Test reproduce state for Automation.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Automation states.""" + hass.states.async_set("automation.entity_off", "off", {}) + hass.states.async_set("automation.entity_on", "on", {}) + + turn_on_calls = async_mock_service(hass, "automation", "turn_on") + turn_off_calls = async_mock_service(hass, "automation", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [State("automation.entity_off", "off"), State("automation.entity_on", "on")], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("automation.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("automation.entity_on", "off"), + State("automation.entity_off", "on"), + # Should not raise + State("automation.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "automation" + assert turn_on_calls[0].data == {"entity_id": "automation.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "automation" + assert turn_off_calls[0].data == {"entity_id": "automation.entity_on"} From 3d1e743b0cf24ee8af27541f3326f3b9479faa8c Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Sat, 5 Oct 2019 21:34:18 +0200 Subject: [PATCH 043/639] Add set_location service doc (#27216) --- homeassistant/components/homeassistant/services.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/homeassistant/services.yaml b/homeassistant/components/homeassistant/services.yaml index 2219564abb8..cb3efb0d524 100644 --- a/homeassistant/components/homeassistant/services.yaml +++ b/homeassistant/components/homeassistant/services.yaml @@ -7,6 +7,16 @@ reload_core_config: restart: description: Restart the Home Assistant service. +set_location: + description: Update the Home Assistant location. + fields: + latitude: + description: Latitude of your location + example: 32.87336 + longitude: + description: Longitude of your location + example: 117.22743 + stop: description: Stop the Home Assistant service. From e088119d6d1fe5063998f047807b8be43da5273d Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 5 Oct 2019 21:42:37 +0200 Subject: [PATCH 044/639] fan_reproduce_state (#27227) --- .../components/fan/reproduce_state.py | 100 ++++++++++++++++++ tests/components/fan/test_reproduce_state.py | 89 ++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 homeassistant/components/fan/reproduce_state.py create mode 100644 tests/components/fan/test_reproduce_state.py diff --git a/homeassistant/components/fan/reproduce_state.py b/homeassistant/components/fan/reproduce_state.py new file mode 100644 index 00000000000..1053861e2bf --- /dev/null +++ b/homeassistant/components/fan/reproduce_state.py @@ -0,0 +1,100 @@ +"""Reproduce an Fan state.""" +import asyncio +import logging +from types import MappingProxyType +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + DOMAIN, + ATTR_DIRECTION, + ATTR_OSCILLATING, + ATTR_SPEED, + SERVICE_OSCILLATE, + SERVICE_SET_DIRECTION, + SERVICE_SET_SPEED, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} +ATTRIBUTES = { # attribute: service + ATTR_DIRECTION: SERVICE_SET_DIRECTION, + ATTR_OSCILLATING: SERVICE_OSCILLATE, + ATTR_SPEED: SERVICE_SET_SPEED, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and all( + check_attr_equal(cur_state.attributes, state.attributes, attr) + for attr in ATTRIBUTES + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + service_calls = {} # service: service_data + + if state.state == STATE_ON: + # The fan should be on + if cur_state.state != STATE_ON: + # Turn on the fan at first + service_calls[SERVICE_TURN_ON] = service_data + + for attr, service in ATTRIBUTES.items(): + # Call services to adjust the attributes + if attr in state.attributes and not check_attr_equal( + state.attributes, cur_state.attributes, attr + ): + data = service_data.copy() + data[attr] = state.attributes[attr] + service_calls[service] = data + + elif state.state == STATE_OFF: + service_calls[SERVICE_TURN_OFF] = service_data + + for service, data in service_calls.items(): + await hass.services.async_call( + DOMAIN, service, data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Fan states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + +def check_attr_equal( + attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str +) -> bool: + """Return true if the given attributes are equal.""" + return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/tests/components/fan/test_reproduce_state.py b/tests/components/fan/test_reproduce_state.py new file mode 100644 index 00000000000..0dcd38580b8 --- /dev/null +++ b/tests/components/fan/test_reproduce_state.py @@ -0,0 +1,89 @@ +"""Test reproduce state for Fan.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Fan states.""" + hass.states.async_set("fan.entity_off", "off", {}) + hass.states.async_set("fan.entity_on", "on", {}) + hass.states.async_set("fan.entity_speed", "on", {"speed": "high"}) + hass.states.async_set("fan.entity_oscillating", "on", {"oscillating": True}) + hass.states.async_set("fan.entity_direction", "on", {"direction": "forward"}) + + turn_on_calls = async_mock_service(hass, "fan", "turn_on") + turn_off_calls = async_mock_service(hass, "fan", "turn_off") + set_direction_calls = async_mock_service(hass, "fan", "set_direction") + oscillate_calls = async_mock_service(hass, "fan", "oscillate") + set_speed_calls = async_mock_service(hass, "fan", "set_speed") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("fan.entity_off", "off"), + State("fan.entity_on", "on"), + State("fan.entity_speed", "on", {"speed": "high"}), + State("fan.entity_oscillating", "on", {"oscillating": True}), + State("fan.entity_direction", "on", {"direction": "forward"}), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_direction_calls) == 0 + assert len(oscillate_calls) == 0 + assert len(set_speed_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("fan.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(set_direction_calls) == 0 + assert len(oscillate_calls) == 0 + assert len(set_speed_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("fan.entity_on", "off"), + State("fan.entity_off", "on"), + State("fan.entity_speed", "on", {"speed": "low"}), + State("fan.entity_oscillating", "on", {"oscillating": False}), + State("fan.entity_direction", "on", {"direction": "reverse"}), + # Should not raise + State("fan.non_existing", "on"), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "fan" + assert turn_on_calls[0].data == {"entity_id": "fan.entity_off"} + + assert len(set_direction_calls) == 1 + assert set_direction_calls[0].domain == "fan" + assert set_direction_calls[0].data == { + "entity_id": "fan.entity_direction", + "direction": "reverse", + } + + assert len(oscillate_calls) == 1 + assert oscillate_calls[0].domain == "fan" + assert oscillate_calls[0].data == { + "entity_id": "fan.entity_oscillating", + "oscillating": False, + } + + assert len(set_speed_calls) == 1 + assert set_speed_calls[0].domain == "fan" + assert set_speed_calls[0].data == {"entity_id": "fan.entity_speed", "speed": "low"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "fan" + assert turn_off_calls[0].data == {"entity_id": "fan.entity_on"} From 46ac98379eec66a5bb44a882e2d4b2d94ee0a6ee Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 5 Oct 2019 21:43:12 +0200 Subject: [PATCH 045/639] Add improved scene support to the light integration (#27182) * light reproduce state * Add types * Fix linting error * Add tests * Improve test * Fix failing tests * Another try * avoid repetition * simplified if * Remove attributes that are no attributes --- .../components/light/reproduce_state.py | 94 ++++++++++++++ .../components/light/test_reproduce_state.py | 117 ++++++++++++++++++ tests/helpers/test_state.py | 10 +- 3 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/light/reproduce_state.py create mode 100644 tests/components/light/test_reproduce_state.py diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py new file mode 100644 index 00000000000..ae618f7a8ef --- /dev/null +++ b/homeassistant/components/light/reproduce_state.py @@ -0,0 +1,94 @@ +"""Reproduce an Light state.""" +import asyncio +import logging +from types import MappingProxyType +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_ON, + STATE_OFF, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + DOMAIN, + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_RGB_COLOR, + ATTR_WHITE_VALUE, + ATTR_XY_COLOR, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_ON, STATE_OFF} +ATTR_GROUP = [ATTR_BRIGHTNESS, ATTR_EFFECT, ATTR_WHITE_VALUE] +COLOR_GROUP = [ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_RGB_COLOR, ATTR_XY_COLOR] + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and all( + check_attr_equal(cur_state.attributes, state.attributes, attr) + for attr in ATTR_GROUP + COLOR_GROUP + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATE_ON: + service = SERVICE_TURN_ON + for attr in ATTR_GROUP: + # All attributes that are not colors + if attr in state.attributes: + service_data[attr] = state.attributes[attr] + + for color_attr in COLOR_GROUP: + # Choose the first color that is specified + if color_attr in state.attributes: + service_data[color_attr] = state.attributes[color_attr] + break + + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Light states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + +def check_attr_equal( + attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str +) -> bool: + """Return true if the given attributes are equal.""" + return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py new file mode 100644 index 00000000000..92790890a4c --- /dev/null +++ b/tests/components/light/test_reproduce_state.py @@ -0,0 +1,117 @@ +"""Test reproduce state for Light.""" +from homeassistant.core import State + +from tests.common import async_mock_service + +VALID_BRIGHTNESS = {"brightness": 180} +VALID_WHITE_VALUE = {"white_value": 200} +VALID_EFFECT = {"effect": "random"} +VALID_COLOR_TEMP = {"color_temp": 240} +VALID_HS_COLOR = {"hs_color": (345, 75)} +VALID_RGB_COLOR = {"rgb_color": (255, 63, 111)} +VALID_XY_COLOR = {"xy_color": (0.59, 0.274)} + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Light states.""" + hass.states.async_set("light.entity_off", "off", {}) + hass.states.async_set("light.entity_bright", "on", VALID_BRIGHTNESS) + hass.states.async_set("light.entity_white", "on", VALID_WHITE_VALUE) + hass.states.async_set("light.entity_effect", "on", VALID_EFFECT) + hass.states.async_set("light.entity_temp", "on", VALID_COLOR_TEMP) + hass.states.async_set("light.entity_hs", "on", VALID_HS_COLOR) + hass.states.async_set("light.entity_rgb", "on", VALID_RGB_COLOR) + hass.states.async_set("light.entity_xy", "on", VALID_XY_COLOR) + + turn_on_calls = async_mock_service(hass, "light", "turn_on") + turn_off_calls = async_mock_service(hass, "light", "turn_off") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("light.entity_off", "off"), + State("light.entity_bright", "on", VALID_BRIGHTNESS), + State("light.entity_white", "on", VALID_WHITE_VALUE), + State("light.entity_effect", "on", VALID_EFFECT), + State("light.entity_temp", "on", VALID_COLOR_TEMP), + State("light.entity_hs", "on", VALID_HS_COLOR), + State("light.entity_rgb", "on", VALID_RGB_COLOR), + State("light.entity_xy", "on", VALID_XY_COLOR), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("light.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("light.entity_xy", "off"), + State("light.entity_off", "on", VALID_BRIGHTNESS), + State("light.entity_bright", "on", VALID_WHITE_VALUE), + State("light.entity_white", "on", VALID_EFFECT), + State("light.entity_effect", "on", VALID_COLOR_TEMP), + State("light.entity_temp", "on", VALID_HS_COLOR), + State("light.entity_hs", "on", VALID_RGB_COLOR), + State("light.entity_rgb", "on", VALID_XY_COLOR), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 7 + + expected_calls = [] + + expected_off = VALID_BRIGHTNESS + expected_off["entity_id"] = "light.entity_off" + expected_calls.append(expected_off) + + expected_bright = VALID_WHITE_VALUE + expected_bright["entity_id"] = "light.entity_bright" + expected_calls.append(expected_bright) + + expected_white = VALID_EFFECT + expected_white["entity_id"] = "light.entity_white" + expected_calls.append(expected_white) + + expected_effect = VALID_COLOR_TEMP + expected_effect["entity_id"] = "light.entity_effect" + expected_calls.append(expected_effect) + + expected_temp = VALID_HS_COLOR + expected_temp["entity_id"] = "light.entity_temp" + expected_calls.append(expected_temp) + + expected_hs = VALID_RGB_COLOR + expected_hs["entity_id"] = "light.entity_hs" + expected_calls.append(expected_hs) + + expected_rgb = VALID_XY_COLOR + expected_rgb["entity_id"] = "light.entity_rgb" + expected_calls.append(expected_rgb) + + for call in turn_on_calls: + assert call.domain == "light" + found = False + for expected in expected_calls: + if call.data["entity_id"] == expected["entity_id"]: + # We found the matching entry + assert call.data == expected + found = True + break + # No entry found + assert found + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "light" + assert turn_off_calls[0].data == {"entity_id": "light.entity_xy"} diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 7f428c0833d..14bcbde5094 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -129,7 +129,7 @@ async def test_reproduce_turn_on(hass): last_call = calls[-1] assert last_call.domain == "light" assert SERVICE_TURN_ON == last_call.service - assert ["light.test"] == last_call.data.get("entity_id") + assert "light.test" == last_call.data.get("entity_id") async def test_reproduce_turn_off(hass): @@ -146,7 +146,7 @@ async def test_reproduce_turn_off(hass): last_call = calls[-1] assert last_call.domain == "light" assert SERVICE_TURN_OFF == last_call.service - assert ["light.test"] == last_call.data.get("entity_id") + assert "light.test" == last_call.data.get("entity_id") async def test_reproduce_complex_data(hass): @@ -155,10 +155,10 @@ async def test_reproduce_complex_data(hass): hass.states.async_set("light.test", "off") - complex_data = ["hello", {"11": "22"}] + complex_data = [255, 100, 100] await state.async_reproduce_state( - hass, ha.State("light.test", "on", {"complex": complex_data}) + hass, ha.State("light.test", "on", {"rgb_color": complex_data}) ) await hass.async_block_till_done() @@ -167,7 +167,7 @@ async def test_reproduce_complex_data(hass): last_call = calls[-1] assert last_call.domain == "light" assert SERVICE_TURN_ON == last_call.service - assert complex_data == last_call.data.get("complex") + assert complex_data == last_call.data.get("rgb_color") async def test_reproduce_bad_state(hass): From 9c08c3588179d5adb8bfc3536f937537153d6231 Mon Sep 17 00:00:00 2001 From: definitio <37266727+definitio@users.noreply.github.com> Date: Sat, 5 Oct 2019 23:43:57 +0400 Subject: [PATCH 046/639] Improve influxdb error handling (#27225) --- homeassistant/components/influxdb/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 2bb5207aa85..86d489621ea 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -353,7 +353,11 @@ class InfluxThread(threading.Thread): _LOGGER.debug("Wrote %d events", len(json)) break - except (exceptions.InfluxDBClientError, IOError) as err: + except ( + exceptions.InfluxDBClientError, + exceptions.InfluxDBServerError, + IOError, + ) as err: if retry < self.max_tries: time.sleep(RETRY_DELAY) else: From 0b838f88c1ba03735e9f7293d399b1a2227bcc77 Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Sat, 5 Oct 2019 12:44:51 -0700 Subject: [PATCH 047/639] Bump adb-shell to 0.0.4; bump androidtv to 0.0.30 (#27224) --- homeassistant/components/androidtv/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index 4fd3b062a10..e84ed35c763 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.3", - "androidtv==0.0.29" + "adb-shell==0.0.4", + "androidtv==0.0.30" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/requirements_all.txt b/requirements_all.txt index 112304cd48a..086482eda7d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.3 +adb-shell==0.0.4 # homeassistant.components.adguard adguardhome==0.2.1 @@ -203,7 +203,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.29 +androidtv==0.0.30 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8114352b04..60780ec7c55 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -83,7 +83,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.29 +androidtv==0.0.30 # homeassistant.components.apns apns2==0.3.0 From 5ae497bfdc6dd3358752b3ff523721a10b53b871 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sat, 5 Oct 2019 21:46:16 +0200 Subject: [PATCH 048/639] Refactor Tradfri switch device (#26864) * Refactor Tradfri switch device * Lint * Lint * Removed unused constant * Add base_class * Lint * Improvements after review * Typo --- .coveragerc | 1 + .../components/tradfri/base_class.py | 96 +++++++++++++++ homeassistant/components/tradfri/switch.py | 111 +++--------------- 3 files changed, 112 insertions(+), 96 deletions(-) create mode 100644 homeassistant/components/tradfri/base_class.py diff --git a/.coveragerc b/.coveragerc index a6f65430074..5c2d2e02f45 100644 --- a/.coveragerc +++ b/.coveragerc @@ -682,6 +682,7 @@ omit = homeassistant/components/tradfri/* homeassistant/components/tradfri/light.py homeassistant/components/tradfri/cover.py + homeassistant/components/tradfri/base_class.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py homeassistant/components/transmission/__init__.py diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py new file mode 100644 index 00000000000..5fce3c08510 --- /dev/null +++ b/homeassistant/components/tradfri/base_class.py @@ -0,0 +1,96 @@ +"""Base class for IKEA TRADFRI.""" +import logging + +from pytradfri.error import PytradfriError + +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity +from . import DOMAIN as TRADFRI_DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class TradfriBaseDevice(Entity): + """Base class for a TRADFRI device.""" + + def __init__(self, device, api, gateway_id): + """Initialize a device.""" + self._available = True + self._api = api + self._device = None + self._device_control = None + self._device_data = None + self._gateway_id = gateway_id + self._name = None + self._unique_id = None + + self._refresh(device) + + @callback + def _async_start_observe(self, exc=None): + """Start observation of device.""" + if exc: + self._available = False + self.async_schedule_update_ha_state() + _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) + + try: + cmd = self._device.observe( + callback=self._observe_update, + err_callback=self._async_start_observe, + duration=0, + ) + self.hass.async_create_task(self._api(cmd)) + except PytradfriError as err: + _LOGGER.warning("Observation failed, trying again", exc_info=err) + self._async_start_observe() + + async def async_added_to_hass(self): + """Start thread when added to hass.""" + self._async_start_observe() + + @property + def available(self): + """Return True if entity is available.""" + return self._available + + @property + def device_info(self): + """Return the device info.""" + info = self._device.device_info + + return { + "identifiers": {(TRADFRI_DOMAIN, self._device.id)}, + "name": self._name, + "manufacturer": info.manufacturer, + "model": info.model_number, + "sw_version": info.firmware_version, + "via_device": (TRADFRI_DOMAIN, self._gateway_id), + } + + @property + def name(self): + """Return the display name of this device.""" + return self._name + + @property + def should_poll(self): + """No polling needed for tradfri device.""" + return False + + @property + def unique_id(self): + """Return unique ID for device.""" + return self._unique_id + + @callback + def _observe_update(self, device): + """Receive new state data for this device.""" + self._refresh(device) + self.async_schedule_update_ha_state() + + def _refresh(self, device): + """Refresh the device data.""" + self._device = device + self._name = device.name + self._available = device.reachable diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 545c1ad93ce..1e322ff47f5 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,17 +1,9 @@ """Support for IKEA Tradfri switches.""" -import logging - -from pytradfri.error import PytradfriError - from homeassistant.components.switch import SwitchDevice -from homeassistant.core import callback -from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY +from . import KEY_API, KEY_GATEWAY +from .base_class import TradfriBaseDevice from .const import CONF_GATEWAY_ID -_LOGGER = logging.getLogger(__name__) - -TRADFRI_SWITCH_MANAGER = "Tradfri Switch Manager" - async def async_setup_entry(hass, config_entry, async_add_entities): """Load Tradfri switches based on a config entry.""" @@ -28,104 +20,31 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) -class TradfriSwitch(SwitchDevice): +class TradfriSwitch(TradfriBaseDevice, SwitchDevice): """The platform class required by Home Assistant.""" - def __init__(self, switch, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a switch.""" - self._api = api - self._unique_id = f"{gateway_id}-{switch.id}" - self._switch = None - self._socket_control = None - self._switch_data = None - self._name = None - self._available = True - self._gateway_id = gateway_id + super().__init__(device, api, gateway_id) + self._unique_id = f"{gateway_id}-{device.id}" - self._refresh(switch) + def _refresh(self, device): + """Refresh the switch data.""" + super()._refresh(device) - @property - def unique_id(self): - """Return unique ID for switch.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - info = self._switch.device_info - - return { - "identifiers": {(TRADFRI_DOMAIN, self._switch.id)}, - "name": self._name, - "manufacturer": info.manufacturer, - "model": info.model_number, - "sw_version": info.firmware_version, - "via_device": (TRADFRI_DOMAIN, self._gateway_id), - } - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def should_poll(self): - """No polling needed for tradfri switch.""" - return False - - @property - def name(self): - """Return the display name of this switch.""" - return self._name + # Caching of switch control and switch object + self._device_control = device.socket_control + self._device_data = device.socket_control.sockets[0] @property def is_on(self): """Return true if switch is on.""" - return self._switch_data.state + return self._device_data.state async def async_turn_off(self, **kwargs): """Instruct the switch to turn off.""" - await self._api(self._socket_control.set_state(False)) + await self._api(self._device_control.set_state(False)) async def async_turn_on(self, **kwargs): """Instruct the switch to turn on.""" - await self._api(self._socket_control.set_state(True)) - - @callback - def _async_start_observe(self, exc=None): - """Start observation of switch.""" - if exc: - self._available = False - self.async_schedule_update_ha_state() - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._switch.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, switch): - """Refresh the switch data.""" - self._switch = switch - - # Caching of switchControl and switch object - self._available = switch.reachable - self._socket_control = switch.socket_control - self._switch_data = switch.socket_control.sockets[0] - self._name = switch.name - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this switch.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() + await self._api(self._device_control.set_state(True)) From 601d15701bd899ade881567ba1ab857d11d6982c Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 5 Oct 2019 21:57:12 +0200 Subject: [PATCH 049/639] Add initial state to Flux Switch (#27089) * flux restore state * Add config options * Add tests * Add more tests * just restores state --- homeassistant/components/flux/switch.py | 10 +++++- tests/components/flux/test_switch.py | 48 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 800ccd1938f..7b58ffbe449 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -31,10 +31,12 @@ from homeassistant.const import ( CONF_LIGHTS, CONF_MODE, SERVICE_TURN_ON, + STATE_ON, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify from homeassistant.util.color import ( @@ -169,7 +171,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass.services.async_register(DOMAIN, service_name, async_update) -class FluxSwitch(SwitchDevice): +class FluxSwitch(SwitchDevice, RestoreEntity): """Representation of a Flux switch.""" def __init__( @@ -214,6 +216,12 @@ class FluxSwitch(SwitchDevice): """Return true if switch is on.""" return self.unsub_tracker is not None + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + last_state = await self.async_get_last_state() + if last_state and last_state.state == STATE_ON: + await self.async_turn_on() + async def async_turn_on(self, **kwargs): """Turn on flux.""" if self.is_on: diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index fb35485f5c9..91871666f46 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -10,12 +10,14 @@ from homeassistant.const import ( SERVICE_TURN_ON, SUN_EVENT_SUNRISE, ) +from homeassistant.core import State import homeassistant.util.dt as dt_util from tests.common import ( assert_setup_component, async_fire_time_changed, async_mock_service, + mock_restore_cache, ) from tests.components.light import common as common_light from tests.components.switch import common @@ -35,6 +37,52 @@ async def test_valid_config(hass): }, ) + state = hass.states.get("switch.flux") + assert state + assert state.state == "off" + + +async def test_restore_state_last_on(hass): + """Test restoring state when the last state is on.""" + mock_restore_cache(hass, [State("switch.flux", "on")]) + + assert await async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "flux", + "name": "flux", + "lights": ["light.desk", "light.lamp"], + } + }, + ) + + state = hass.states.get("switch.flux") + assert state + assert state.state == "on" + + +async def test_restore_state_last_off(hass): + """Test restoring state when the last state is off.""" + mock_restore_cache(hass, [State("switch.flux", "off")]) + + assert await async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "flux", + "name": "flux", + "lights": ["light.desk", "light.lamp"], + } + }, + ) + + state = hass.states.get("switch.flux") + assert state + assert state.state == "off" + async def test_valid_config_with_info(hass): """Test configuration.""" From 99859485e21fc652d537e3d559c129dbea064496 Mon Sep 17 00:00:00 2001 From: scheric <38077357+scheric@users.noreply.github.com> Date: Sat, 5 Oct 2019 22:07:01 +0200 Subject: [PATCH 050/639] Repair SolarEdge_local inverter fahrenheit temperature (#27096) * Add Fahrenheit check * Rounding values * add missing bracket * Fix spelling * round fahrenheit to 1 decimal * Change unit on the fly * Use new sensor names * Use TEMP_FAHRENHEIT constant * Pass new sensors fully to SolarEdgeSensor * applying snake_case * applying snake_case lower case * Update sensor.py * applying feedback --- .../components/solaredge_local/sensor.py | 66 ++++++++++++------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index 4fc62e44921..ce51efa07ca 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta import statistics +from copy import deepcopy from requests.exceptions import HTTPError, ConnectTimeout from solaredge_local import SolarEdge @@ -14,6 +15,7 @@ from homeassistant.const import ( POWER_WATT, ENERGY_WATT_HOUR, TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -58,25 +60,25 @@ SENSOR_TYPES = { ], "optimizer_current": [ "optimizercurrent", - "Avrage Optimizer Current", + "Average Optimizer Current", "A", "mdi:solar-panel", ], "optimizer_power": [ "optimizerpower", - "Avrage Optimizer Power", + "Average Optimizer Power", POWER_WATT, "mdi:solar-panel", ], "optimizer_temperature": [ "optimizertemperature", - "Avrage Optimizer Temperature", + "Average Optimizer Temperature", TEMP_CELSIUS, "mdi:solar-panel", ], "optimizer_voltage": [ "optimizervoltage", - "Avrage Optimizer Voltage", + "Average Optimizer Voltage", "V", "mdi:solar-panel", ], @@ -112,13 +114,30 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error("Could not retrieve details from SolarEdge API") return + # Changing inverter temperature unit. + sensors = deepcopy(SENSOR_TYPES) + if status.inverters.primary.temperature.units.farenheit: + sensors["inverter_temperature"] = [ + "invertertemperature", + "Inverter Temperature", + TEMP_FAHRENHEIT, + "mdi:thermometer", + ] + # Create solaredge data service which will retrieve and update the data. data = SolarEdgeData(hass, api) # Create a new sensor for each sensor type. entities = [] - for sensor_key in SENSOR_TYPES: - sensor = SolarEdgeSensor(platform_name, sensor_key, data) + for sensor_info in sensors.values(): + sensor = SolarEdgeSensor( + platform_name, + data, + sensor_info[0], + sensor_info[1], + sensor_info[2], + sensor_info[3], + ) entities.append(sensor) add_entities(entities, True) @@ -127,20 +146,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SolarEdgeSensor(Entity): """Representation of an SolarEdge Monitoring API sensor.""" - def __init__(self, platform_name, sensor_key, data): + def __init__(self, platform_name, data, json_key, name, unit, icon): """Initialize the sensor.""" - self.platform_name = platform_name - self.sensor_key = sensor_key - self.data = data + self._platform_name = platform_name + self._data = data self._state = None - self._json_key = SENSOR_TYPES[self.sensor_key][0] - self._unit_of_measurement = SENSOR_TYPES[self.sensor_key][2] + self._json_key = json_key + self._name = name + self._unit_of_measurement = unit + self._icon = icon @property def name(self): """Return the name.""" - return f"{self.platform_name} ({SENSOR_TYPES[self.sensor_key][1]})" + return f"{self._platform_name} ({self._name})" @property def unit_of_measurement(self): @@ -150,7 +170,7 @@ class SolarEdgeSensor(Entity): @property def icon(self): """Return the sensor icon.""" - return SENSOR_TYPES[self.sensor_key][3] + return self._icon @property def state(self): @@ -159,8 +179,8 @@ class SolarEdgeSensor(Entity): def update(self): """Get the latest data from the sensor and update the state.""" - self.data.update() - self._state = self.data.data[self._json_key] + self._data.update() + self._state = self._data.data[self._json_key] class SolarEdgeData: @@ -220,11 +240,11 @@ class SolarEdgeData: self.data["energyThisMonth"] = round(status.energy.thisMonth, 2) self.data["energyToday"] = round(status.energy.today, 2) self.data["currentPower"] = round(status.powerWatt, 2) - self.data[ - "invertertemperature" - ] = status.inverters.primary.temperature.value + self.data["invertertemperature"] = round( + status.inverters.primary.temperature.value, 2 + ) if maintenance.system.name: - self.data["optimizertemperature"] = statistics.mean(temperature) - self.data["optimizervoltage"] = statistics.mean(voltage) - self.data["optimizercurrent"] = statistics.mean(current) - self.data["optimizerpower"] = power + self.data["optimizertemperature"] = round(statistics.mean(temperature), 2) + self.data["optimizervoltage"] = round(statistics.mean(voltage), 2) + self.data["optimizercurrent"] = round(statistics.mean(current), 2) + self.data["optimizerpower"] = round(power, 2) From 43d14130507904dd578c52a47532a6cb486a6a29 Mon Sep 17 00:00:00 2001 From: Pierre Sicot Date: Sat, 5 Oct 2019 22:28:19 +0200 Subject: [PATCH 051/639] Fix closed status for non horizontal awnings. (#26840) --- homeassistant/components/tahoma/cover.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index a189199bfb2..7448eb27ae0 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -137,14 +137,13 @@ class TahomaCover(TahomaDevice, CoverDevice): if self._closure is not None: if self.tahoma_device.type == HORIZONTAL_AWNING: self._position = self._closure - self._closed = self._position == 0 else: self._position = 100 - self._closure - self._closed = self._position == 100 if self._position <= 5: self._position = 0 if self._position >= 95: self._position = 100 + self._closed = self._position == 0 else: self._position = None if "core:OpenClosedState" in self.tahoma_device.active_states: From d16edb3ef016e8515410a84206e6bbc695a23cd2 Mon Sep 17 00:00:00 2001 From: Matthew Donoughe Date: Sat, 5 Oct 2019 16:30:43 -0400 Subject: [PATCH 052/639] add script shortcut for activating scenes (#27223) * add script shortcut for activating scenes use `- scene: scene.` in a script to activate a scene * Update validation --- homeassistant/helpers/config_validation.py | 3 +++ homeassistant/helpers/script.py | 24 +++++++++++++++++++ tests/helpers/test_script.py | 27 ++++++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 4d5df8785e2..8598b50f140 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -885,6 +885,8 @@ DEVICE_ACTION_BASE_SCHEMA = vol.Schema( DEVICE_ACTION_SCHEMA = DEVICE_ACTION_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA) +_SCRIPT_SCENE_SCHEMA = vol.Schema({vol.Required("scene"): entity_domain("scene")}) + SCRIPT_SCHEMA = vol.All( ensure_list, [ @@ -895,6 +897,7 @@ SCRIPT_SCHEMA = vol.All( EVENT_SCHEMA, CONDITION_SCHEMA, DEVICE_ACTION_SCHEMA, + _SCRIPT_SCENE_SCHEMA, ) ], ) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 05b28102726..1e65c24eaaf 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -9,12 +9,15 @@ from typing import Optional, Sequence, Callable, Dict, List, Set, Tuple, Any import voluptuous as vol import homeassistant.components.device_automation as device_automation +import homeassistant.components.scene as scene from homeassistant.core import HomeAssistant, Context, callback, CALLBACK_TYPE from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_CONDITION, CONF_DEVICE_ID, CONF_DOMAIN, CONF_TIMEOUT, + SERVICE_TURN_ON, ) from homeassistant import exceptions from homeassistant.helpers import ( @@ -46,6 +49,7 @@ CONF_EVENT_DATA_TEMPLATE = "event_data_template" CONF_DELAY = "delay" CONF_WAIT_TEMPLATE = "wait_template" CONF_CONTINUE = "continue_on_timeout" +CONF_SCENE = "scene" ACTION_DELAY = "delay" @@ -54,6 +58,7 @@ ACTION_CHECK_CONDITION = "condition" ACTION_FIRE_EVENT = "event" ACTION_CALL_SERVICE = "call_service" ACTION_DEVICE_AUTOMATION = "device" +ACTION_ACTIVATE_SCENE = "scene" def _determine_action(action): @@ -73,6 +78,9 @@ def _determine_action(action): if CONF_DEVICE_ID in action: return ACTION_DEVICE_AUTOMATION + if CONF_SCENE in action: + return ACTION_ACTIVATE_SCENE + return ACTION_CALL_SERVICE @@ -147,6 +155,7 @@ class Script: ACTION_FIRE_EVENT: self._async_fire_event, ACTION_CALL_SERVICE: self._async_call_service, ACTION_DEVICE_AUTOMATION: self._async_device_automation, + ACTION_ACTIVATE_SCENE: self._async_activate_scene, } @property @@ -362,6 +371,21 @@ class Script: self.hass, action, variables, context ) + async def _async_activate_scene(self, action, variables, context): + """Activate the scene specified in the action. + + This method is a coroutine. + """ + self.last_action = action.get(CONF_ALIAS, "activate scene") + self._log("Executing step %s" % self.last_action) + await self.hass.services.async_call( + scene.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: action[CONF_SCENE]}, + blocking=True, + context=context, + ) + async def _async_fire_event(self, action, variables, context): """Fire an event.""" self.last_action = action.get(CONF_ALIAS, action[CONF_EVENT]) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index ebc56c111ee..4b8be715f37 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -9,7 +9,9 @@ import jinja2 import voluptuous as vol import pytest +import homeassistant.components.scene as scene from homeassistant import exceptions +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON from homeassistant.core import Context, callback # Otherwise can't test just this file (import order issue) @@ -120,6 +122,31 @@ async def test_calling_service(hass): assert calls[0].data.get("hello") == "world" +async def test_activating_scene(hass): + """Test the activation of a scene.""" + calls = [] + context = Context() + + @callback + def record_call(service): + """Add recorded event to set.""" + calls.append(service) + + hass.services.async_register(scene.DOMAIN, SERVICE_TURN_ON, record_call) + + hass.async_add_job( + ft.partial( + script.call_from_config, hass, {"scene": "scene.hello"}, context=context + ) + ) + + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data.get(ATTR_ENTITY_ID) == "scene.hello" + + async def test_calling_service_template(hass): """Test the calling of a service.""" calls = [] From be60b065a348233901113635e5ad3be3cfec0114 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 5 Oct 2019 22:31:01 +0200 Subject: [PATCH 053/639] Bump python-miio version to 0.4.6 (#27231) --- homeassistant/components/xiaomi_miio/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/manifest.json b/homeassistant/components/xiaomi_miio/manifest.json index 4c01cce2d3c..b675e6e6746 100644 --- a/homeassistant/components/xiaomi_miio/manifest.json +++ b/homeassistant/components/xiaomi_miio/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/xiaomi_miio", "requirements": [ "construct==2.9.45", - "python-miio==0.4.5" + "python-miio==0.4.6" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 086482eda7d..c2c2c96a26f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1534,7 +1534,7 @@ python-juicenet==0.0.5 # python-lirc==1.2.3 # homeassistant.components.xiaomi_miio -python-miio==0.4.5 +python-miio==0.4.6 # homeassistant.components.mpd python-mpd2==1.0.0 From 5c01dd483fc6bf2d15848b7f4ebcc99fb45bb705 Mon Sep 17 00:00:00 2001 From: Sebastian Muszynski Date: Sat, 5 Oct 2019 22:31:10 +0200 Subject: [PATCH 054/639] Add Xiaomi Air Humidifier CB1 (zhimi.humidifier.cb1) support (#27232) --- homeassistant/components/xiaomi_miio/fan.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 67dc12565d8..acac60e108a 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -43,7 +43,8 @@ MODEL_AIRPURIFIER_SA2 = "zhimi.airpurifier.sa2" MODEL_AIRPURIFIER_2S = "zhimi.airpurifier.mc1" MODEL_AIRHUMIDIFIER_V1 = "zhimi.humidifier.v1" -MODEL_AIRHUMIDIFIER_CA = "zhimi.humidifier.ca1" +MODEL_AIRHUMIDIFIER_CA1 = "zhimi.humidifier.ca1" +MODEL_AIRHUMIDIFIER_CB1 = "zhimi.humidifier.cb1" MODEL_AIRFRESH_VA2 = "zhimi.airfresh.va2" @@ -68,7 +69,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( MODEL_AIRPURIFIER_SA2, MODEL_AIRPURIFIER_2S, MODEL_AIRHUMIDIFIER_V1, - MODEL_AIRHUMIDIFIER_CA, + MODEL_AIRHUMIDIFIER_CA1, + MODEL_AIRHUMIDIFIER_CB1, MODEL_AIRFRESH_VA2, ] ), @@ -235,7 +237,7 @@ AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER = { ATTR_BUTTON_PRESSED: "button_pressed", } -AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA = { +AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB = { **AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_COMMON, ATTR_MOTOR_SPEED: "motor_speed", ATTR_DEPTH: "depth", @@ -335,7 +337,7 @@ FEATURE_FLAGS_AIRHUMIDIFIER = ( | FEATURE_SET_TARGET_HUMIDITY ) -FEATURE_FLAGS_AIRHUMIDIFIER_CA = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY +FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB = FEATURE_FLAGS_AIRHUMIDIFIER | FEATURE_SET_DRY FEATURE_FLAGS_AIRFRESH = ( FEATURE_SET_BUZZER @@ -880,9 +882,9 @@ class XiaomiAirHumidifier(XiaomiGenericDevice): super().__init__(name, device, model, unique_id) - if self._model == MODEL_AIRHUMIDIFIER_CA: - self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA - self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA + if self._model in [MODEL_AIRHUMIDIFIER_CA1, MODEL_AIRHUMIDIFIER_CB1]: + self._device_features = FEATURE_FLAGS_AIRHUMIDIFIER_CA_AND_CB + self._available_attributes = AVAILABLE_ATTRIBUTES_AIRHUMIDIFIER_CA_AND_CB self._speed_list = [ mode.name for mode in OperationMode if mode is not OperationMode.Strong ] From bd92532ebbea07cac5ea574d68e13b7a57a3a74e Mon Sep 17 00:00:00 2001 From: Jens Date: Sun, 6 Oct 2019 01:12:50 +0200 Subject: [PATCH 055/639] Add io:SomfyBasicContactIOSystemSensor to TaHoma component (#27234) --- homeassistant/components/tahoma/__init__.py | 1 + homeassistant/components/tahoma/sensor.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/tahoma/__init__.py b/homeassistant/components/tahoma/__init__.py index 4400df6db96..6bcc783400c 100644 --- a/homeassistant/components/tahoma/__init__.py +++ b/homeassistant/components/tahoma/__init__.py @@ -42,6 +42,7 @@ TAHOMA_TYPES = { "io:RollerShutterUnoIOComponent": "cover", "io:RollerShutterVeluxIOComponent": "cover", "io:RollerShutterWithLowSpeedManagementIOComponent": "cover", + "io:SomfyBasicContactIOSystemSensor": "sensor", "io:SomfyContactIOSystemSensor": "sensor", "io:VerticalExteriorAwningIOComponent": "cover", "io:WindowOpenerVeluxIOComponent": "cover", diff --git a/homeassistant/components/tahoma/sensor.py b/homeassistant/components/tahoma/sensor.py index 0ed3879cc7a..5279b160d9c 100644 --- a/homeassistant/components/tahoma/sensor.py +++ b/homeassistant/components/tahoma/sensor.py @@ -44,6 +44,8 @@ class TahomaSensor(TahomaDevice, Entity): return None if self.tahoma_device.type == "io:SomfyContactIOSystemSensor": return None + if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": + return None if self.tahoma_device.type == "io:LightIOSystemSensor": return "lx" if self.tahoma_device.type == "Humidity Sensor": @@ -66,6 +68,11 @@ class TahomaSensor(TahomaDevice, Entity): self._available = bool( self.tahoma_device.active_states.get("core:StatusState") == "available" ) + if self.tahoma_device.type == "io:SomfyBasicContactIOSystemSensor": + self.current_value = self.tahoma_device.active_states["core:ContactState"] + self._available = bool( + self.tahoma_device.active_states.get("core:StatusState") == "available" + ) if self.tahoma_device.type == "rtds:RTDSContactSensor": self.current_value = self.tahoma_device.active_states["core:ContactState"] self._available = True From 2c6a869bc61d922b77cc4a7588d079e8a2836013 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 6 Oct 2019 00:32:15 +0000 Subject: [PATCH 056/639] [ci skip] Translation update --- .../components/airly/.translations/ca.json | 22 ++++++++++++++++++ .../components/airly/.translations/it.json | 22 ++++++++++++++++++ .../components/airly/.translations/ru.json | 22 ++++++++++++++++++ .../ambient_station/.translations/ru.json | 2 +- .../components/axis/.translations/ru.json | 4 ++-- .../cert_expiry/.translations/ru.json | 4 ++-- .../components/daikin/.translations/ru.json | 2 +- .../emulated_roku/.translations/ru.json | 2 +- .../components/esphome/.translations/ru.json | 2 +- .../geonetnz_quakes/.translations/ru.json | 2 +- .../components/hangouts/.translations/ru.json | 2 +- .../homematicip_cloud/.translations/ru.json | 2 +- .../components/hue/.translations/ru.json | 4 ++-- .../components/ipma/.translations/ru.json | 2 +- .../components/iqvia/.translations/ru.json | 2 +- .../components/life360/.translations/ru.json | 4 ++-- .../components/linky/.translations/ru.json | 4 ++-- .../luftdaten/.translations/ru.json | 2 +- .../components/met/.translations/ru.json | 2 +- .../components/notion/.translations/ru.json | 2 +- .../opentherm_gw/.translations/ca.json | 23 +++++++++++++++++++ .../opentherm_gw/.translations/it.json | 23 +++++++++++++++++++ .../opentherm_gw/.translations/nl.json | 11 +-------- .../opentherm_gw/.translations/no.json | 14 +++++++++++ .../opentherm_gw/.translations/ru.json | 23 +++++++++++++++++++ .../components/openuv/.translations/ru.json | 2 +- .../components/plex/.translations/ru.json | 6 ++--- .../rainmachine/.translations/ru.json | 2 +- .../simplisafe/.translations/ru.json | 2 +- .../components/smhi/.translations/ru.json | 2 +- .../solaredge/.translations/ru.json | 4 ++-- .../tellduslive/.translations/ru.json | 2 +- .../components/unifi/.translations/ru.json | 2 +- .../components/upnp/.translations/ru.json | 2 +- .../components/wwlln/.translations/ru.json | 2 +- .../components/zone/.translations/ru.json | 2 +- .../components/zwave/.translations/ru.json | 2 +- 37 files changed, 187 insertions(+), 47 deletions(-) create mode 100644 homeassistant/components/airly/.translations/ca.json create mode 100644 homeassistant/components/airly/.translations/it.json create mode 100644 homeassistant/components/airly/.translations/ru.json create mode 100644 homeassistant/components/opentherm_gw/.translations/ca.json create mode 100644 homeassistant/components/opentherm_gw/.translations/it.json create mode 100644 homeassistant/components/opentherm_gw/.translations/no.json create mode 100644 homeassistant/components/opentherm_gw/.translations/ru.json diff --git a/homeassistant/components/airly/.translations/ca.json b/homeassistant/components/airly/.translations/ca.json new file mode 100644 index 00000000000..bf50b4f23e5 --- /dev/null +++ b/homeassistant/components/airly/.translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clau API no \u00e9s correcta.", + "name_exists": "El nom ja existeix.", + "wrong_location": "No hi ha estacions de mesura Airly en aquesta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clau API d'Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nom de la integraci\u00f3" + }, + "description": "Configura una integraci\u00f3 de qualitat d\u2019aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/it.json b/homeassistant/components/airly/.translations/it.json new file mode 100644 index 00000000000..e50f618575b --- /dev/null +++ b/homeassistant/components/airly/.translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La chiave API non \u00e8 corretta.", + "name_exists": "Il nome \u00e8 gi\u00e0 esistente", + "wrong_location": "Nessuna stazione di misurazione Airly in quest'area." + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API Airly", + "latitude": "Latitudine", + "longitude": "Logitudine", + "name": "Nome dell'integrazione" + }, + "description": "Configurazione dell'integrazione della qualit\u00e0 dell'aria Airly. Per generare la chiave API andare su https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/ru.json b/homeassistant/components/airly/.translations/ru.json new file mode 100644 index 00000000000..36080c9f372 --- /dev/null +++ b/homeassistant/components/airly/.translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", + "wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 Airly. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register.", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index d1264010b75..2d7964f18eb 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b" }, diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 67d720aa85f..951263d53f9 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis" }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json index 6a795dee13e..d962c793121 100644 --- a/homeassistant/components/cert_expiry/.translations/ru.json +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430" + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." }, "error": { "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430", "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443", - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430", + "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442" }, "step": { diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index ce1f1ab3caa..98ab98e6b17 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, diff --git a/homeassistant/components/emulated_roku/.translations/ru.json b/homeassistant/components/emulated_roku/.translations/ru.json index c7b85c19592..32bf473ac38 100644 --- a/homeassistant/components/emulated_roku/.translations/ru.json +++ b/homeassistant/components/emulated_roku/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index 1405112c070..62d24662ab6 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430" + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json index 7d6583bc1d5..d6763d17e2d 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ru.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 52b8798c0f4..6942f683fa6 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" }, "error": { diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 82ecd4a3250..5155a42c4c3 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP", "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index be5d2b7159d..79a46e1861b 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json index a260efa5bd9..a302572ed12 100644 --- a/homeassistant/components/ipma/.translations/ru.json +++ b/homeassistant/components/ipma/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "user": { diff --git a/homeassistant/components/iqvia/.translations/ru.json b/homeassistant/components/iqvia/.translations/ru.json index 06a5b7e69dd..0c3afc88c94 100644 --- a/homeassistant/components/iqvia/.translations/ru.json +++ b/homeassistant/components/iqvia/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d", + "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" }, "step": { diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index 1e962142373..d033da4bae7 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "create_entry": { "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." @@ -11,7 +11,7 @@ "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d", "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360", - "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "step": { "user": { diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index 498b5b2f12f..b569cce9239 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430" + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443", "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", - "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430", + "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c" }, "step": { diff --git a/homeassistant/components/luftdaten/.translations/ru.json b/homeassistant/components/luftdaten/.translations/ru.json index d37aa3567d1..7ae83b550e3 100644 --- a/homeassistant/components/luftdaten/.translations/ru.json +++ b/homeassistant/components/luftdaten/.translations/ru.json @@ -3,7 +3,7 @@ "error": { "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten", "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d", - "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { "user": { diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json index d92d28d9484..559382cf209 100644 --- a/homeassistant/components/met/.translations/ru.json +++ b/homeassistant/components/met/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "name_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index c7e89c368c1..7345cf46295 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c", "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e" }, diff --git a/homeassistant/components/opentherm_gw/.translations/ca.json b/homeassistant/components/opentherm_gw/.translations/ca.json new file mode 100644 index 00000000000..0224d663a83 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ca.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Passarel\u00b7la ja configurada", + "id_exists": "L'identificador de passarel\u00b7la ja existeix", + "serial_error": "S'ha produ\u00eft un error en connectar-se al dispositiu", + "timeout": "S'ha acabat el temps d'espera en l'intent de connexi\u00f3" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "floor_temperature": "Temperatura del pis", + "id": "ID", + "name": "Nom", + "precision": "Precisi\u00f3 de la temperatura" + }, + "title": "Passarel\u00b7la d'OpenTherm" + } + }, + "title": "Passarel\u00b7la d'OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/it.json b/homeassistant/components/opentherm_gw/.translations/it.json new file mode 100644 index 00000000000..9c62686e190 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/it.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway gi\u00e0 configurato", + "id_exists": "ID del gateway esiste gi\u00e0", + "serial_error": "Errore durante la connessione al dispositivo", + "timeout": "Tentativo di connessione scaduto" + }, + "step": { + "init": { + "data": { + "device": "Percorso o URL", + "floor_temperature": "Temperatura climatica del pavimento", + "id": "ID", + "name": "Nome", + "precision": "Precisione della temperatura climatica" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "Gateway OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json index ef3daafe4fe..4fec1baba7b 100644 --- a/homeassistant/components/opentherm_gw/.translations/nl.json +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -1,19 +1,10 @@ { "config": { - "error": { - "already_configured": "Gateway is reeds geconfigureerd", - "id_exists": "Gateway id bestaat reeds", - "serial_error": "Kan niet verbinden met de Gateway", - "timeout": "Time-out van de verbinding" - }, "step": { "init": { "data": { "device": "Pad of URL", - "floor_temperature": "Thermostaat temperaturen naar beneden afronden", - "id": "ID", - "name": "Naam", - "precision": "Thermostaat temperatuur precisie" + "id": "ID" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json new file mode 100644 index 00000000000..a1df80f3b12 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "init": { + "data": { + "id": "ID", + "name": "Navn" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json new file mode 100644 index 00000000000..718322ec171 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ru.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", + "serial_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "init": { + "data": { + "device": "\u041f\u0443\u0442\u044c \u0438\u043b\u0438 URL-\u0430\u0434\u0440\u0435\u0441", + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", + "id": "ID", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b" + }, + "title": "OpenTherm" + } + }, + "title": "OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/.translations/ru.json b/homeassistant/components/openuv/.translations/ru.json index 9683c5d7c36..58d57b28056 100644 --- a/homeassistant/components/openuv/.translations/ru.json +++ b/homeassistant/components/openuv/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b", + "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b.", "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API" }, "step": { diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 2b63840d001..48cacacddfe 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -1,9 +1,9 @@ { "config": { "abort": { - "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b", - "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d", - "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", + "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", + "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", + "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430", "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index 6eec3ef0eba..6248890389d 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" }, "step": { diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index f685297890e..e82172f92f8 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" }, "step": { diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json index 88ea988ff1b..03b17b3ba8b 100644 --- a/homeassistant/components/smhi/.translations/ru.json +++ b/homeassistant/components/smhi/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442", + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438" }, "step": { diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json index fe36e4296fe..d8622cdd2c1 100644 --- a/homeassistant/components/solaredge/.translations/ru.json +++ b/homeassistant/components/solaredge/.translations/ru.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "error": { - "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d" + "site_exists": "\u042d\u0442\u043e\u0442 site_id \u0443\u0436\u0435 \u0441\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { "user": { diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index afaaf4edbf5..9d3c97ad902 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index 76802a96367..2a3a6207cf5 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c" }, "error": { diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 8d41ec1d5de..3351f0d5d8a 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP", "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP / IGD \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", diff --git a/homeassistant/components/wwlln/.translations/ru.json b/homeassistant/components/wwlln/.translations/ru.json index ad553def6c3..3bdaf85498b 100644 --- a/homeassistant/components/wwlln/.translations/ru.json +++ b/homeassistant/components/wwlln/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e" + "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, "step": { "user": { diff --git a/homeassistant/components/zone/.translations/ru.json b/homeassistant/components/zone/.translations/ru.json index dc408035d0f..6a017e9e1c3 100644 --- a/homeassistant/components/zone/.translations/ru.json +++ b/homeassistant/components/zone/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "\u0418\u043c\u044f \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442" + "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "step": { "init": { diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json index a64b4db185d..ed2e20f3527 100644 --- a/homeassistant/components/zwave/.translations/ru.json +++ b/homeassistant/components/zwave/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave" }, "error": { From 476f24e451ee1797f7c67aed080da5a8178eca97 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sun, 6 Oct 2019 11:54:26 +0200 Subject: [PATCH 057/639] Add basic test support to Homematic IP Cloud (#27228) * Add basic test support to Homematic IP Cloud * move test data address comments --- .../components/homematicip_cloud/sensor.py | 1 + .../components/homematicip_cloud/conftest.py | 75 + tests/components/homematicip_cloud/helper.py | 128 + .../homematicip_cloud/test_binary_sensors.py | 41 + .../homematicip_cloud/test_config_flow.py | 3 +- .../components/homematicip_cloud/test_hap.py | 10 +- .../components/homematicip_cloud/test_init.py | 6 +- .../homematicip_cloud/test_lights.py | 77 + tests/fixtures/homematicip_cloud.json | 5341 +++++++++++++++++ 9 files changed, 5672 insertions(+), 10 deletions(-) create mode 100644 tests/components/homematicip_cloud/conftest.py create mode 100644 tests/components/homematicip_cloud/helper.py create mode 100644 tests/components/homematicip_cloud/test_binary_sensors.py create mode 100644 tests/components/homematicip_cloud/test_lights.py create mode 100644 tests/fixtures/homematicip_cloud.json diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 770921288b9..30e910cc33a 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -115,6 +115,7 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): def __init__(self, home: AsyncHome) -> None: """Initialize access point device.""" + home.modelType = "HmIP-HAP" super().__init__(home, home) @property diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py new file mode 100644 index 00000000000..c301c73b4d0 --- /dev/null +++ b/tests/components/homematicip_cloud/conftest.py @@ -0,0 +1,75 @@ +"""Initializer helpers for HomematicIP fake server.""" +from unittest.mock import MagicMock, patch + +from homematicip.aio.connection import AsyncConnection +import pytest + +from homeassistant import config_entries +from homeassistant.components.homematicip_cloud import ( + DOMAIN as HMIPC_DOMAIN, + const as hmipc, + hap as hmip_hap, +) +from homeassistant.core import HomeAssistant + +from .helper import AUTH_TOKEN, HAPID, HomeTemplate + +from tests.common import MockConfigEntry, mock_coro + + +@pytest.fixture(name="mock_connection") +def mock_connection_fixture(): + """Return a mockked connection.""" + connection = MagicMock(spec=AsyncConnection) + + def _rest_call_side_effect(path, body=None): + return path, body + + connection._restCall.side_effect = _rest_call_side_effect # pylint: disable=W0212 + connection.api_call.return_value = mock_coro(True) + + return connection + + +@pytest.fixture(name="default_mock_home") +def default_mock_home_fixture(mock_connection): + """Create a fake homematic async home.""" + return HomeTemplate(connection=mock_connection).init_home().get_async_home_mock() + + +@pytest.fixture(name="hmip_config_entry") +def hmip_config_entry_fixture(): + """Create a fake config entriy for homematic ip cloud.""" + entry_data = { + hmipc.HMIPC_HAPID: HAPID, + hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, + hmipc.HMIPC_NAME: "", + } + config_entry = MockConfigEntry( + version=1, + domain=HMIPC_DOMAIN, + title=HAPID, + data=entry_data, + source="import", + connection_class=config_entries.CONN_CLASS_CLOUD_PUSH, + system_options={"disable_new_entities": False}, + ) + + return config_entry + + +@pytest.fixture(name="default_mock_hap") +async def default_mock_hap_fixture( + hass: HomeAssistant, default_mock_home, hmip_config_entry +): + """Create a fake homematic access point.""" + hass.config.components.add(HMIPC_DOMAIN) + hap = hmip_hap.HomematicipHAP(hass, hmip_config_entry) + with patch.object(hap, "get_hap", return_value=mock_coro(default_mock_home)): + assert await hap.async_setup() is True + + hass.data[HMIPC_DOMAIN] = {HAPID: hap} + + await hass.async_block_till_done() + + return hap diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py new file mode 100644 index 00000000000..79a5bc0b201 --- /dev/null +++ b/tests/components/homematicip_cloud/helper.py @@ -0,0 +1,128 @@ +"""Helper for HomematicIP Cloud Tests.""" +import json +from unittest.mock import Mock + +from homematicip.aio.class_maps import ( + TYPE_CLASS_MAP, + TYPE_GROUP_MAP, + TYPE_SECURITY_EVENT_MAP, +) +from homematicip.aio.home import AsyncHome +from homematicip.home import Home + +from tests.common import load_fixture + +HAPID = "Mock_HAP" +AUTH_TOKEN = "1234" +HOME_JSON = "homematicip_cloud.json" + + +def get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model +): + """Get and test basic device.""" + ha_entity = hass.states.get(entity_id) + assert ha_entity is not None + assert ha_entity.attributes["model_type"] == device_model + assert ha_entity.name == entity_name + + hmip_device = default_mock_hap.home.template.search_mock_device_by_id( + ha_entity.attributes["id"] + ) + assert hmip_device is not None + return ha_entity, hmip_device + + +async def async_manipulate_test_data( + hass, hmip_device, attribute, new_value, channel=1 +): + """Set new value on hmip device.""" + if channel == 1: + setattr(hmip_device, attribute, new_value) + functional_channel = hmip_device.functionalChannels[channel] + setattr(functional_channel, attribute, new_value) + + hmip_device.fire_update_event() + await hass.async_block_till_done() + + +class HomeTemplate(Home): + """ + Home template as builder for home mock. + + It is based on the upstream libs home class to generate hmip devices + and groups based on the given homematicip_cloud.json. + + All further testing activities should be done by using the AsyncHome mock, + that is generated by get_async_home_mock(self). + + The class also generated mocks of devices and groups for further testing. + """ + + _typeClassMap = TYPE_CLASS_MAP + _typeGroupMap = TYPE_GROUP_MAP + _typeSecurityEventMap = TYPE_SECURITY_EVENT_MAP + + def __init__(self, connection=None): + """Init template with connection.""" + super().__init__(connection=connection) + self.mock_devices = [] + self.mock_groups = [] + + def init_home(self, json_path=HOME_JSON): + """Init template with json.""" + json_state = json.loads(load_fixture(HOME_JSON), encoding="UTF-8") + self.update_home(json_state=json_state, clearConfig=True) + self._generate_mocks() + return self + + def _generate_mocks(self): + """Generate mocks for groups and devices.""" + for device in self.devices: + self.mock_devices.append(_get_mock(device)) + for group in self.groups: + self.mock_groups.append(_get_mock(group)) + + def search_mock_device_by_id(self, device_id): + """Search a device by given id.""" + for device in self.mock_devices: + if device.id == device_id: + return device + return None + + def search_mock_group_by_id(self, group_id): + """Search a group by given id.""" + for group in self.mock_groups: + if group.id == group_id: + return group + return None + + def get_async_home_mock(self): + """ + Create Mock for Async_Home. based on template to be used for testing. + + It adds collections of mocked devices and groups to the home objects, + and sets reuired attributes. + """ + mock_home = Mock( + check_connection=self._connection, + id=HAPID, + connected=True, + dutyCycle=self.dutyCycle, + devices=self.mock_devices, + groups=self.mock_groups, + weather=self.weather, + location=self.location, + label="home label", + template=self, + spec=AsyncHome, + ) + mock_home.name = "" + return mock_home + + +def _get_mock(instance): + """Create a mock and copy instance attributes over mock.""" + mock = Mock(spec=instance, wraps=instance) + mock.__dict__.update(instance.__dict__) + return mock diff --git a/tests/components/homematicip_cloud/test_binary_sensors.py b/tests/components/homematicip_cloud/test_binary_sensors.py new file mode 100644 index 00000000000..4471c5dd7f3 --- /dev/null +++ b/tests/components/homematicip_cloud/test_binary_sensors.py @@ -0,0 +1,41 @@ +"""Tests for HomematicIP Cloud lights.""" +import logging + +from tests.components.homematicip_cloud.helper import ( + async_manipulate_test_data, + get_and_check_entity_basics, +) + +_LOGGER = logging.getLogger(__name__) + + +async def test_hmip_sam(hass, default_mock_hap): + """Test HomematicipLight.""" + entity_id = "binary_sensor.garagentor" + entity_name = "Garagentor" + device_model = "HmIP-SAM" + + ha_entity, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_entity.state == "on" + assert ha_entity.attributes["acceleration_sensor_mode"] == "FLAT_DECT" + assert ha_entity.attributes["acceleration_sensor_neutral_position"] == "VERTICAL" + assert ha_entity.attributes["acceleration_sensor_sensitivity"] == "SENSOR_RANGE_4G" + assert ha_entity.attributes["acceleration_sensor_trigger_angle"] == 45 + service_call_counter = len(hmip_device.mock_calls) + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", False + ) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "off" + assert len(hmip_device.mock_calls) == service_call_counter + 1 + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", True + ) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "on" + assert len(hmip_device.mock_calls) == service_call_counter + 2 diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index c1bad855701..54cb309755d 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,8 +1,7 @@ """Tests for HomematicIP Cloud config flow.""" from unittest.mock import patch -from homeassistant.components.homematicip_cloud import hap as hmipc -from homeassistant.components.homematicip_cloud import config_flow, const +from homeassistant.components.homematicip_cloud import config_flow, const, hap as hmipc from tests.common import MockConfigEntry, mock_coro diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 34afd19310f..cd8ead40c43 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -3,9 +3,9 @@ from unittest.mock import Mock, patch import pytest +from homeassistant.components.homematicip_cloud import const, errors, hap as hmipc from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.components.homematicip_cloud import hap as hmipc -from homeassistant.components.homematicip_cloud import const, errors + from tests.common import mock_coro, mock_coro_func @@ -94,8 +94,8 @@ async def test_hap_setup_connection_error(): ), pytest.raises(ConfigEntryNotReady): await hap.async_setup() - assert len(hass.async_add_job.mock_calls) == 0 - assert len(hass.config_entries.flow.async_init.mock_calls) == 0 + assert not hass.async_add_job.mock_calls + assert not hass.config_entries.flow.async_init.mock_calls async def test_hap_reset_unloads_entry_if_setup(): @@ -114,7 +114,7 @@ async def test_hap_reset_unloads_entry_if_setup(): assert await hap.async_setup() is True assert hap.home is home - assert len(hass.services.async_register.mock_calls) == 0 + assert not hass.services.async_register.mock_calls assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8 hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index d77d4a7e5b2..894db2e691b 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -2,10 +2,10 @@ from unittest.mock import patch -from homeassistant.setup import async_setup_component from homeassistant.components import homematicip_cloud as hmipc +from homeassistant.setup import async_setup_component -from tests.common import mock_coro, MockConfigEntry +from tests.common import MockConfigEntry, mock_coro async def test_config_with_accesspoint_passed_to_config_entry(hass): @@ -53,7 +53,7 @@ async def test_config_already_registered_not_passed_to_config_entry(hass): ) # No flow started - assert len(mock_config_entries.flow.mock_calls) == 0 + assert not mock_config_entries.flow.mock_calls async def test_setup_entry_successful(hass): diff --git a/tests/components/homematicip_cloud/test_lights.py b/tests/components/homematicip_cloud/test_lights.py new file mode 100644 index 00000000000..dcf5f76d0a0 --- /dev/null +++ b/tests/components/homematicip_cloud/test_lights.py @@ -0,0 +1,77 @@ +"""Tests for HomematicIP Cloud lights.""" +import logging + +from tests.components.homematicip_cloud.helper import ( + async_manipulate_test_data, + get_and_check_entity_basics, +) + +_LOGGER = logging.getLogger(__name__) + + +async def test_hmip_light(hass, default_mock_hap): + """Test HomematicipLight.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_entity, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_entity.state == "on" + + service_call_counter = len(hmip_device.mock_calls) + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "off" + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "on" + + +# HomematicipLightMeasuring +# HomematicipDimmer + + +async def test_hmip_notification_light(hass, default_mock_hap): + """Test HomematicipNotificationLight.""" + entity_id = "light.treppe_top_notification" + entity_name = "Treppe Top Notification" + device_model = "HmIP-BSL" + + ha_entity, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_entity.state == "off" + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 100, 2) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "on" + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, 2) + ha_entity = hass.states.get(entity_id) + assert ha_entity.state == "off" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json new file mode 100644 index 00000000000..b96bff8fac9 --- /dev/null +++ b/tests/fixtures/homematicip_cloud.json @@ -0,0 +1,5341 @@ +{ + "clients": { + "00000000-0000-0000-0000-000000000000": { + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000000", + "label": "TEST-Client", + "clientType": "APP" + } + }, + "devices": { + "3014F7110000000000000031": { + "availableFirmwareVersion": "1.2.1", + "firmwareVersion": "1.2.1", + "firmwareVersionInteger": 66049, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000031", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -88, + "rssiPeerValue": null, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "accelerationSensorEventFilterPeriod": 3.0, + "accelerationSensorMode": "FLAT_DECT", + "accelerationSensorNeutralPosition": "VERTICAL", + "accelerationSensorSensitivity": "SENSOR_RANGE_4G", + "accelerationSensorTriggerAngle": 45, + "accelerationSensorTriggered": true, + "deviceId": "3014F7110000000000000031", + "functionalChannelType": "ACCELERATION_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "notificationSoundTypeHighToLow": "SOUND_LONG", + "notificationSoundTypeLowToHigh": "SOUND_LONG" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000031", + "label": "Garagentor", + "lastStatusUpdate": 1567850423788, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 315, + "modelType": "HmIP-SAM", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000031", + "type": "ACCELERATION_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000052": { + "availableFirmwareVersion": "1.0.5", + "firmwareVersion": "1.0.5", + "firmwareVersionInteger": 65541, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000052", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": null, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F7110000000000000052", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 8, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000052", + "label": "Alarm-Melder", + "lastStatusUpdate": 1564733931898, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 336, + "modelType": "HmIP-MOD-RC8", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000052", + "type": "REMOTE_CONTROL_8_MODULE", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000FAL24C10": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "coolingEmergencyValue": 0.0, + "deviceId": "3014F71100000000FAL24C10", + "dutyCycle": false, + "frostProtectionTemperature": 8.0, + "functionalChannelType": "DEVICE_GLOBAL_PUMP_CONTROL", + "globalPumpControl": true, + "groupIndex": 0, + "groups": [ + ], + "heatingEmergencyValue": 0.25, + "heatingLoadType": "LOAD_BALANCING", + "heatingValveType": "NORMALLY_CLOSE", + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": -74, + "unreach": false, + "valveProtectionDuration": 5, + "valveProtectionSwitchingInterval": 14 + }, + "1": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_LOCAL_PUMP_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "pumpFollowUpTime": 2, + "pumpLeadTime": 2, + "pumpProtectionDuration": 1, + "pumpProtectionSwitchingInterval": 14 + }, + "10": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 10, + "groups": [ + ], + "index": 10, + "label": "" + }, + "11": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "HEAT_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + ], + "index": 11, + "label": "" + }, + "12": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "DEHUMIDIFIER_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + ], + "index": 12, + "label": "" + }, + "2": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 4, + "groups": [ + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 5, + "groups": [ + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 6, + "groups": [ + ], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 7, + "groups": [ + ], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 8, + "groups": [ + ], + "index": 8, + "label": "" + }, + "9": { + "deviceId": "3014F71100000000FAL24C10", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 9, + "groups": [ + ], + "index": 9, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000FAL24C10", + "label": "Fu\u00dfbodenheizungsaktor", + "lastStatusUpdate": 1558461135830, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 280, + "modelType": "HmIP-FAL24-C10", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000FAL24C10", + "type": "FLOOR_TERMINAL_BLOCK_10", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000000BBL24": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000000BBL24", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000034" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -64, + "rssiPeerValue": -76, + "unreach": false + }, + "1": { + "blindModeActive": true, + "bottomToTopReferenceTime": 54.88, + "changeOverDelay": 0.5, + "delayCompensationValue": 12.7, + "deviceId": "3014F71100000000000BBL24", + "endpositionAutoDetectionEnabled": true, + "functionalChannelType": "BLIND_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 0.885, + "slatsLevel": 1.0, + "slatsReferenceTime": 1.6, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": true, + "supportingSelfCalibration": true, + "topToBottomReferenceTime": 53.68, + "userDesiredProfileMode": "MANUAL" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000000BBL24", + "label": "Jalousie Schiebet\u00fcr", + "lastStatusUpdate": 1558464454532, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 332, + "modelType": "HmIP-BBL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000000BBL24", + "type": "BRAND_BLIND", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000BCBB11": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.10.10", + "firmwareVersionInteger": 68106, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000BCBB11", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -53, + "rssiPeerValue": -56, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000BCBB11", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F7110000000000BCBB11", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000038" + ], + "index": 2, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000BCBB11", + "label": "Jalousien - 1 KiZi, 2 SchlaZi", + "lastStatusUpdate": 1555621612744, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 357, + "modelType": "HmIP-PCBS2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000BCBB11", + "type": "PRINTED_CIRCUIT_BOARD_SWITCH_2", + "updateState": "UP_TO_DATE" + }, + "3014F711ABCD0ABCD000002": { + "availableFirmwareVersion": "1.6.4", + "firmwareVersion": "1.6.4", + "firmwareVersionInteger": 67076, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711ABCD0ABCD000002", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000027" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -79, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "on": true, + "profileMode": null, + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 2, + "label": "", + "on": false, + "profileMode": null, + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "GENERIC_INPUT_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "GENERIC_INPUT_CHANNEL", + "groupIndex": 4, + "groups": [], + "index": 4, + "label": "" + }, + "5": { + "analogOutputLevel": 12.5, + "deviceId": "3014F711ABCD0ABCD000002", + "functionalChannelType": "ANALOG_OUTPUT_CHANNEL", + "groupIndex": 0, + "groups": [], + "index": 5, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711ABCD0ABCD000002", + "label": "Multi IO Box", + "lastStatusUpdate": 1552508702220, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 283, + "modelType": "HmIP-MIOB", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711ABCD0ABCD000002", + "type": "MULTI_IO_BOX", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000ABCDEF10": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000ABCDEF10", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -47, + "rssiPeerValue": -50, + "sabotage": null, + "unreach": false + }, + "1": { + "deviceId": "3014F71100000000ABCDEF10", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000013" + ], + "index": 1, + "label": "", + "setPointTemperature": 21.0, + "temperatureOffset": 0.0, + "valveActualTemperature": 21.6, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000ABCDEF10", + "label": "Wohnzimmer 3", + "lastStatusUpdate": 1550912664486, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 325, + "modelType": "HmIP-eTRV-C", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000ABCDEF10", + "type": "HEATING_THERMOSTAT_COMPACT", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000000TEST1": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.8", + "firmwareVersionInteger": 67592, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000000TEST1", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -51, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F71100000000000TEST1", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F71100000000000TEST1", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000000TEST1", + "label": "Remote", + "lastStatusUpdate": 1550512733995, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 358, + "modelType": "HmIP-BRC2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000000TEST1", + "type": "BRAND_PUSH_BUTTON", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000064": { + "availableFirmwareVersion": "1.0.6", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000064", + "deviceOverheated": true, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000032", + "00000000-0000-0000-0000-000000000013" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -42, + "rssiPeerValue": null, + "sabotage": false, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "alarmContactType": "WINDOW_DOOR_CONTACT", + "contactType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000000064", + "eventDelay": 0, + "functionalChannelType": "CONTACT_INTERFACE_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000033", + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000013" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000064", + "label": "Schlie\u00dfer Magnet", + "lastStatusUpdate": 1524515854304, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 375, + "modelType": "HmIP-SCI", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000064", + "type": "SHUTTER_CONTACT_INTERFACE", + "updateState": "UP_TO_DATE" + }, + "3014F711BADCAFE000000001": { + "availableFirmwareVersion": "1.2.0", + "firmwareVersion": "1.2.0", + "firmwareVersionInteger": 66048, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BADCAFE000000001", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": -78, + "unreach": false + }, + "1": { + "blindModeActive": true, + "bottomToTopReferenceTime": 41.0, + "changeOverDelay": 0.5, + "delayCompensationValue": 1.0, + "deviceId": "3014F711BADCAFE000000001", + "endpositionAutoDetectionEnabled": false, + "functionalChannelType": "BLIND_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "", + "previousShutterLevel": null, + "previousSlatsLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 1.0, + "slatsLevel": 1.0, + "slatsReferenceTime": 2.0, + "supportingDelayCompensation": false, + "supportingEndpositionAutoDetection": false, + "supportingSelfCalibration": false, + "topToBottomReferenceTime": 41.0, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BADCAFE000000001", + "label": "Sofa links", + "lastStatusUpdate": 1548616026922, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 333, + "modelType": "HmIP-FBL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711BADCAFE000000001", + "type": "FULL_FLUSH_BLIND", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000055": { + "availableFirmwareVersion": "1.2.4", + "firmwareVersion": "1.2.4", + "firmwareVersionInteger": 66052, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000055", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000034" + ], + "index": 0, + "label": "", + "lowBat": null, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": -77, + "unreach": false + }, + "1": { + "actualTemperature": 21.0, + "deviceId": "3014F7110000000000000055", + "display": "SETPOINT", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000035" + ], + "humidity": 40, + "index": 1, + "label": "", + "vaporAmount": 6.177718198711658, + "valveActualTemperature": 20.0, + "setPointTemperature": 21.5, + "temperatureOffset": 0.0 + }, + "2": { + "deviceId": "3014F7110000000000000055", + "frostProtectionTemperature": 8.0, + "functionalChannelType": "INTERNAL_SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000035" + ], + "heatingValveType": "NORMALLY_CLOSE", + "index": 2, + "internalSwitchOutputEnabled": true, + "label": "", + "valveProtectionDuration": 5, + "valveProtectionSwitchingInterval": 14 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000055", + "label": "BWTH 1", + "lastStatusUpdate": 1547283716818, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 305, + "modelType": "HmIP-BWTH", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000055", + "type": "BRAND_WALL_MOUNTED_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F711ABCDEF0000000014": { + "availableFirmwareVersion": "1.4.2", + "firmwareVersion": "1.4.2", + "firmwareVersionInteger": 66562, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711ABCDEF0000000014", + "dutyCycle": null, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000033" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": null, + "rssiPeerValue": null, + "unreach": null + }, + "1": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711ABCDEF0000000014", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 4, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711ABCDEF0000000014", + "label": "FFB 1", + "lastStatusUpdate": 0, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 266, + "modelType": "HmIP-KRC4", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711ABCDEF0000000014", + "type": "KEY_REMOTE_CONTROL_4", + "updateState": "UP_TO_DATE" + }, + "3014F711BSL0000000000050": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.2", + "firmwareVersionInteger": 65538, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BSL0000000000050", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -67, + "rssiPeerValue": -70, + "unreach": false + }, + "1": { + "deviceId": "3014F711BSL0000000000050", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F711BSL0000000000050", + "dimLevel": 0.0, + "functionalChannelType": "NOTIFICATION_LIGHT_CHANNEL", + "groupIndex": 2, + "groups": [], + "index": 2, + "label": "", + "on": null, + "profileMode": "AUTOMATIC", + "simpleRGBColorState": "RED", + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "deviceId": "3014F711BSL0000000000050", + "dimLevel": 1.0, + "functionalChannelType": "NOTIFICATION_LIGHT_CHANNEL", + "groupIndex": 3, + "groups": [], + "index": 3, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "simpleRGBColorState": "GREEN", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BSL0000000000050", + "label": "Treppe", + "lastStatusUpdate": 1548431183264, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 360, + "modelType": "HmIP-BSL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711BSL0000000000050", + "type": "BRAND_SWITCH_NOTIFICATION_LIGHT", + "updateState": "UP_TO_DATE" + }, + "3014F711SLO0000000000026": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.16", + "firmwareVersionInteger": 65552, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711SLO0000000000026", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -60, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "averageIllumination": 807.3, + "currentIllumination": 785.2, + "deviceId": "3014F711SLO0000000000026", + "functionalChannelType": "LIGHT_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [], + "highestIllumination": 837.1, + "index": 1, + "label": "", + "lowestIllumination": 785.2 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711SLO0000000000026", + "label": "Lichtsensor Nord", + "lastStatusUpdate": 1548494235548, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 308, + "modelType": "HmIP-SLO", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711SLO0000000000026", + "type": "LIGHT_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000054": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.0", + "firmwareVersionInteger": 65536, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000054", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000053" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000054", + "functionalChannelType": "PASSAGE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000055" + ], + "index": 1, + "label": "", + "leftCounter": 966, + "leftRightCounterDelta": 164, + "passageBlindtime": 1.5, + "passageDirection": "LEFT", + "passageSensorSensitivity": 50.0, + "passageTimeout": 0.5, + "rightCounter": 802 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000054", + "label": "SPDR_1", + "lastStatusUpdate": 1547282742305, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 304, + "modelType": "HmIP-SPDR", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000054", + "type": "PASSAGE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F711000000000AAAAA25": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.12", + "firmwareVersionInteger": 65548, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711000000000AAAAA25", + "dutyCycle": false, + "functionalChannelType": "DEVICE_PERMANENT_FULL_RX", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 0, + "label": "", + "lowBat": false, + "permanentFullRx": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711000000000AAAAA25", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000048", + "00000000-0000-0000-0000-000000000034" + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711000000000AAAAA25", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000048", + "00000000-0000-0000-0000-000000000034" + ], + "index": 2, + "label": "" + }, + "3": { + "currentIllumination": null, + "deviceId": "3014F711000000000AAAAA25", + "functionalChannelType": "MOTION_DETECTION_CHANNEL", + "groupIndex": 2, + "groups": [], + "illumination": 14.2, + "index": 3, + "label": "", + "motionBufferActive": true, + "motionDetected": false, + "motionDetectionSendInterval": "SECONDS_240", + "numberOfBrightnessMeasurements": 7 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000000000AAAAA25", + "label": "Bewegungsmelder für 55er Rahmen – innen", + "lastStatusUpdate": 1546776387401, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 338, + "modelType": "HmIP-SMI55", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711000000000AAAAA25", + "type": "MOTION_DETECTOR_PUSH_BUTTON", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000038": { + "availableFirmwareVersion": "1.0.18", + "firmwareVersion": "1.0.18", + "firmwareVersionInteger": 65554, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000038", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -55, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 4.3, + "deviceId": "3014F7110000000000000038", + "functionalChannelType": "WEATHER_SENSOR_PLUS_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "humidity": 97, + "vaporAmount": 6.177718198711658, + "illumination": 26.4, + "illuminationThresholdSunshine": 3500.0, + "index": 1, + "label": "", + "raining": false, + "storm": false, + "sunshine": false, + "todayRainCounter": 3.8999999999999773, + "todaySunshineDuration": 0, + "totalRainCounter": 544.0999999999999, + "totalSunshineDuration": 132057, + "windSpeed": 15.0, + "windValueType": "CURRENT_VALUE", + "yesterdayRainCounter": 25.600000000000023, + "yesterdaySunshineDuration": 0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000038", + "label": "Weather Sensor – plus", + "lastStatusUpdate": 1546789939739, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 351, + "modelType": "HmIP-SWO-PL", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000038", + "type": "WEATHER_SENSOR_PLUS", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000BBBBB1": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "coolingEmergencyValue": 0.0, + "deviceId": "3014F7110000000000BBBBB1", + "dutyCycle": false, + "frostProtectionTemperature": 8.0, + "functionalChannelType": "DEVICE_GLOBAL_PUMP_CONTROL", + "globalPumpControl": true, + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "heatingEmergencyValue": 0.25, + "heatingLoadType": "LOAD_BALANCING", + "heatingValveType": "NORMALLY_CLOSE", + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -62, + "rssiPeerValue": null, + "unreach": false, + "valveProtectionDuration": 5, + "valveProtectionSwitchingInterval": 14 + }, + "1": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_LOCAL_PUMP_CHANNEL", + "groupIndex": 1, + "groups": [], + "index": 1, + "label": "", + "pumpFollowUpTime": 2, + "pumpLeadTime": 2, + "pumpProtectionDuration": 1, + "pumpProtectionSwitchingInterval": 14 + }, + "2": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 3, + "groups": [ + "00000000-0000-0000-0000-000000000009" + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 4, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 5, + "groups": [ + "00000000-0000-0000-0000-000000000011" + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "FLOOR_TERMINAL_BLOCK_CHANNEL", + "groupIndex": 6, + "groups": [], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "HEAT_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013" + ], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F7110000000000BBBBB1", + "functionalChannelType": "DEHUMIDIFIER_DEMAND_CHANNEL", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014" + ], + "index": 8, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000BBBBB1", + "label": "Fußbodenheizungsaktor", + "lastStatusUpdate": 1545746610807, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 277, + "modelType": "HmIP-FAL230-C6", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000BBBBB1", + "type": "FLOOR_TERMINAL_BLOCK_6", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000BBBBB8": { + "availableFirmwareVersion": "1.2.16", + "firmwareVersion": "1.2.16", + "firmwareVersionInteger": 66064, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000BBBBB8", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -59, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000BBBBB8", + "functionalChannelType": "ALARM_SIREN_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000BBBBB8", + "label": "Alarmsirene", + "lastStatusUpdate": 1544480290322, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 298, + "modelType": "HmIP-ASIR", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000BBBBB8", + "type": "ALARM_SIREN_INDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F711000000000000BB11": { + "availableFirmwareVersion": "1.4.8", + "firmwareVersion": "1.4.8", + "firmwareVersionInteger": 66568, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711000000000000BB11", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -56, + "rssiPeerValue": -52, + "sabotage": false, + "unreach": false + }, + "1": { + "currentIllumination": null, + "deviceId": "3014F711000000000000BB11", + "functionalChannelType": "MOTION_DETECTION_CHANNEL", + "groupIndex": 1, + "groups": [], + "illumination": 0.1, + "index": 1, + "label": "", + "motionBufferActive": false, + "motionDetected": true, + "motionDetectionSendInterval": "SECONDS_480", + "numberOfBrightnessMeasurements": 7 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711000000000000BB11", + "label": "Wohnzimmer", + "lastStatusUpdate": 1544480290322, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 291, + "modelType": "HmIP-SMI", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000011", + "type": "MOTION_DETECTOR_INDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F71100000000000BBB17": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.2", + "firmwareVersionInteger": 65538, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F71100000000000BBB17", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -70, + "rssiPeerValue": -67, + "unreach": false + }, + "1": { + "currentIllumination": null, + "deviceId": "3014F71100000000000BBB17", + "functionalChannelType": "MOTION_DETECTION_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "illumination": 233.4, + "index": 1, + "label": "", + "motionBufferActive": true, + "motionDetected": true, + "motionDetectionSendInterval": "SECONDS_240", + "numberOfBrightnessMeasurements": 7 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F71100000000000BBB17", + "label": "Außen Küche", + "lastStatusUpdate": 1546776559553, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 302, + "modelType": "HmIP-SMO-A", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F71100000000000BBB17", + "type": "MOTION_DETECTOR_OUTDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000050": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.2", + "firmwareVersionInteger": "65538", + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000050", + "dutyCycle": false, + "functionalChannelType": "DEVICE_INCORRECT_POSITIONED", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "incorrectPositioned": true, + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -65, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "acousticAlarmSignal": "FREQUENCY_RISING", + "acousticAlarmTiming": "ONCE_PER_MINUTE", + "acousticWaterAlarmTrigger": "WATER_DETECTION", + "deviceId": "3014F7110000000000000050", + "functionalChannelType": "WATER_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000023" + ], + "inAppWaterAlarmTrigger": "WATER_MOISTURE_DETECTION", + "index": 1, + "label": "", + "moistureDetected": false, + "sirenWaterAlarmTrigger": "WATER_MOISTURE_DETECTION", + "waterlevelDetected": false + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000050", + "label": "Wassersensor", + "lastStatusUpdate": 1530802738493, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 353, + "modelType": "HmIP-SWD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000050", + "type": "WATER_SENSOR", + "updateState": "UP_TO_DATE" + + }, + "3014F7110000000000000000": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000000", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -85, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000000", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "OPEN" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000000", + "label": "Balkontüre", + "lastStatusUpdate": 1524516526498, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000000", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000005551": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.2.12", + "firmwareVersionInteger": 66060, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000005551", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -73, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000005551", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000005551", + "label": "Eingangst\u00fcrkontakt", + "lastStatusUpdate": 1524515854304, + "liveUpdateState": "UP_TO_DATE", + "manufacturerCode": 1, + "modelId": 340, + "modelType": "HmIP-SWDM", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000005551", + "type": "SHUTTER_CONTACT_MAGNETIC", + "updateState": "BACKGROUND_UPDATE_NOT_SUPPORTED" + }, + "3014F7110000000000000001": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000001", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -64, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000001", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000009", + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000001", + "label": "Fenster", + "lastStatusUpdate": 1524515854304, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000001", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000002": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000002", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -95, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000002", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000002", + "label": "Balkonfenster", + "lastStatusUpdate": 1524516088763, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000002", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000003": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000003", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -78, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000003", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000003", + "label": "Küche", + "lastStatusUpdate": 1524514836466, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000003", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000004": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000004", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -56, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000004", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "OPEN" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000004", + "label": "Fenster", + "lastStatusUpdate": 1524512404032, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000004", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000005": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000005", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -80, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000005", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "OPEN" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000005", + "label": "Wohnzimmer", + "lastStatusUpdate": 0, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000005", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000006": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000006", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000005" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000006", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000015", + "00000000-0000-0000-0000-000000000005" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000006", + "label": "Wohnungstüre", + "lastStatusUpdate": 1524516489316, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000006", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000007": { + "availableFirmwareVersion": "1.16.8", + "firmwareVersion": "1.16.8", + "firmwareVersionInteger": 69640, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000007", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -56, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000007", + "eventDelay": 0, + "functionalChannelType": "SHUTTER_CONTACT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000016", + "00000000-0000-0000-0000-000000000015" + ], + "index": 1, + "label": "", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000007", + "label": "Vorzimmer", + "lastStatusUpdate": 1524515489257, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 258, + "modelType": "HMIP-SWDO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000007", + "type": "SHUTTER_CONTACT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000008": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000008", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -48, + "rssiPeerValue": -49, + "unreach": false + }, + "1": { + "currentPowerConsumption": 195.3, + "deviceId": "3014F7110000000000000008", + "energyCounter": 35.536, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000008", + "label": "Pc", + "lastStatusUpdate": 1524516554056, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000008", + "type": "PLUGABLE_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000009": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000009", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -60, + "rssiPeerValue": -66, + "unreach": false + }, + "1": { + "currentPowerConsumption": 0.0, + "deviceId": "3014F7110000000000000009", + "energyCounter": 0.4754, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000009", + "label": "Brunnen", + "lastStatusUpdate": 1524515786303, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000009", + "type": "PLUGABLE_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000010": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000010", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -47, + "rssiPeerValue": -49, + "unreach": false + }, + "1": { + "currentPowerConsumption": 2.04, + "deviceId": "3014F7110000000000000010", + "energyCounter": 1.5343, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000010", + "label": "Büro", + "lastStatusUpdate": 1524513613922, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000010", + "type": "PLUGABLE_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000011": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000011", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -54, + "rssiPeerValue": -51, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000011", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000012" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000011", + "label": "Heizung", + "lastStatusUpdate": 1524516360178, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000011", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000012": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000012", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": -54, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000012", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 19.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000012", + "label": "Heizkörperthermostat", + "lastStatusUpdate": 1524514105832, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000012", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000013": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000013", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -58, + "rssiPeerValue": -58, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000013", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000019" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000013", + "label": "Heizkörperthermostat", + "lastStatusUpdate": 1524514007132, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000013", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000014": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000014", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -60, + "rssiPeerValue": -58, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000014", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000014", + "label": "Küche-Heizung", + "lastStatusUpdate": 1524513898337, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000014", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000015": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000015", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -65, + "rssiPeerValue": -66, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000015", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000015", + "label": "Wohnzimmer-Heizung", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000015", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000016": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000016", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -50, + "rssiPeerValue": -51, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000016", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000021" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000016", + "label": "Heizkörperthermostat", + "lastStatusUpdate": 1524514626157, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000016", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000017": { + "automaticValveAdaptionNeeded": false, + "availableFirmwareVersion": "2.0.2", + "firmwareVersion": "2.0.2", + "firmwareVersionInteger": 131074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000017", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": true, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -67, + "rssiPeerValue": -62, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000017", + "functionalChannelType": "HEATING_THERMOSTAT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0, + "valvePosition": 0.0, + "valveState": "ADAPTION_DONE" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000017", + "label": "Balkon-Heizung", + "lastStatusUpdate": 1524511331830, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 269, + "modelType": "HMIP-eTRV", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000017", + "type": "HEATING_THERMOSTAT", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000018": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000018", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -67, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000018", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000006" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000018", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524461072721, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000018", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000019": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000019", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -50, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000019", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000009" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000019", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524480981494, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000019", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000020": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000020", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -54, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000020", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000013", + "00000000-0000-0000-0000-000000000022" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000020", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524456324824, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000020", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000021": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.11", + "firmwareVersionInteger": 65547, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000021", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000016" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -80, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000021", + "functionalChannelType": "SMOKE_DETECTOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000015" + ], + "index": 1, + "label": "", + "smokeDetectorAlarmType": "IDLE_OFF" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000021", + "label": "Rauchwarnmelder", + "lastStatusUpdate": 1524443129876, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 296, + "modelType": "HmIP-SWSD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000021", + "type": "SMOKE_DETECTOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000022": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000022", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000011" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -76, + "rssiPeerValue": -63, + "unreach": false + }, + "1": { + "actualTemperature": 24.7, + "deviceId": "3014F7110000000000000022", + "display": "ACTUAL_HUMIDITY", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000012" + ], + "humidity": 43, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000022", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516534382, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000022", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000023": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000023", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -61, + "rssiPeerValue": -58, + "unreach": false + }, + "1": { + "actualTemperature": 24.5, + "deviceId": "3014F7110000000000000023", + "display": "ACTUAL_HUMIDITY", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000010" + ], + "humidity": 46, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 19.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000023", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516454116, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000023", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000024": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000024", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000004" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -75, + "rssiPeerValue": -85, + "unreach": false + }, + "1": { + "actualTemperature": 23.6, + "deviceId": "3014F7110000000000000024", + "display": "ACTUAL", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000007" + ], + "humidity": 45, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000024", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516436601, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000024", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000025": { + "availableFirmwareVersion": "1.8.0", + "firmwareVersion": "1.8.0", + "firmwareVersionInteger": 67584, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000025", + "dutyCycle": false, + "functionalChannelType": "DEVICE_OPERATIONLOCK", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "index": 0, + "label": "", + "lowBat": false, + "operationLockActive": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": -47, + "unreach": false + }, + "1": { + "actualTemperature": 23.8, + "deviceId": "3014F7110000000000000025", + "display": "ACTUAL_HUMIDITY", + "functionalChannelType": "WALL_MOUNTED_THERMOSTAT_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000021" + ], + "humidity": 47, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "", + "valveActualTemperature": 20.0, + "setPointTemperature": 5.0, + "temperatureOffset": 0.0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000025", + "label": "Wandthermostat", + "lastStatusUpdate": 1524516556479, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 297, + "modelType": "HmIP-WTH-2", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000025", + "type": "WALL_MOUNTED_THERMOSTAT_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000029": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.14", + "firmwareVersionInteger": 65550, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000029", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000019" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -46, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110000000000000029", + "functionalChannelType": "MULTI_MODE_INPUT_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000020" + ], + "index": 1, + "label": "", + "multiModeInputMode": "KEY_BEHAVIOR", + "windowState": "CLOSED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000029", + "label": "Kontakt-Schnittstelle Unterputz – 1-fach", + "lastStatusUpdate": 1547923306429, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 382, + "modelType": "HmIP-FCI1", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F7110000000000000029", + "type": "FULL_FLUSH_CONTACT_INTERFACE", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000001": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.10", + "firmwareVersionInteger": 65546, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000001", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -68, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 15.4, + "deviceId": "3014F711AAAA000000000001", + "functionalChannelType": "WEATHER_SENSOR_PRO_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-AAAA-0000-0000-000000000001" + ], + "humidity": 65, + "vaporAmount": 6.177718198711658, + "illumination": 4153.0, + "illuminationThresholdSunshine": 10.0, + "index": 1, + "label": "", + "raining": false, + "storm": false, + "sunshine": true, + "todayRainCounter": 6.5, + "todaySunshineDuration": 100, + "totalRainCounter": 6.5, + "totalSunshineDuration": 100, + "weathervaneAlignmentNeeded": false, + "windDirection": 295.0, + "windDirectionVariation": 56.25, + "windSpeed": 2.6, + "windValueType": "AVERAGE_VALUE", + "yesterdayRainCounter": 0.0, + "yesterdaySunshineDuration": 0 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000001", + "label": "Wettersensor - pro", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 352, + "modelType": "HmIP-SWO-PR", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000001", + "type": "WEATHER_SENSOR_PRO", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000002": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.6", + "firmwareVersionInteger": 65542, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000002", + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -55, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 15.1, + "deviceId": "3014F711AAAA000000000002", + "functionalChannelType": "CLIMATE_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-AAAA-0000-0000-000000000001" + ], + "humidity": 70, + "vaporAmount": 6.177718198711658, + "index": 1, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000002", + "label": "Temperatur- und Luftfeuchtigkeitssensor - außen", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 314, + "modelType": "HmIP-STHO", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000002", + "type": "TEMPERATURE_HUMIDITY_SENSOR_OUTDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000003": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.0.10", + "firmwareVersionInteger": 65546, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000003", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ "00000000-0000-0000-0000-000000000008" ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -77, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "actualTemperature": 15.2, + "deviceId": "3014F711AAAA000000000003", + "functionalChannelType": "WEATHER_SENSOR_CHANNEL", + "groupIndex": 1, + "groups": [ "00000000-AAAA-0000-0000-000000000001" ], + "humidity": 42, + "vaporAmount": 6.177718198711658, + "illumination": 4890.0, + "illuminationThresholdSunshine": 3500.0, + "index": 1, + "label": "", + "storm": false, + "sunshine": true, + "todaySunshineDuration": 51, + "totalSunshineDuration": 54, + "windSpeed": 6.6, + "windValueType": "MAX_VALUE", + "yesterdaySunshineDuration": 3 + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000003", + "label": "Wettersensor", + "lastStatusUpdate": 1524513950325, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 350, + "modelType": "HmIP-SWO-B", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000003", + "type": "WEATHER_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000004": { + "availableFirmwareVersion": "1.2.10", + "firmwareVersion": "1.2.10", + "firmwareVersionInteger": 66058, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000004", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ "00000000-0000-0000-0000-000000000008" ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -54, + "rssiPeerValue": null, + "sabotage": false, + "unreach": false + }, + "1": { + "deviceId": "3014F711AAAA000000000004", + "eventDelay": 0, + "functionalChannelType": "ROTARY_HANDLE_CHANNEL", + "groupIndex": 1, + "groups": [ "00000000-0000-0000-0000-000000000009" ], + "index": 1, + "label": "", + "windowState": "TILTED" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000004", + "label": "Fenstergriffsensor", + "lastStatusUpdate": 1524816385462, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 286, + "modelType": "HmIP-SRH", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000004", + "type": "ROTARY_HANDLE_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAA000000000005": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.4.8", + "firmwareVersionInteger": 66568, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAA000000000005", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -44, + "rssiPeerValue": -42, + "unreach": false + }, + "1": { + "deviceId": "3014F711AAAA000000000005", + "dimLevel": 0.0, + "functionalChannelType": "DIMMER_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000008" + ], + "index": 1, + "label": "", + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAA000000000005", + "label": "Schlafzimmerlicht", + "lastStatusUpdate": 1524816385462, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 290, + "modelType": "HmIP-BDT", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAA000000000005", + "type": "BRAND_DIMMER", + "updateState": "UP_TO_DATE" + }, + "3014F711BBBBBBBBBBBBB017": { + "availableFirmwareVersion": "1.0.19", + "firmwareVersion": "1.0.19", + "firmwareVersionInteger": 65555, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BBBBBBBBBBBBB017", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -61, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F711BBBBBBBBBBBBB017", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 6, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BBBBBBBBBBBBB017", + "label": "Wandtaster - 6-fach", + "lastStatusUpdate": 1544475961687, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 300, + "modelType": "HmIP-WRC6", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB017", + "type": "PUSH_BUTTON_6", + "updateState": "UP_TO_DATE" + }, + "3014F711BBBBBBBBBBBBB016": { + "availableFirmwareVersion": "1.0.19", + "firmwareVersion": "1.0.19", + "firmwareVersionInteger": 65555, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BBBBBBBBBBBBB016", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -42, + "rssiPeerValue": null, + "unreach": false + }, + "1": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + }, + "3": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 3, + "label": "" + }, + "4": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 2, + "groups": [ + ], + "index": 4, + "label": "" + }, + "5": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 5, + "label": "" + }, + "6": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 3, + "groups": [ + ], + "index": 6, + "label": "" + }, + "7": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [ + ], + "index": 7, + "label": "" + }, + "8": { + "deviceId": "3014F711BBBBBBBBBBBBB016", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 4, + "groups": [ + ], + "index": 8, + "label": "" + } + + + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BBBBBBBBBBBBB016", + "label": "Fernbedienung - 8 Tasten", + "lastStatusUpdate": 1544479483638, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 299, + "modelType": "HmIP-RC8", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB016", + "type": "REMOTE_CONTROL_8", + "updateState": "UP_TO_DATE" + }, + "3014F711AAAAAAAAAAAAAA51": { + "availableFirmwareVersion": "1.4.0", + "firmwareVersion": "1.4.0", + "firmwareVersionInteger": 66560, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711AAAAAAAAAAAAAA51", + "dutyCycle": false, + "functionalChannelType": "DEVICE_SABOTAGE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000021", + "00000000-0000-0000-0000-000000000060" + ], + "index": 0, + "label": "", + "lowBat": false, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -62, + "rssiPeerValue": -61, + "sabotage": false, + "unreach": false + }, + "1": { + "currentIllumination": null, + "deviceId": "3014F711AAAAAAAAAAAAAA51", + "functionalChannelType": "PRESENCE_DETECTION_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000022", + "00000000-0000-0000-0000-000000000060" + ], + "illumination": 1.8, + "index": 1, + "label": "", + "motionBufferActive": false, + "motionDetectionSendInterval": "SECONDS_240", + "numberOfBrightnessMeasurements": 7, + "presenceDetected": false + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711AAAAAAAAAAAAAA51", + "label": "SPI_1", + "lastStatusUpdate": 1542758692234, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 303, + "modelType": "HmIP-SPI", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711AAAAAAAAAAAAAA51", + "type": "PRESENCE_DETECTOR_INDOOR", + "updateState": "UP_TO_DATE" + }, + "3014F711ACBCDABCADCA66": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711ACBCDABCADCA66", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000024" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -78, + "rssiPeerValue": -77, + "unreach": false + }, + "1": { + "bottomToTopReferenceTime": 30.080000000000002, + "changeOverDelay": 0.5, + "delayCompensationValue": 12.7, + "deviceId": "3014F711ACBCDABCADCA66", + "endpositionAutoDetectionEnabled": true, + "functionalChannelType": "SHUTTER_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000069", + "00000000-0000-0000-0000-000000000070" + ], + "index": 1, + "label": "", + "previousShutterLevel": null, + "processing": false, + "profileMode": "AUTOMATIC", + "selfCalibrationInProgress": null, + "shutterLevel": 1.0, + "supportingDelayCompensation": true, + "supportingEndpositionAutoDetection": true, + "supportingSelfCalibration": true, + "topToBottomReferenceTime": 24.68, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711ACBCDABCADCA66", + "label": "BROLL_1", + "lastStatusUpdate": 1542756558785, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 323, + "modelType": "HmIP-BROLL", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711ACBCDABCADCA66", + "type": "BRAND_SHUTTER", + "updateState": "UP_TO_DATE" + }, + "3014F711BBBBBBBBBBBBB18": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "1.8.12", + "firmwareVersionInteger": 67596, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F711BBBBBBBBBBBBB18", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000041" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -35, + "rssiPeerValue": -36, + "unreach": false + }, + "1": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "2": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 2, + "groups": [ + "00000000-0000-0000-0000-000000000042", + "00000000-0000-0000-0000-000000000040" + ], + "index": 2, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "3": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 3, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 3, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "4": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 4, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 4, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "5": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 5, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 5, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "6": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 6, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 6, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "7": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 7, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 7, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + }, + "8": { + "deviceId": "3014F711BBBBBBBBBBBBB18", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 8, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 8, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711BBBBBBBBBBBBB18", + "label": "ioBroker", + "lastStatusUpdate": 1543746604446, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 307, + "modelType": "HmIP-MOD-OC8", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F711BBBBBBBBBBBBB18", + "type": "OPEN_COLLECTOR_8_MODULE", + "updateState": "UP_TO_DATE" + } + }, + "groups": { + "00000000-0000-0000-0000-000000000020": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000025" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000016" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000050" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000021" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000020", + "incorrectPositioned": null, + "label": "Badezimmer", + "lastStatusUpdate": 1524516556479, + "lowBat": false, + "metaGroupId": null, + "sabotage": null, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000012": { + "activeProfile": "PROFILE_1", + "actualTemperature": 24.7, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000022" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000011" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 43, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000012", + "label": "Schlafzimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516534382, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000011", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000023", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000024", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000025", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000026", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000027", + "visible": true + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000012", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000028", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000016": { + "active": false, + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000021" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000020" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000019" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000018" + } + ], + "configPending": false, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000016", + "ignorableDevices": [], + "label": "INTERNAL", + "lastStatusUpdate": 1524515489257, + "lowBat": false, + "metaGroupId": null, + "motionDetected": null, + "presenceDetected": null, + "sabotage": false, + "silent": true, + "type": "SECURITY_ZONE", + "unreach": false, + "windowState": "CLOSED", + "zoneAssignmentIndex": "ALARM_MODE_ZONE_3" + }, + "00000000-0000-0000-0000-000000000017": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000008" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000009" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000010" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000017", + "incorrectPositioned": null, + "label": "Strom", + "lastStatusUpdate": 1524516554056, + "lowBat": null, + "metaGroupId": null, + "sabotage": null, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000029": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000029", + "label": "HEATING_TEMPERATURE_LIMITER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "HEATING_TEMPERATURE_LIMITER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000030": { + "boilerFollowUpTime": 0, + "boilerLeadTime": 0, + "channels": [], + "dutyCycle": null, + "heatDemand": null, + "heatDemandRuleEnabled": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000030", + "label": "HEATING_COOLING_DEMAND_BOILER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "triggered": false, + "type": "HEATING_COOLING_DEMAND_BOILER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000010": { + "activeProfile": "PROFILE_1", + "actualTemperature": 24.5, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000023" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000012" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 46, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000010", + "label": "Büro", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516454116, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000031", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000032", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000033", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000034", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000035", + "visible": true + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000010", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000036", + "visible": false + } + }, + "setPointTemperature": 19.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": "CLOSED" + }, + "00000000-0000-0000-0000-000000000018": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000010" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000009" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000008" + } + ], + "dimLevel": null, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000018", + "label": "Strom", + "lastStatusUpdate": 1524516554056, + "lowBat": null, + "metaGroupId": "00000000-0000-0000-0000-000000000017", + "on": true, + "processing": null, + "shutterLevel": null, + "slatsLevel": null, + "type": "SWITCHING", + "unreach": false + }, + "00000000-0000-0000-0000-000000000009": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000019" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000009", + "label": "Büro", + "lastStatusUpdate": 1524515854304, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "CLOSED" + }, + "00000000-0000-0000-0000-000000000013": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000020" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000013", + "label": "Schlafzimmer", + "lastStatusUpdate": 1524512404032, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000011", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000005": { + "active": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000003" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000003" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000004" + } + ], + "configPending": false, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000005", + "ignorableDevices": [], + "label": "EXTERNAL", + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "metaGroupId": null, + "motionDetected": null, + "presenceDetected": null, + "sabotage": false, + "silent": true, + "type": "SECURITY_ZONE", + "unreach": false, + "windowState": "OPEN", + "zoneAssignmentIndex": "ALARM_MODE_ZONE_2" + }, + "00000000-0000-0000-0000-000000000022": { + "acousticFeedbackEnabled": true, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000020" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000018" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000021" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000019" + } + ], + "dimLevel": null, + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000022", + "label": "SIREN", + "lastStatusUpdate": 1524480981494, + "lowBat": false, + "metaGroupId": null, + "on": false, + "onTime": 180.0, + "signalAcoustic": "FREQUENCY_RISING", + "signalOptical": "DOUBLE_FLASHING_REPEATING", + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "ALARM_SWITCHING", + "unreach": false + }, + "00000000-0000-0000-0000-000000000037": { + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000037", + "label": "COMING_HOME", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "LINKED_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-0000-000000000021": { + "activeProfile": "PROFILE_1", + "actualTemperature": 23.8, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000025" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000016" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 47, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000021", + "label": "Badezimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516556479, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000020", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000038", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000039", + "visible": false + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000040", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000041", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000042", + "visible": false + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000021", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000043", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, + "00000000-0000-0000-0000-000000000006": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000018" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000003" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000006", + "label": "Wohnzimmer", + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000004", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000044": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000044", + "label": "INBOX", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "INBOX", + "unreach": null + }, + "00000000-0000-0000-0000-000000000045": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000045", + "label": "HEATING_HUMIDITY_LIMITER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "HEATING_HUMIDITY_LIMITER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000008": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000001" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000012" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000023" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000019" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000009" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000008", + "incorrectPositioned": null, + "label": "Büro", + "lastStatusUpdate": 1524516454116, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000011": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000022" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000004" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000020" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000011" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000013" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000011", + "incorrectPositioned": null, + "label": "Schlafzimmer", + "lastStatusUpdate": 1524516534382, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000046": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000046", + "label": "HEATING_CHANGEOVER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "HEATING_CHANGEOVER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000014": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000021" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000013" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000015", + "00000000-0000-0000-0000-000000000019" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000014", + "incorrectPositioned": null, + "label": "Vorzimmer", + "lastStatusUpdate": 1524516489316, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000007": { + "activeProfile": "PROFILE_1", + "actualTemperature": 23.6, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000024" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000017" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000015" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000014" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000003" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": false, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 45, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000007", + "label": "Wohnzimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000004", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000047", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000048", + "visible": true + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000049", + "visible": false + }, + "PROFILE_4": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000050", + "visible": true + }, + "PROFILE_5": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000051", + "visible": true + }, + "PROFILE_6": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000007", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000052", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": "OPEN" + }, + "00000000-0000-0000-0000-000000000053": { + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000053", + "label": "PANIC", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "LINKED_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-0000-000000000054": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000054", + "label": "HEATING_EXTERNAL_CLOCK", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "type": "HEATING_EXTERNAL_CLOCK", + "unreach": null + }, + "00000000-0000-0000-0000-000000000055": { + "channels": [], + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000055", + "label": "HEATING_DEHUMIDIFIER", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "type": "HEATING_DEHUMIDIFIER", + "unreach": null + }, + "00000000-0000-0000-0000-000000000056": { + "acousticFeedbackEnabled": true, + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000056", + "label": "ALARM", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "onTime": 7200.0, + "signalAcoustic": "FREQUENCY_RISING", + "signalOptical": "DOUBLE_FLASHING_REPEATING", + "smokeDetectorAlarmType": null, + "type": "ALARM_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-0000-000000000057": { + "channels": [], + "dutyCycle": null, + "heatDemand": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000057", + "label": "HEATING_COOLING_DEMAND_PUMP", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "pumpFollowUpTime": 2, + "pumpLeadTime": 2, + "pumpProtectionDuration": 1, + "pumpProtectionSwitchingInterval": 14, + "type": "HEATING_COOLING_DEMAND_PUMP", + "unreach": null + }, + "00000000-0000-0000-0000-000000000015": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000007" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000006" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000021" + } + ], + "dutyCycle": false, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000015", + "label": "Vorzimmer", + "lastStatusUpdate": 1524516489316, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "motionDetected": null, + "presenceDetected": null, + "moistureDetected": null, + "waterlevelDetected": null, + "powerMainsFailure": null, + "sabotage": false, + "smokeDetectorAlarmType": "IDLE_OFF", + "type": "SECURITY", + "unreach": false, + "windowState": "CLOSED" + }, + "00000000-0000-0000-0000-000000000004": { + "channels": [ + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000024" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000005" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000002" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000000" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000014" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000003" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000017" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000015" + }, + { + "channelIndex": 0, + "deviceId": "3014F7110000000000000018" + } + ], + "configPending": false, + "dutyCycle": false, + "groups": [ + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000007" + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000004", + "incorrectPositioned": null, + "label": "Wohnzimmer", + "lastStatusUpdate": 1524516526498, + "lowBat": false, + "metaGroupId": null, + "sabotage": false, + "type": "META", + "unreach": false + }, + "00000000-0000-0000-0000-000000000019": { + "activeProfile": "PROFILE_1", + "actualTemperature": null, + "boostDuration": 15, + "boostMode": false, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000013" + } + ], + "controlMode": "AUTOMATIC", + "controllable": true, + "cooling": null, + "coolingAllowed": false, + "coolingIgnored": false, + "dutyCycle": false, + "ecoAllowed": true, + "ecoIgnored": false, + "externalClockCoolingTemperature": 23.0, + "externalClockEnabled": false, + "externalClockHeatingTemperature": 19.0, + "floorHeatingMode": "FLOOR_HEATING_STANDARD", + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": null, + "humidityLimitEnabled": true, + "humidityLimitValue": 60, + "id": "00000000-0000-0000-0000-000000000019", + "label": "Vorzimmer", + "lastSetPointReachedTimestamp": 1557767559939, + "lastSetPointUpdatedTimestamp": 1557767559939, + "lastStatusUpdate": 1524514007132, + "lowBat": false, + "maxTemperature": 30.0, + "metaGroupId": "00000000-0000-0000-0000-000000000014", + "minTemperature": 5.0, + "partyMode": false, + "profiles": { + "PROFILE_1": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_1", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000058", + "visible": true + }, + "PROFILE_2": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_2", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000059", + "visible": false + }, + "PROFILE_3": { + "enabled": true, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_3", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000060", + "visible": false + }, + "PROFILE_4": { + "enabled": false, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_4", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000061", + "visible": true + }, + "PROFILE_5": { + "enabled": false, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_5", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000062", + "visible": false + }, + "PROFILE_6": { + "enabled": false, + "groupId": "00000000-0000-0000-0000-000000000019", + "index": "PROFILE_6", + "name": "", + "profileId": "00000000-0000-0000-0000-000000000063", + "visible": false + } + }, + "setPointTemperature": 5.0, + "type": "HEATING", + "unreach": false, + "valvePosition": 0.0, + "valveSilentModeEnabled": false, + "valveSilentModeSupported": false, + "heatingFailureSupported": true, + "windowOpenTemperature": 5.0, + "windowState": null + }, + "00000000-AAAA-0000-0000-000000000001": { + "actualTemperature": 15.4, + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F711AAAA000000000003" + }, + { + "channelIndex": 1, + "deviceId": "3014F711AAAA000000000002" + }, + { + "channelIndex": 1, + "deviceId": "3014F711AAAA000000000001" + } + ], + "homeId": "00000000-0000-0000-0000-000000000001", + "humidity": 65, + "id": "00000000-AAAA-0000-0000-000000000001", + "illumination": 4703.0, + "label": "Terrasse", + "lastStatusUpdate": 1520770214834, + "lowBat": false, + "metaGroupId": "76df95a5-afa5-45ee-b817-f724ffaf04a1", + "raining": false, + "type": "ENVIRONMENT", + "unreach": false, + "windSpeed": 29.1 + }, + "00000000-BBBB-0000-0000-000000000052": { + "channels": [], + "checkInterval": 600, + "dutyCycle": null, + "enabled": true, + "heatingFailureValidationResult": "NO_HEATING_FAILURE", + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-BBBB-0000-0000-000000000052", + "label": "HEATING_FAILURE_ALERT_RULE_GROUP", + "lastExecutionTimestamp": 1550773800084, + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "triggered": false, + "type": "HEATING_FAILURE_ALERT_RULE_GROUP", + "unreach": null, + "validationTimeout": 86400000 + }, + "00000000-AAAA-0000-0000-000000000068": { + "acousticFeedbackEnabled": true, + "channels": [], + "dimLevel": null, + "dutyCycle": null, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-AAAA-0000-0000-000000000068", + "label": "BACKUP_ALARM_SIREN", + "lastStatusUpdate": 0, + "lowBat": null, + "metaGroupId": null, + "on": null, + "onTime": 180.0, + "signalAcoustic": "FREQUENCY_RISING", + "signalOptical": "DISABLE_OPTICAL_SIGNAL", + "smokeDetectorAlarmType": null, + "type": "SECURITY_BACKUP_ALARM_SWITCHING", + "unreach": null + }, + "00000000-0000-0000-AAAA-000000000029": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000023" + } + ], + "dutyCycle": false, + "enabled": true, + "homeId": "00000000-0000-0000-0000-000000000001", + "humidityLowerThreshold": 40, + "humidityUpperThreshold": 60, + "humidityValidationResult": "LESSER_LOWER_THRESHOLD", + "id": "00000000-0000-0000-AAAA-000000000029", + "label": "B\u00fcro", + "lastExecutionTimestamp": 1551387905665, + "lastStatusUpdate": 1551388104260, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "outdoorClimateSensor": null, + "triggered": false, + "type": "HUMIDITY_WARNING_RULE_GROUP", + "unreach": false, + "ventilationRecommended": true + }, + "00000000-0000-0000-0000-000000000049": { + "channels": [ + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000038" + }, + { + "channelIndex": 1, + "deviceId": "3014F7110000000000000023" + } + ], + "dutyCycle": false, + "enabled": true, + "homeId": "00000000-0000-0000-0000-000000000001", + "humidityLowerThreshold": 30, + "humidityUpperThreshold": 60, + "humidityValidationResult": null, + "id": "00000000-0000-0000-0000-000000000049", + "label": "Schlafzimmer", + "lastExecutionTimestamp": 0, + "lastStatusUpdate": 1551003370150, + "lowBat": false, + "metaGroupId": "00000000-0000-0000-0000-000000000008", + "outdoorClimateSensor": { + "channelIndex": 1, + "deviceId": "3014F7110000000000000038" + }, + "triggered": false, + "type": "HUMIDITY_WARNING_RULE_GROUP", + "unreach": false, + "ventilationRecommended": false + } + }, + "home": { + "apExchangeClientId": null, + "apExchangeState": "NONE", + "availableAPVersion": null, + "carrierSense": null, + "clients": [ + "00000000-0000-0000-0000-000000000000" + ], + "connected": true, + "currentAPVersion": "1.2.4", + "deviceUpdateStrategy": "AUTOMATICALLY_IF_POSSIBLE", + "dutyCycle": 8.0, + "functionalHomes": { + "INDOOR_CLIMATE": { + "absenceEndTime": null, + "absenceType": "NOT_ABSENT", + "active": true, + "coolingEnabled": false, + "ecoDuration": "PERMANENT", + "ecoTemperature": 17.0, + "floorHeatingSpecificGroups": { + "HEATING_CHANGEOVER": "00000000-0000-0000-0000-000000000046", + "HEATING_COOLING_DEMAND_BOILER": "00000000-0000-0000-0000-000000000030", + "HEATING_COOLING_DEMAND_PUMP": "00000000-0000-0000-0000-000000000057", + "HEATING_DEHUMIDIFIER": "00000000-0000-0000-0000-000000000055", + "HEATING_EXTERNAL_CLOCK": "00000000-0000-0000-0000-000000000054", + "HEATING_HUMIDITY_LIMITER": "00000000-0000-0000-0000-000000000045", + "HEATING_TEMPERATURE_LIMITER": "00000000-0000-0000-0000-000000000029" + }, + "functionalGroups": [ + "00000000-0000-0000-0000-000000000012", + "00000000-0000-0000-0000-000000000007", + "00000000-0000-0000-0000-000000000019", + "00000000-0000-0000-0000-000000000010", + "00000000-0000-0000-0000-000000000021" + ], + "optimumStartStopEnabled": false, + "solution": "INDOOR_CLIMATE" + }, + "LIGHT_AND_SHADOW": { + "active": true, + "extendedLinkedShutterGroups": [], + "extendedLinkedSwitchingGroups": [], + "functionalGroups": [ + "00000000-0000-0000-0000-000000000018" + ], + "shutterProfileGroups": [], + "solution": "LIGHT_AND_SHADOW", + "switchingProfileGroups": [] + }, + "SECURITY_AND_ALARM": { + "activationInProgress": false, + "active": true, + "alarmActive": false, + "alarmEventDeviceId": "3014F7110000000000000007", + "alarmEventTimestamp": 1524504122047, + "alarmSecurityJournalEntryType": "SENSOR_EVENT", + "functionalGroups": [ + "00000000-0000-0000-0000-000000000013", + "00000000-0000-0000-0000-000000000006", + "00000000-0000-0000-0000-000000000015", + "00000000-0000-0000-0000-000000000009" + ], + "intrusionAlertThroughSmokeDetectors": false, + "securitySwitchingGroups": { + "ALARM": "00000000-0000-0000-0000-000000000056", + "BACKUP_ALARM_SIREN": "00000000-AAAA-0000-0000-000000000068", + "COMING_HOME": "00000000-0000-0000-0000-000000000037", + "PANIC": "00000000-0000-0000-0000-000000000053", + "SIREN": "00000000-0000-0000-0000-000000000022" + }, + "securityZoneActivationMode": "ACTIVATION_WITH_DEVICE_IGNORELIST", + "securityZones": { + "EXTERNAL": "00000000-0000-0000-0000-000000000005", + "INTERNAL": "00000000-0000-0000-0000-000000000016" + }, + "solution": "SECURITY_AND_ALARM", + "zoneActivationDelay": 0.0 + }, + "WEATHER_AND_ENVIRONMENT": { + "active": true, + "functionalGroups": [ + "00000000-AAAA-0000-0000-000000000001" + ], + "solution": "WEATHER_AND_ENVIRONMENT" + } + }, + "id": "00000000-0000-0000-0000-000000000001", + "inboxGroup": "00000000-0000-0000-0000-000000000044", + "lastReadyForUpdateTimestamp": 1522319489138, + "location": { + "city": "1010 Wien, Österreich", + "latitude": "48.208088", + "longitude": "16.358608" + }, + "metaGroups": [ + "00000000-0000-0000-0000-000000000011", + "00000000-0000-0000-0000-000000000008", + "00000000-0000-0000-0000-000000000014", + "00000000-0000-0000-0000-000000000004", + "00000000-0000-0000-0000-000000000017", + "00000000-0000-0000-0000-000000000020" + ], + "pinAssigned": false, + "powerMeterCurrency": "EUR", + "powerMeterUnitPrice": 0.0, + "ruleGroups": [ + "00000000-0000-0000-0000-000000000057", + "00000000-0000-0000-0000-000000000030" + ], + "ruleMetaDatas": { + "00000000-0000-0000-0000-000000000065": { + "active": true, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "00000000-0000-0000-0000-000000000065", + "label": "Alarmanlage", + "ruleErrorCategories": [], + "type": "SIMPLE" + } + }, + "timeZoneId": "Europe/Vienna", + "updateState": "UP_TO_DATE", + "voiceControlSettings": { + "allowedActiveSecurityZoneIds": [] + }, + "weather": { + "humidity": 54, + "maxTemperature": 16.6, + "minTemperature": 16.6, + "temperature": 16.6, + "vaporAmount": 5.465858858389302, + "weatherCondition": "LIGHT_CLOUDY", + "weatherDayTime": "NIGHT", + "windDirection": 294, + "windSpeed": 8.568 + } + } +} From bd6bbcd5affaeac0076357937c3da94dee8f987f Mon Sep 17 00:00:00 2001 From: Santobert Date: Sun, 6 Oct 2019 13:05:51 +0200 Subject: [PATCH 058/639] Neato config flow (#26579) * initial commit * Minor changes * add async setup entry * Add translations and some other stuff * add and remove entry * use async_setup_entry * Update config_flows.py * dshokouhi's changes * Improve workflow * Add valid_vendors * Add entity registry * Add device registry * Update entry from configuration.yaml * Revert unneccesary changes * Update .coveragerc * Prepared tests * Add dshokouhi and Santobert as codeowners * Fix unload entry and abort when already_configured * First tests * Add test for abort cases * Add test for invalid credentials on import * Add one last test * Add test_init.py with some tests * Address reviews, part 1 * Update outdated entry * await instead of add_job * run IO inside an executor * remove faulty test * Fix pylint issues * Move IO out of constructur * Edit error translations * Edit imports * Minor changes * Remove test for invalid vendor * Async setup platform * Edit login function * Moved IO out if init * Update switches after added to hass * Revert update outdated entry * try and update new entrys from config.yaml * Add test invalid vendor * Default to neato --- .coveragerc | 4 +- CODEOWNERS | 1 + .../components/neato/.translations/en.json | 26 ++ homeassistant/components/neato/__init__.py | 271 +++++++----------- homeassistant/components/neato/camera.py | 20 +- homeassistant/components/neato/config_flow.py | 112 ++++++++ homeassistant/components/neato/const.py | 150 ++++++++++ homeassistant/components/neato/manifest.json | 8 +- homeassistant/components/neato/strings.json | 26 ++ homeassistant/components/neato/switch.py | 29 +- homeassistant/components/neato/vacuum.py | 29 +- homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/neato/__init__.py | 1 + tests/components/neato/test_config_flow.py | 129 +++++++++ tests/components/neato/test_init.py | 70 +++++ 17 files changed, 691 insertions(+), 190 deletions(-) create mode 100644 homeassistant/components/neato/.translations/en.json create mode 100644 homeassistant/components/neato/config_flow.py create mode 100644 homeassistant/components/neato/const.py create mode 100644 homeassistant/components/neato/strings.json create mode 100644 tests/components/neato/__init__.py create mode 100644 tests/components/neato/test_config_flow.py create mode 100644 tests/components/neato/test_init.py diff --git a/.coveragerc b/.coveragerc index 5c2d2e02f45..6f3dfbc94a8 100644 --- a/.coveragerc +++ b/.coveragerc @@ -420,7 +420,9 @@ omit = homeassistant/components/n26/* homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py - homeassistant/components/neato/* + homeassistant/components/neato/camera.py + homeassistant/components/neato/vacuum.py + homeassistant/components/neato/switch.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py homeassistant/components/nest/* diff --git a/CODEOWNERS b/CODEOWNERS index 935d68033e3..ba4058d5acf 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -187,6 +187,7 @@ homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core homeassistant/components/mysensors/* @MartinHjelmare homeassistant/components/mystrom/* @fabaff +homeassistant/components/neato/* @dshokouhi @Santobert homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan diff --git a/homeassistant/components/neato/.translations/en.json b/homeassistant/components/neato/.translations/en.json new file mode 100644 index 00000000000..dc13242cc1d --- /dev/null +++ b/homeassistant/components/neato/.translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "Neato", + "step": { + "user": { + "title": "Neato Account Info", + "data": { + "username": "Username", + "password": "Password", + "vendor": "Vendor" + }, + "description": "See [Neato documentation]({docs_url})." + } + }, + "error": { + "invalid_credentials": "Invalid credentials" + }, + "create_entry": { + "default": "See [Neato documentation]({docs_url})." + }, + "abort": { + "already_configured": "Already configured", + "invalid_credentials": "Invalid credentials" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index e17c562171a..8fd545c59bb 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -1,194 +1,125 @@ """Support for Neato botvac connected vacuum cleaners.""" +import asyncio import logging from datetime import timedelta -from urllib.error import HTTPError +from requests.exceptions import HTTPError, ConnectionError as ConnError import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle +from .config_flow import NeatoConfigFlow +from .const import ( + CONF_VENDOR, + NEATO_CONFIG, + NEATO_DOMAIN, + NEATO_LOGIN, + NEATO_ROBOTS, + NEATO_PERSISTENT_MAPS, + NEATO_MAP_DATA, + VALID_VENDORS, +) + _LOGGER = logging.getLogger(__name__) -CONF_VENDOR = "vendor" -DOMAIN = "neato" -NEATO_ROBOTS = "neato_robots" -NEATO_LOGIN = "neato_login" -NEATO_MAP_DATA = "neato_map_data" -NEATO_PERSISTENT_MAPS = "neato_persistent_maps" CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( + NEATO_DOMAIN: vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_VENDOR, default="neato"): vol.In( - ["neato", "vorwerk"] - ), + vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS), } ) }, extra=vol.ALLOW_EXTRA, ) -MODE = {1: "Eco", 2: "Turbo"} -ACTION = { - 0: "Invalid", - 1: "House Cleaning", - 2: "Spot Cleaning", - 3: "Manual Cleaning", - 4: "Docking", - 5: "User Menu Active", - 6: "Suspended Cleaning", - 7: "Updating", - 8: "Copying logs", - 9: "Recovering Location", - 10: "IEC test", - 11: "Map cleaning", - 12: "Exploring map (creating a persistent map)", - 13: "Acquiring Persistent Map IDs", - 14: "Creating & Uploading Map", - 15: "Suspended Exploration", -} - -ERRORS = { - "ui_error_battery_battundervoltlithiumsafety": "Replace battery", - "ui_error_battery_critical": "Replace battery", - "ui_error_battery_invalidsensor": "Replace battery", - "ui_error_battery_lithiumadapterfailure": "Replace battery", - "ui_error_battery_mismatch": "Replace battery", - "ui_error_battery_nothermistor": "Replace battery", - "ui_error_battery_overtemp": "Replace battery", - "ui_error_battery_overvolt": "Replace battery", - "ui_error_battery_undercurrent": "Replace battery", - "ui_error_battery_undertemp": "Replace battery", - "ui_error_battery_undervolt": "Replace battery", - "ui_error_battery_unplugged": "Replace battery", - "ui_error_brush_stuck": "Brush stuck", - "ui_error_brush_overloaded": "Brush overloaded", - "ui_error_bumper_stuck": "Bumper stuck", - "ui_error_check_battery_switch": "Check battery", - "ui_error_corrupt_scb": "Call customer service corrupt board", - "ui_error_deck_debris": "Deck debris", - "ui_error_dflt_app": "Check Neato app", - "ui_error_disconnect_chrg_cable": "Disconnected charge cable", - "ui_error_disconnect_usb_cable": "Disconnected USB cable", - "ui_error_dust_bin_missing": "Dust bin missing", - "ui_error_dust_bin_full": "Dust bin full", - "ui_error_dust_bin_emptied": "Dust bin emptied", - "ui_error_hardware_failure": "Hardware failure", - "ui_error_ldrop_stuck": "Clear my path", - "ui_error_lds_jammed": "Clear my path", - "ui_error_lds_bad_packets": "Check Neato app", - "ui_error_lds_disconnected": "Check Neato app", - "ui_error_lds_missed_packets": "Check Neato app", - "ui_error_lwheel_stuck": "Clear my path", - "ui_error_navigation_backdrop_frontbump": "Clear my path", - "ui_error_navigation_backdrop_leftbump": "Clear my path", - "ui_error_navigation_backdrop_wheelextended": "Clear my path", - "ui_error_navigation_noprogress": "Clear my path", - "ui_error_navigation_origin_unclean": "Clear my path", - "ui_error_navigation_pathproblems": "Cannot return to base", - "ui_error_navigation_pinkycommsfail": "Clear my path", - "ui_error_navigation_falling": "Clear my path", - "ui_error_navigation_noexitstogo": "Clear my path", - "ui_error_navigation_nomotioncommands": "Clear my path", - "ui_error_navigation_rightdrop_leftbump": "Clear my path", - "ui_error_navigation_undockingfailed": "Clear my path", - "ui_error_picked_up": "Picked up", - "ui_error_qa_fail": "Check Neato app", - "ui_error_rdrop_stuck": "Clear my path", - "ui_error_reconnect_failed": "Reconnect failed", - "ui_error_rwheel_stuck": "Clear my path", - "ui_error_stuck": "Stuck!", - "ui_error_unable_to_return_to_base": "Unable to return to base", - "ui_error_unable_to_see": "Clean vacuum sensors", - "ui_error_vacuum_slip": "Clear my path", - "ui_error_vacuum_stuck": "Clear my path", - "ui_error_warning": "Error check app", - "batt_base_connect_fail": "Battery failed to connect to base", - "batt_base_no_power": "Battery base has no power", - "batt_low": "Battery low", - "batt_on_base": "Battery on base", - "clean_tilt_on_start": "Clean the tilt on start", - "dustbin_full": "Dust bin full", - "dustbin_missing": "Dust bin missing", - "gen_picked_up": "Picked up", - "hw_fail": "Hardware failure", - "hw_tof_sensor_sensor": "Hardware sensor disconnected", - "lds_bad_packets": "Bad packets", - "lds_deck_debris": "Debris on deck", - "lds_disconnected": "Disconnected", - "lds_jammed": "Jammed", - "lds_missed_packets": "Missed packets", - "maint_brush_stuck": "Brush stuck", - "maint_brush_overload": "Brush overloaded", - "maint_bumper_stuck": "Bumper stuck", - "maint_customer_support_qa": "Contact customer support", - "maint_vacuum_stuck": "Vacuum is stuck", - "maint_vacuum_slip": "Vacuum is stuck", - "maint_left_drop_stuck": "Vacuum is stuck", - "maint_left_wheel_stuck": "Vacuum is stuck", - "maint_right_drop_stuck": "Vacuum is stuck", - "maint_right_wheel_stuck": "Vacuum is stuck", - "not_on_charge_base": "Not on the charge base", - "nav_robot_falling": "Clear my path", - "nav_no_path": "Clear my path", - "nav_path_problem": "Clear my path", - "nav_backdrop_frontbump": "Clear my path", - "nav_backdrop_leftbump": "Clear my path", - "nav_backdrop_wheelextended": "Clear my path", - "nav_mag_sensor": "Clear my path", - "nav_no_exit": "Clear my path", - "nav_no_movement": "Clear my path", - "nav_rightdrop_leftbump": "Clear my path", - "nav_undocking_failed": "Clear my path", -} - -ALERTS = { - "ui_alert_dust_bin_full": "Please empty dust bin", - "ui_alert_recovering_location": "Returning to start", - "ui_alert_battery_chargebasecommerr": "Battery error", - "ui_alert_busy_charging": "Busy charging", - "ui_alert_charging_base": "Base charging", - "ui_alert_charging_power": "Charging power", - "ui_alert_connect_chrg_cable": "Connect charge cable", - "ui_alert_info_thank_you": "Thank you", - "ui_alert_invalid": "Invalid check app", - "ui_alert_old_error": "Old error", - "ui_alert_swupdate_fail": "Update failed", - "dustbin_full": "Please empty dust bin", - "maint_brush_change": "Change the brush", - "maint_filter_change": "Change the filter", - "clean_completed_to_start": "Cleaning completed", - "nav_floorplan_not_created": "No floorplan found", - "nav_floorplan_load_fail": "Failed to load floorplan", - "nav_floorplan_localization_fail": "Failed to load floorplan", - "clean_incomplete_to_start": "Cleaning incomplete", - "log_upload_failed": "Logs failed to upload", -} - - -def setup(hass, config): +async def async_setup(hass, config): """Set up the Neato component.""" + + if NEATO_DOMAIN not in config: + # There is an entry and nothing in configuration.yaml + return True + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + hass.data[NEATO_CONFIG] = config[NEATO_DOMAIN] + + if entries: + # There is an entry and something in the configuration.yaml + entry = entries[0] + conf = config[NEATO_DOMAIN] + if ( + entry.data[CONF_USERNAME] == conf[CONF_USERNAME] + and entry.data[CONF_PASSWORD] == conf[CONF_PASSWORD] + and entry.data[CONF_VENDOR] == conf[CONF_VENDOR] + ): + # The entry is not outdated + return True + + # The entry is outdated + error = await hass.async_add_executor_job( + NeatoConfigFlow.try_login, + conf[CONF_USERNAME], + conf[CONF_PASSWORD], + conf[CONF_VENDOR], + ) + if error is not None: + _LOGGER.error(error) + return False + + # Update the entry + hass.config_entries.async_update_entry(entry, data=config[NEATO_DOMAIN]) + else: + # Create the new entry + hass.async_create_task( + hass.config_entries.flow.async_init( + NEATO_DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[NEATO_DOMAIN], + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up config entry.""" from pybotvac import Account, Neato, Vorwerk - if config[DOMAIN][CONF_VENDOR] == "neato": - hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Neato) - elif config[DOMAIN][CONF_VENDOR] == "vorwerk": - hass.data[NEATO_LOGIN] = NeatoHub(hass, config[DOMAIN], Account, Vorwerk) + if entry.data[CONF_VENDOR] == "neato": + hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Neato) + elif entry.data[CONF_VENDOR] == "vorwerk": + hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Vorwerk) + hub = hass.data[NEATO_LOGIN] - if not hub.login(): + await hass.async_add_executor_job(hub.login) + if not hub.logged_in: _LOGGER.debug("Failed to login to Neato API") return False - hub.update_robots() - for component in ("camera", "vacuum", "switch"): - discovery.load_platform(hass, component, DOMAIN, {}, config) + await hass.async_add_executor_job(hub.update_robots) + for component in ("camera", "vacuum", "switch"): + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass, entry): + """Unload config entry.""" + hass.data.pop(NEATO_LOGIN) + await asyncio.gather( + hass.config_entries.async_forward_entry_unload(entry, "camera"), + hass.config_entries.async_forward_entry_unload(entry, "vacuum"), + hass.config_entries.async_forward_entry_unload(entry, "switch"), + ) return True @@ -202,12 +133,8 @@ class NeatoHub: self._hass = hass self._vendor = vendor - self.my_neato = neato( - domain_config[CONF_USERNAME], domain_config[CONF_PASSWORD], vendor - ) - self._hass.data[NEATO_ROBOTS] = self.my_neato.robots - self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps - self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + self.my_neato = None + self.logged_in = False def login(self): """Login to My Neato.""" @@ -216,10 +143,16 @@ class NeatoHub: self.my_neato = self._neato( self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor ) - return True - except HTTPError: + self.logged_in = True + except (HTTPError, ConnError): _LOGGER.error("Unable to connect to Neato API") - return False + self.logged_in = False + return + + _LOGGER.debug("Successfully connected to Neato API") + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps + self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps @Throttle(timedelta(seconds=300)) def update_robots(self): diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 5d4e0057960..c565fa3d9ac 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -4,21 +4,30 @@ import logging from homeassistant.components.camera import Camera -from . import NEATO_LOGIN, NEATO_MAP_DATA, NEATO_ROBOTS +from .const import NEATO_DOMAIN, NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=10) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Neato Camera.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Neato camera with config entry.""" dev = [] for robot in hass.data[NEATO_ROBOTS]: if "maps" in robot.traits: dev.append(NeatoCleaningMap(hass, robot)) + + if not dev: + return + _LOGGER.debug("Adding robots for cleaning maps %s", dev) - add_entities(dev, True) + async_add_entities(dev, True) class NeatoCleaningMap(Camera): @@ -61,3 +70,8 @@ class NeatoCleaningMap(Camera): def unique_id(self): """Return unique ID.""" return self._robot_serial + + @property + def device_info(self): + """Device info for neato robot.""" + return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py new file mode 100644 index 00000000000..0c71cdbd069 --- /dev/null +++ b/homeassistant/components/neato/config_flow.py @@ -0,0 +1,112 @@ +"""Config flow to configure Neato integration.""" + +import logging + +import voluptuous as vol +from requests.exceptions import HTTPError, ConnectionError as ConnError + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +# pylint: disable=unused-import +from .const import CONF_VENDOR, NEATO_DOMAIN, VALID_VENDORS + + +DOCS_URL = "https://www.home-assistant.io/components/neato" +DEFAULT_VENDOR = "neato" + +_LOGGER = logging.getLogger(__name__) + + +class NeatoConfigFlow(config_entries.ConfigFlow, domain=NEATO_DOMAIN): + """Neato integration config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize flow.""" + self._username = vol.UNDEFINED + self._password = vol.UNDEFINED + self._vendor = vol.UNDEFINED + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + errors = {} + + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + if user_input is not None: + self._username = user_input["username"] + self._password = user_input["password"] + self._vendor = user_input["vendor"] + + error = await self.hass.async_add_executor_job( + self.try_login, self._username, self._password, self._vendor + ) + if error: + errors["base"] = error + else: + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data=user_input, + description_placeholders={"docs_url": DOCS_URL}, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_VENDOR, default="neato"): vol.In(VALID_VENDORS), + } + ), + description_placeholders={"docs_url": DOCS_URL}, + errors=errors, + ) + + async def async_step_import(self, user_input): + """Import a config flow from configuration.""" + + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + vendor = user_input[CONF_VENDOR] + + error = await self.hass.async_add_executor_job( + self.try_login, username, password, vendor + ) + if error is not None: + _LOGGER.error(error) + return self.async_abort(reason=error) + + return self.async_create_entry( + title=f"{username} (from configuration)", + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_VENDOR: vendor, + }, + ) + + @staticmethod + def try_login(username, password, vendor): + """Try logging in to device and return any errors.""" + from pybotvac import Account, Neato, Vorwerk + + this_vendor = None + if vendor == "vorwerk": + this_vendor = Vorwerk() + else: # Neato + this_vendor = Neato() + + try: + Account(username, password, this_vendor) + except (HTTPError, ConnError): + return "invalid_credentials" + + return None diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py new file mode 100644 index 00000000000..6fb41bda710 --- /dev/null +++ b/homeassistant/components/neato/const.py @@ -0,0 +1,150 @@ +"""Constants for Neato integration.""" + +NEATO_DOMAIN = "neato" + +CONF_VENDOR = "vendor" +NEATO_ROBOTS = "neato_robots" +NEATO_LOGIN = "neato_login" +NEATO_CONFIG = "neato_config" +NEATO_MAP_DATA = "neato_map_data" +NEATO_PERSISTENT_MAPS = "neato_persistent_maps" + +VALID_VENDORS = ["neato", "vorwerk"] + +MODE = {1: "Eco", 2: "Turbo"} + +ACTION = { + 0: "Invalid", + 1: "House Cleaning", + 2: "Spot Cleaning", + 3: "Manual Cleaning", + 4: "Docking", + 5: "User Menu Active", + 6: "Suspended Cleaning", + 7: "Updating", + 8: "Copying logs", + 9: "Recovering Location", + 10: "IEC test", + 11: "Map cleaning", + 12: "Exploring map (creating a persistent map)", + 13: "Acquiring Persistent Map IDs", + 14: "Creating & Uploading Map", + 15: "Suspended Exploration", +} + +ERRORS = { + "ui_error_battery_battundervoltlithiumsafety": "Replace battery", + "ui_error_battery_critical": "Replace battery", + "ui_error_battery_invalidsensor": "Replace battery", + "ui_error_battery_lithiumadapterfailure": "Replace battery", + "ui_error_battery_mismatch": "Replace battery", + "ui_error_battery_nothermistor": "Replace battery", + "ui_error_battery_overtemp": "Replace battery", + "ui_error_battery_overvolt": "Replace battery", + "ui_error_battery_undercurrent": "Replace battery", + "ui_error_battery_undertemp": "Replace battery", + "ui_error_battery_undervolt": "Replace battery", + "ui_error_battery_unplugged": "Replace battery", + "ui_error_brush_stuck": "Brush stuck", + "ui_error_brush_overloaded": "Brush overloaded", + "ui_error_bumper_stuck": "Bumper stuck", + "ui_error_check_battery_switch": "Check battery", + "ui_error_corrupt_scb": "Call customer service corrupt board", + "ui_error_deck_debris": "Deck debris", + "ui_error_dflt_app": "Check Neato app", + "ui_error_disconnect_chrg_cable": "Disconnected charge cable", + "ui_error_disconnect_usb_cable": "Disconnected USB cable", + "ui_error_dust_bin_missing": "Dust bin missing", + "ui_error_dust_bin_full": "Dust bin full", + "ui_error_dust_bin_emptied": "Dust bin emptied", + "ui_error_hardware_failure": "Hardware failure", + "ui_error_ldrop_stuck": "Clear my path", + "ui_error_lds_jammed": "Clear my path", + "ui_error_lds_bad_packets": "Check Neato app", + "ui_error_lds_disconnected": "Check Neato app", + "ui_error_lds_missed_packets": "Check Neato app", + "ui_error_lwheel_stuck": "Clear my path", + "ui_error_navigation_backdrop_frontbump": "Clear my path", + "ui_error_navigation_backdrop_leftbump": "Clear my path", + "ui_error_navigation_backdrop_wheelextended": "Clear my path", + "ui_error_navigation_noprogress": "Clear my path", + "ui_error_navigation_origin_unclean": "Clear my path", + "ui_error_navigation_pathproblems": "Cannot return to base", + "ui_error_navigation_pinkycommsfail": "Clear my path", + "ui_error_navigation_falling": "Clear my path", + "ui_error_navigation_noexitstogo": "Clear my path", + "ui_error_navigation_nomotioncommands": "Clear my path", + "ui_error_navigation_rightdrop_leftbump": "Clear my path", + "ui_error_navigation_undockingfailed": "Clear my path", + "ui_error_picked_up": "Picked up", + "ui_error_qa_fail": "Check Neato app", + "ui_error_rdrop_stuck": "Clear my path", + "ui_error_reconnect_failed": "Reconnect failed", + "ui_error_rwheel_stuck": "Clear my path", + "ui_error_stuck": "Stuck!", + "ui_error_unable_to_return_to_base": "Unable to return to base", + "ui_error_unable_to_see": "Clean vacuum sensors", + "ui_error_vacuum_slip": "Clear my path", + "ui_error_vacuum_stuck": "Clear my path", + "ui_error_warning": "Error check app", + "batt_base_connect_fail": "Battery failed to connect to base", + "batt_base_no_power": "Battery base has no power", + "batt_low": "Battery low", + "batt_on_base": "Battery on base", + "clean_tilt_on_start": "Clean the tilt on start", + "dustbin_full": "Dust bin full", + "dustbin_missing": "Dust bin missing", + "gen_picked_up": "Picked up", + "hw_fail": "Hardware failure", + "hw_tof_sensor_sensor": "Hardware sensor disconnected", + "lds_bad_packets": "Bad packets", + "lds_deck_debris": "Debris on deck", + "lds_disconnected": "Disconnected", + "lds_jammed": "Jammed", + "lds_missed_packets": "Missed packets", + "maint_brush_stuck": "Brush stuck", + "maint_brush_overload": "Brush overloaded", + "maint_bumper_stuck": "Bumper stuck", + "maint_customer_support_qa": "Contact customer support", + "maint_vacuum_stuck": "Vacuum is stuck", + "maint_vacuum_slip": "Vacuum is stuck", + "maint_left_drop_stuck": "Vacuum is stuck", + "maint_left_wheel_stuck": "Vacuum is stuck", + "maint_right_drop_stuck": "Vacuum is stuck", + "maint_right_wheel_stuck": "Vacuum is stuck", + "not_on_charge_base": "Not on the charge base", + "nav_robot_falling": "Clear my path", + "nav_no_path": "Clear my path", + "nav_path_problem": "Clear my path", + "nav_backdrop_frontbump": "Clear my path", + "nav_backdrop_leftbump": "Clear my path", + "nav_backdrop_wheelextended": "Clear my path", + "nav_mag_sensor": "Clear my path", + "nav_no_exit": "Clear my path", + "nav_no_movement": "Clear my path", + "nav_rightdrop_leftbump": "Clear my path", + "nav_undocking_failed": "Clear my path", +} + +ALERTS = { + "ui_alert_dust_bin_full": "Please empty dust bin", + "ui_alert_recovering_location": "Returning to start", + "ui_alert_battery_chargebasecommerr": "Battery error", + "ui_alert_busy_charging": "Busy charging", + "ui_alert_charging_base": "Base charging", + "ui_alert_charging_power": "Charging power", + "ui_alert_connect_chrg_cable": "Connect charge cable", + "ui_alert_info_thank_you": "Thank you", + "ui_alert_invalid": "Invalid check app", + "ui_alert_old_error": "Old error", + "ui_alert_swupdate_fail": "Update failed", + "dustbin_full": "Please empty dust bin", + "maint_brush_change": "Change the brush", + "maint_filter_change": "Change the filter", + "clean_completed_to_start": "Cleaning completed", + "nav_floorplan_not_created": "No floorplan found", + "nav_floorplan_load_fail": "Failed to load floorplan", + "nav_floorplan_localization_fail": "Failed to load floorplan", + "clean_incomplete_to_start": "Cleaning incomplete", + "log_upload_failed": "Logs failed to upload", +} diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 8b0c5acc723..160f194cd63 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -1,10 +1,14 @@ { "domain": "neato", "name": "Neato", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", "requirements": [ "pybotvac==0.0.15" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@dshokouhi", + "@Santobert" + ] +} \ No newline at end of file diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json new file mode 100644 index 00000000000..dc13242cc1d --- /dev/null +++ b/homeassistant/components/neato/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "Neato", + "step": { + "user": { + "title": "Neato Account Info", + "data": { + "username": "Username", + "password": "Password", + "vendor": "Vendor" + }, + "description": "See [Neato documentation]({docs_url})." + } + }, + "error": { + "invalid_credentials": "Invalid credentials" + }, + "create_entry": { + "default": "See [Neato documentation]({docs_url})." + }, + "abort": { + "already_configured": "Already configured", + "invalid_credentials": "Invalid credentials" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 539e8cb748c..3efee11853d 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -7,7 +7,7 @@ import requests from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity -from . import NEATO_LOGIN, NEATO_ROBOTS +from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS _LOGGER = logging.getLogger(__name__) @@ -18,14 +18,23 @@ SWITCH_TYPE_SCHEDULE = "schedule" SWITCH_TYPES = {SWITCH_TYPE_SCHEDULE: ["Schedule"]} -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Neato switches.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Neato switch with config entry.""" dev = [] for robot in hass.data[NEATO_ROBOTS]: for type_name in SWITCH_TYPES: dev.append(NeatoConnectedSwitch(hass, robot, type_name)) + + if not dev: + return + _LOGGER.debug("Adding switches %s", dev) - add_entities(dev) + async_add_entities(dev, True) class NeatoConnectedSwitch(ToggleEntity): @@ -37,14 +46,7 @@ class NeatoConnectedSwitch(ToggleEntity): self.robot = robot self.neato = hass.data[NEATO_LOGIN] self._robot_name = "{} {}".format(self.robot.name, SWITCH_TYPES[self.type][0]) - try: - self._state = self.robot.state - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as ex: - _LOGGER.warning("Neato connection error: %s", ex) - self._state = None + self._state = None self._schedule_state = None self._clean_state = None self._robot_serial = self.robot.serial @@ -94,6 +96,11 @@ class NeatoConnectedSwitch(ToggleEntity): return True return False + @property + def device_info(self): + """Device info for neato robot.""" + return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} + def turn_on(self, **kwargs): """Turn the switch on.""" if self.type == SWITCH_TYPE_SCHEDULE: diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index f284b2eda1e..96c4e8f3c5f 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -31,12 +31,13 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import extract_entity_ids -from . import ( +from .const import ( ACTION, ALERTS, ERRORS, MODE, NEATO_LOGIN, + NEATO_DOMAIN, NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, @@ -83,8 +84,13 @@ SERVICE_NEATO_CUSTOM_CLEANING_SCHEMA = vol.Schema( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Neato vacuum.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up Neato vacuum with config entry.""" dev = [] for robot in hass.data[NEATO_ROBOTS]: dev.append(NeatoConnectedVacuum(hass, robot)) @@ -93,7 +99,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return _LOGGER.debug("Adding vacuums %s", dev) - add_entities(dev, True) + async_add_entities(dev, True) def neato_custom_cleaning_service(call): """Zone cleaning service that allows user to change options.""" @@ -111,7 +117,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entities = [entity for entity in dev if entity.entity_id in entity_ids] return entities - hass.services.register( + hass.services.async_register( DOMAIN, SERVICE_NEATO_CUSTOM_CLEANING, neato_custom_cleaning_service, @@ -144,10 +150,14 @@ class NeatoConnectedVacuum(StateVacuumDevice): self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS] self._robot_boundaries = {} self._robot_has_map = self.robot.has_persistent_maps + self._robot_stats = None def update(self): """Update the states of Neato Vacuums.""" _LOGGER.debug("Running Neato Vacuums update") + if self._robot_stats is None: + self._robot_stats = self.robot.get_robot_info().json() + self.neato.update_robots() try: self._state = self.robot.state @@ -290,6 +300,17 @@ class NeatoConnectedVacuum(StateVacuumDevice): return data + @property + def device_info(self): + """Device info for neato robot.""" + return { + "identifiers": {(NEATO_DOMAIN, self._robot_serial)}, + "name": self._name, + "manufacturer": self._robot_stats["data"]["mfg_name"], + "model": self._robot_stats["data"]["modelName"], + "sw_version": self._state["meta"]["firmware"], + } + def start(self): """Start cleaning or resume cleaning.""" if self._state["state"] == 1: diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1eb08709741..4a4effc36ce 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -43,6 +43,7 @@ FLOWS = [ "met", "mobile_app", "mqtt", + "neato", "nest", "notion", "opentherm_gw", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60780ec7c55..8627ddb0d86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -296,6 +296,9 @@ pyMetno==0.4.6 # homeassistant.components.blackbird pyblackbird==0.5 +# homeassistant.components.neato +pybotvac==0.0.15 + # homeassistant.components.cast pychromecast==4.0.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 70c81c66025..3c0941fc887 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -122,6 +122,7 @@ TEST_REQUIREMENTS = ( "py-canary", "py17track", "pyblackbird", + "pybotvac", "pychromecast", "pydeconz", "pydispatcher", diff --git a/tests/components/neato/__init__.py b/tests/components/neato/__init__.py new file mode 100644 index 00000000000..7927918395c --- /dev/null +++ b/tests/components/neato/__init__.py @@ -0,0 +1 @@ +"""Tests for the Neato component.""" diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py new file mode 100644 index 00000000000..99691c101a6 --- /dev/null +++ b/tests/components/neato/test_config_flow.py @@ -0,0 +1,129 @@ +"""Tests for the Neato config flow.""" +import pytest +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components.neato import config_flow +from homeassistant.components.neato.const import NEATO_DOMAIN, CONF_VENDOR +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +USERNAME = "myUsername" +PASSWORD = "myPassword" +VENDOR_NEATO = "neato" +VENDOR_VORWERK = "vorwerk" +VENDOR_INVALID = "invalid" + + +@pytest.fixture(name="account") +def mock_controller_login(): + """Mock a successful login.""" + with patch("pybotvac.Account", return_value=True): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.NeatoConfigFlow() + flow.hass = hass + return flow + + +async def test_user(hass, account): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_VENDOR] == VENDOR_NEATO + + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_VORWERK} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USERNAME + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_VENDOR] == VENDOR_VORWERK + + +async def test_import(hass, account): + """Test import step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{USERNAME} (from configuration)" + assert result["data"][CONF_USERNAME] == USERNAME + assert result["data"][CONF_PASSWORD] == PASSWORD + assert result["data"][CONF_VENDOR] == VENDOR_NEATO + + +async def test_abort_if_already_setup(hass, account): + """Test we abort if Neato is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain=NEATO_DOMAIN, + data={ + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + }, + ).add_to_hass(hass) + + # Should fail, same USERNAME (import) + result = await flow.async_step_import( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same USERNAME (flow) + result = await flow.async_step_user( + {CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, CONF_VENDOR: VENDOR_NEATO} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_abort_on_invalid_credentials(hass): + """Test when we have invalid credentials.""" + from requests.exceptions import HTTPError + + flow = init_config_flow(hass) + + with patch("pybotvac.Account", side_effect=HTTPError()): + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_credentials"} + + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "invalid_credentials" diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py new file mode 100644 index 00000000000..be7e43fdc0a --- /dev/null +++ b/tests/components/neato/test_init.py @@ -0,0 +1,70 @@ +"""Tests for the Neato init file.""" +import pytest +from unittest.mock import patch + +from homeassistant.components.neato.const import NEATO_DOMAIN, CONF_VENDOR +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +USERNAME = "myUsername" +PASSWORD = "myPassword" +VENDOR_NEATO = "neato" +VENDOR_VORWERK = "vorwerk" +VENDOR_INVALID = "invalid" + +VALID_CONFIG = { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, +} + +INVALID_CONFIG = { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_INVALID, +} + + +@pytest.fixture(name="account") +def mock_controller_login(): + """Mock a successful login.""" + with patch("pybotvac.Account", return_value=True): + yield + + +async def test_no_config_entry(hass): + """There is nothing in configuration.yaml.""" + res = await async_setup_component(hass, NEATO_DOMAIN, {}) + assert res is True + + +async def test_config_entries_in_sync(hass, account): + """The config entry and configuration.yaml are in sync.""" + MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) + + assert hass.config_entries.async_entries(NEATO_DOMAIN) + assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO + + +async def test_config_entries_not_in_sync(hass, account): + """The config entry and configuration.yaml are not in sync.""" + MockConfigEntry(domain=NEATO_DOMAIN, data=INVALID_CONFIG).add_to_hass(hass) + + assert hass.config_entries.async_entries(NEATO_DOMAIN) + assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO From 1ecc883ef46120d569e39c288a0fd3cc4b35f8e7 Mon Sep 17 00:00:00 2001 From: ktnrg45 <38207570+ktnrg45@users.noreply.github.com> Date: Sun, 6 Oct 2019 05:43:34 -0700 Subject: [PATCH 059/639] PS4 bump to renamed dependency (#27144) * Change to renamed dependency pyps4-2ndscreen 0.9.0 * Rename / bump to ps4 dependency to 1.0.0 * update requirements * Rename test req * Fix import * Bump 1.0.1 * Fix flaky test leaving files behind --- homeassistant/components/ps4/__init__.py | 12 ++-- homeassistant/components/ps4/config_flow.py | 9 +-- homeassistant/components/ps4/manifest.json | 2 +- homeassistant/components/ps4/media_player.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/gen_requirements_all.py | 2 +- tests/components/ps4/test_config_flow.py | 63 ++++++++++---------- tests/components/ps4/test_media_player.py | 24 ++++---- 9 files changed, 56 insertions(+), 66 deletions(-) diff --git a/homeassistant/components/ps4/__init__.py b/homeassistant/components/ps4/__init__.py index 60635bba525..205059be608 100644 --- a/homeassistant/components/ps4/__init__.py +++ b/homeassistant/components/ps4/__init__.py @@ -3,8 +3,8 @@ import logging import os import voluptuous as vol -from pyps4_homeassistant.ddp import async_create_ddp_endpoint -from pyps4_homeassistant.media_art import COUNTRIES +from pyps4_2ndscreen.ddp import async_create_ddp_endpoint +from pyps4_2ndscreen.media_art import COUNTRIES from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_TYPE, @@ -172,12 +172,8 @@ def load_games(hass: HomeAssistantType) -> dict: _LOGGER.error("Games file was not parsed correctly") games = {} - # If file does not exist, create empty file. - if not os.path.isfile(g_file): - _LOGGER.info("Creating PS4 Games File") - games = {} - save_games(hass, games) - else: + # If file exists + if os.path.isfile(g_file): games = _reformat_data(hass, games) return games diff --git a/homeassistant/components/ps4/config_flow.py b/homeassistant/components/ps4/config_flow.py index a4b74077793..44523aea85a 100644 --- a/homeassistant/components/ps4/config_flow.py +++ b/homeassistant/components/ps4/config_flow.py @@ -2,6 +2,9 @@ from collections import OrderedDict import logging +from pyps4_2ndscreen.errors import CredentialTimeout +from pyps4_2ndscreen.helpers import Helper +from pyps4_2ndscreen.media_art import COUNTRIES import voluptuous as vol from homeassistant import config_entries @@ -37,8 +40,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): def __init__(self): """Initialize the config flow.""" - from pyps4_homeassistant import Helper - self.helper = Helper() self.creds = None self.name = None @@ -61,8 +62,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): async def async_step_creds(self, user_input=None): """Return PS4 credentials from 2nd Screen App.""" - from pyps4_homeassistant.errors import CredentialTimeout - errors = {} if user_input is not None: try: @@ -103,8 +102,6 @@ class PlayStation4FlowHandler(config_entries.ConfigFlow): async def async_step_link(self, user_input=None): """Prompt user input. Create or edit entry.""" - from pyps4_homeassistant.media_art import COUNTRIES - regions = sorted(COUNTRIES.keys()) default_region = None errors = {} diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 98a14d877e8..361711c400c 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", "requirements": [ - "pyps4-homeassistant==0.8.7" + "pyps4-2ndscreen==1.0.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index e1ec32ddd1f..3e8b667cd13 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -2,8 +2,8 @@ import logging import asyncio -import pyps4_homeassistant.ps4 as pyps4 -from pyps4_homeassistant.errors import NotReady +import pyps4_2ndscreen.ps4 as pyps4 +from pyps4_2ndscreen.errors import NotReady from homeassistant.core import callback from homeassistant.components.media_player import ENTITY_IMAGE_URL, MediaPlayerDevice @@ -254,7 +254,7 @@ class PS4Device(MediaPlayerDevice): async def async_get_title_data(self, title_id, name): """Get PS Store Data.""" - from pyps4_homeassistant.errors import PSDataIncomplete + from pyps4_2ndscreen.errors import PSDataIncomplete app_name = None art = None diff --git a/requirements_all.txt b/requirements_all.txt index c2c2c96a26f..39d4ddcda62 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1396,7 +1396,7 @@ pypjlink2==1.2.0 pypoint==1.1.1 # homeassistant.components.ps4 -pyps4-homeassistant==0.8.7 +pyps4-2ndscreen==1.0.1 # homeassistant.components.qwikswitch pyqwikswitch==0.93 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8627ddb0d86..187e50c4691 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -347,7 +347,7 @@ pyotgw==0.5b0 pyotp==2.3.0 # homeassistant.components.ps4 -pyps4-homeassistant==0.8.7 +pyps4-2ndscreen==1.0.1 # homeassistant.components.qwikswitch pyqwikswitch==0.93 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 3c0941fc887..61174e86a43 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -141,7 +141,7 @@ TEST_REQUIREMENTS = ( "pyopenuv", "pyotgw", "pyotp", - "pyps4-homeassistant", + "pyps4-2ndscreen", "pyqwikswitch", "PyRMVtransport", "pysma", diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 42f319e7343..81f81093a67 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -1,6 +1,8 @@ """Define tests for the PlayStation 4 config flow.""" from unittest.mock import patch +from pyps4_2ndscreen.errors import CredentialTimeout + from homeassistant import data_entry_flow from homeassistant.components import ps4 from homeassistant.components.ps4.const import DEFAULT_NAME, DEFAULT_REGION @@ -73,28 +75,28 @@ async def test_full_flow_implementation(hass): manager = hass.config_entries # User Step Started, results in Step Creds - with patch("pyps4_homeassistant.Helper.port_bind", return_value=None): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. - with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_mode(MOCK_AUTO) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "link" # User Input results in created entry. - with patch("pyps4_homeassistant.Helper.link", return_value=(True, True)), patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(MOCK_CONFIG) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -126,20 +128,20 @@ async def test_multiple_flow_implementation(hass): manager = hass.config_entries # User Step Started, results in Step Creds - with patch("pyps4_homeassistant.Helper.port_bind", return_value=None): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None): result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. - with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_mode(MOCK_AUTO) @@ -147,8 +149,8 @@ async def test_multiple_flow_implementation(hass): assert result["step_id"] == "link" # User Input results in created entry. - with patch("pyps4_homeassistant.Helper.link", return_value=(True, True)), patch( - "pyps4_homeassistant.Helper.has_devices", + with patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_link(MOCK_CONFIG) @@ -175,8 +177,8 @@ async def test_multiple_flow_implementation(hass): # Test additional flow. # User Step Started, results in Step Mode: - with patch("pyps4_homeassistant.Helper.port_bind", return_value=None), patch( - "pyps4_homeassistant.Helper.has_devices", + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=None), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_user() @@ -184,14 +186,14 @@ async def test_multiple_flow_implementation(hass): assert result["step_id"] == "creds" # Step Creds results with form in Step Mode. - with patch("pyps4_homeassistant.Helper.get_creds", return_value=MOCK_CREDS): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=MOCK_CREDS): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "mode" # Step Mode with User Input which is not manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], ): result = await flow.async_step_mode(MOCK_AUTO) @@ -200,9 +202,9 @@ async def test_multiple_flow_implementation(hass): # Step Link with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], - ), patch("pyps4_homeassistant.Helper.link", return_value=(True, True)): + ), patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)): result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS @@ -232,13 +234,13 @@ async def test_port_bind_abort(hass): flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.port_bind", return_value=MOCK_UDP_PORT): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_UDP_PORT): reason = "port_987_bind_error" result = await flow.async_step_user(user_input=None) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == reason - with patch("pyps4_homeassistant.Helper.port_bind", return_value=MOCK_TCP_PORT): + with patch("pyps4_2ndscreen.Helper.port_bind", return_value=MOCK_TCP_PORT): reason = "port_997_bind_error" result = await flow.async_step_user(user_input=None) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -253,7 +255,7 @@ async def test_duplicate_abort(hass): flow.creds = MOCK_CREDS with patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(user_input=None) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -274,9 +276,9 @@ async def test_additional_device(hass): assert len(manager.async_entries()) == 1 with patch( - "pyps4_homeassistant.Helper.has_devices", + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}, {"host-ip": MOCK_HOST_ADDITIONAL}], - ), patch("pyps4_homeassistant.Helper.link", return_value=(True, True)): + ), patch("pyps4_2ndscreen.Helper.link", return_value=(True, True)): result = await flow.async_step_link(MOCK_CONFIG_ADDITIONAL) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"][CONF_TOKEN] == MOCK_CREDS @@ -296,7 +298,7 @@ async def test_no_devices_found_abort(hass): flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.has_devices", return_value=[]): + with patch("pyps4_2ndscreen.Helper.has_devices", return_value=[]): result = await flow.async_step_link() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "no_devices_found" @@ -310,8 +312,7 @@ async def test_manual_mode(hass): # Step Mode with User Input: manual, results in Step Link. with patch( - "pyps4_homeassistant.Helper.has_devices", - return_value=[{"host-ip": flow.m_device}], + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": flow.m_device}] ): result = await flow.async_step_mode(MOCK_MANUAL) assert flow.m_device == MOCK_HOST @@ -324,7 +325,7 @@ async def test_credential_abort(hass): flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.get_creds", return_value=None): + with patch("pyps4_2ndscreen.Helper.get_creds", return_value=None): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "credential_error" @@ -332,12 +333,10 @@ async def test_credential_abort(hass): async def test_credential_timeout(hass): """Test that Credential Timeout shows error.""" - from pyps4_homeassistant.errors import CredentialTimeout - flow = ps4.PlayStation4FlowHandler() flow.hass = hass - with patch("pyps4_homeassistant.Helper.get_creds", side_effect=CredentialTimeout): + with patch("pyps4_2ndscreen.Helper.get_creds", side_effect=CredentialTimeout): result = await flow.async_step_creds({}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "credential_timeout"} @@ -349,8 +348,8 @@ async def test_wrong_pin_error(hass): flow.hass = hass flow.location = MOCK_LOCATION - with patch("pyps4_homeassistant.Helper.link", return_value=(True, False)), patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + with patch("pyps4_2ndscreen.Helper.link", return_value=(True, False)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(MOCK_CONFIG) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -364,8 +363,8 @@ async def test_device_connection_error(hass): flow.hass = hass flow.location = MOCK_LOCATION - with patch("pyps4_homeassistant.Helper.link", return_value=(False, True)), patch( - "pyps4_homeassistant.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] + with patch("pyps4_2ndscreen.Helper.link", return_value=(False, True)), patch( + "pyps4_2ndscreen.Helper.has_devices", return_value=[{"host-ip": MOCK_HOST}] ): result = await flow.async_step_link(MOCK_CONFIG) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index e4f2033c3cb..d6eeb31695c 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -1,7 +1,7 @@ """Tests for the PS4 media player platform.""" from unittest.mock import MagicMock, patch -from pyps4_homeassistant.credential import get_ddp_message +from pyps4_2ndscreen.credential import get_ddp_message from homeassistant.components import ps4 from homeassistant.components.media_player.const import ( @@ -295,9 +295,7 @@ async def test_media_attributes_are_loaded(hass): async def test_device_info_is_set_from_status_correctly(hass): """Test that device info is set correctly from status update.""" mock_d_registry = mock_device_registry(hass) - with patch( - "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_OFF - ), patch(MOCK_SAVE, side_effect=MagicMock()): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_OFF): mock_entity_id = await setup_mock_component(hass) await hass.async_block_till_done() @@ -447,9 +445,9 @@ async def test_media_stop(hass): async def test_select_source(hass): """Test that select source service calls function with title.""" mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch( - "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE - ), patch(MOCK_LOAD, return_value=mock_data): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( + MOCK_LOAD, return_value=mock_data + ): mock_entity_id = await setup_mock_component(hass) mock_func = "{}{}".format( @@ -473,9 +471,9 @@ async def test_select_source(hass): async def test_select_source_caps(hass): """Test that select source service calls function with upper case title.""" mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch( - "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE - ), patch(MOCK_LOAD, return_value=mock_data): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( + MOCK_LOAD, return_value=mock_data + ): mock_entity_id = await setup_mock_component(hass) mock_func = "{}{}".format( @@ -502,9 +500,9 @@ async def test_select_source_caps(hass): async def test_select_source_id(hass): """Test that select source service calls function with Title ID.""" mock_data = {MOCK_TITLE_ID: MOCK_GAMES_DATA} - with patch( - "pyps4_homeassistant.ps4.get_status", return_value=MOCK_STATUS_IDLE - ), patch(MOCK_LOAD, return_value=mock_data): + with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_IDLE), patch( + MOCK_LOAD, return_value=mock_data + ): mock_entity_id = await setup_mock_component(hass) mock_func = "{}{}".format( From 6cc71db385ce9c9cf9bac32624568ee5658b92b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Sun, 6 Oct 2019 17:00:44 +0200 Subject: [PATCH 060/639] Fix onvif PTZ service freeze (#27250) --- homeassistant/components/onvif/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 0c116568780..29af1049fae 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -23,7 +23,7 @@ from homeassistant.components.camera.const import DOMAIN from homeassistant.components.ffmpeg import DATA_FFMPEG, CONF_EXTRA_ARGUMENTS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream -from homeassistant.helpers.service import extract_entity_ids +from homeassistant.helpers.service import async_extract_entity_ids import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -88,7 +88,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= tilt = service.data.get(ATTR_TILT, None) zoom = service.data.get(ATTR_ZOOM, None) all_cameras = hass.data[ONVIF_DATA][ENTITIES] - entity_ids = extract_entity_ids(hass, service) + entity_ids = await async_extract_entity_ids(hass, service) target_cameras = [] if not entity_ids: target_cameras = all_cameras From c7c88b2b68d22d82458599fb3c9c4377946d0412 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sun, 6 Oct 2019 17:17:40 +0200 Subject: [PATCH 061/639] UniFi - Bandwidth sensors (#27229) * First draft of UniFi bandwidth sensors * Clean up * Add tests for sensors --- .../components/unifi/.translations/en.json | 5 + homeassistant/components/unifi/config_flow.py | 31 ++- homeassistant/components/unifi/const.py | 2 + homeassistant/components/unifi/controller.py | 15 +- homeassistant/components/unifi/sensor.py | 168 ++++++++++++++ homeassistant/components/unifi/strings.json | 5 + homeassistant/components/unifi/switch.py | 3 - tests/components/unifi/test_controller.py | 16 +- tests/components/unifi/test_sensor.py | 207 ++++++++++++++++++ tests/components/unifi/test_switch.py | 4 +- 10 files changed, 444 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/unifi/sensor.py create mode 100644 tests/components/unifi/test_sensor.py diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 2025bad6246..d9b65b6d1da 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -32,6 +32,11 @@ "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients" + } } } } diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index fdb75d09194..92281837f48 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -12,6 +12,7 @@ from homeassistant.const import ( ) from .const import ( + CONF_ALLOW_BANDWIDTH_SENSORS, CONF_CONTROLLER, CONF_DETECTION_TIME, CONF_SITE_ID, @@ -19,6 +20,7 @@ from .const import ( CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, CONTROLLER_ID, + DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, DEFAULT_TRACK_WIRED_CLIENTS, @@ -171,6 +173,7 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): def __init__(self, config_entry): """Initialize UniFi options flow.""" self.config_entry = config_entry + self.options = dict(config_entry.options) async def async_step_init(self, user_input=None): """Manage the UniFi options.""" @@ -179,7 +182,8 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): async def async_step_device_tracker(self, user_input=None): """Manage the device tracker options.""" if user_input is not None: - return self.async_create_entry(title="", data=user_input) + self.options.update(user_input) + return await self.async_step_statistics_sensors() return self.async_show_form( step_id="device_tracker", @@ -212,3 +216,28 @@ class UnifiOptionsFlowHandler(config_entries.OptionsFlow): } ), ) + + async def async_step_statistics_sensors(self, user_input=None): + """Manage the statistics sensors options.""" + if user_input is not None: + self.options.update(user_input) + return await self._update_options() + + return self.async_show_form( + step_id="statistics_sensors", + data_schema=vol.Schema( + { + vol.Optional( + CONF_ALLOW_BANDWIDTH_SENSORS, + default=self.config_entry.options.get( + CONF_ALLOW_BANDWIDTH_SENSORS, + DEFAULT_ALLOW_BANDWIDTH_SENSORS, + ), + ): bool + } + ), + ) + + async def _update_options(self): + """Update config entry options.""" + return self.async_create_entry(title="", data=self.options) diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index eac14735074..d82b7b49d45 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -12,6 +12,7 @@ CONF_SITE_ID = "site" UNIFI_CONFIG = "unifi_config" UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients" +CONF_ALLOW_BANDWIDTH_SENSORS = "allow_bandwidth_sensors" CONF_BLOCK_CLIENT = "block_client" CONF_DETECTION_TIME = "detection_time" CONF_TRACK_CLIENTS = "track_clients" @@ -23,6 +24,7 @@ CONF_DONT_TRACK_CLIENTS = "dont_track_clients" CONF_DONT_TRACK_DEVICES = "dont_track_devices" CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" +DEFAULT_ALLOW_BANDWIDTH_SENSORS = False DEFAULT_BLOCK_CLIENTS = [] DEFAULT_TRACK_CLIENTS = True DEFAULT_TRACK_DEVICES = True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index ffea98b9050..fa1164166bd 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -15,6 +15,7 @@ from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( + CONF_ALLOW_BANDWIDTH_SENSORS, CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, @@ -27,6 +28,7 @@ from .const import ( CONF_SITE_ID, CONF_SSID_FILTER, CONTROLLER_ID, + DEFAULT_ALLOW_BANDWIDTH_SENSORS, DEFAULT_BLOCK_CLIENTS, DEFAULT_TRACK_CLIENTS, DEFAULT_TRACK_DEVICES, @@ -40,6 +42,8 @@ from .const import ( ) from .errors import AuthenticationRequired, CannotConnect +SUPPORTED_PLATFORMS = ["device_tracker", "sensor", "switch"] + class UniFiController: """Manages a single UniFi Controller.""" @@ -76,6 +80,13 @@ class UniFiController: """Return the site user role of this controller.""" return self._site_role + @property + def option_allow_bandwidth_sensors(self): + """Config entry option to allow bandwidth sensors.""" + return self.config_entry.options.get( + CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS + ) + @property def option_block_clients(self): """Config entry option with list of clients to control network access.""" @@ -225,7 +236,7 @@ class UniFiController: self.config_entry.add_update_listener(self.async_options_updated) - for platform in ["device_tracker", "switch"]: + for platform in SUPPORTED_PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup( self.config_entry, platform @@ -294,7 +305,7 @@ class UniFiController: if self.api is None: return True - for platform in ["device_tracker", "switch"]: + for platform in SUPPORTED_PLATFORMS: await self.hass.config_entries.async_forward_entry_unload( self.config_entry, platform ) diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py new file mode 100644 index 00000000000..aad013970d1 --- /dev/null +++ b/homeassistant/components/unifi/sensor.py @@ -0,0 +1,168 @@ +"""Support for bandwidth sensors with UniFi clients.""" +import logging + +from homeassistant.components.unifi.config_flow import get_controller_from_config_entry +from homeassistant.core import callback +from homeassistant.helpers import entity_registry +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_registry import DISABLED_CONFIG_ENTRY + +LOGGER = logging.getLogger(__name__) + +ATTR_RECEIVING = "receiving" +ATTR_TRANSMITTING = "transmitting" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Sensor platform doesn't support configuration through configuration.yaml.""" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensors for UniFi integration.""" + controller = get_controller_from_config_entry(hass, config_entry) + sensors = {} + + registry = await entity_registry.async_get_registry(hass) + + @callback + def update_controller(): + """Update the values of the controller.""" + update_items(controller, async_add_entities, sensors) + + async_dispatcher_connect(hass, controller.signal_update, update_controller) + + @callback + def update_disable_on_entities(): + """Update the values of the controller.""" + for entity in sensors.values(): + + disabled_by = None + if not entity.entity_registry_enabled_default and entity.enabled: + disabled_by = DISABLED_CONFIG_ENTRY + + registry.async_update_entity( + entity.registry_entry.entity_id, disabled_by=disabled_by + ) + + async_dispatcher_connect( + hass, controller.signal_options_update, update_disable_on_entities + ) + + update_controller() + + +@callback +def update_items(controller, async_add_entities, sensors): + """Update sensors from the controller.""" + new_sensors = [] + + for client_id in controller.api.clients: + for direction, sensor_class in ( + ("rx", UniFiRxBandwidthSensor), + ("tx", UniFiTxBandwidthSensor), + ): + item_id = f"{direction}-{client_id}" + + if item_id in sensors: + sensor = sensors[item_id] + if sensor.enabled: + sensor.async_schedule_update_ha_state() + continue + + sensors[item_id] = sensor_class( + controller.api.clients[client_id], controller + ) + new_sensors.append(sensors[item_id]) + + if new_sensors: + async_add_entities(new_sensors) + + +class UniFiBandwidthSensor(Entity): + """UniFi Bandwidth sensor base class.""" + + def __init__(self, client, controller): + """Set up client.""" + self.client = client + self.controller = controller + self.is_wired = self.client.mac not in controller.wireless_clients + + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + if self.controller.option_allow_bandwidth_sensors: + return True + return False + + async def async_added_to_hass(self): + """Client entity created.""" + LOGGER.debug("New UniFi bandwidth sensor %s (%s)", self.name, self.client.mac) + + async def async_update(self): + """Synchronize state with controller. + + Make sure to update self.is_wired if client is wireless, there is an issue when clients go offline that they get marked as wired. + """ + LOGGER.debug( + "Updating UniFi bandwidth sensor %s (%s)", self.entity_id, self.client.mac + ) + await self.controller.request_update() + + if self.is_wired and self.client.mac in self.controller.wireless_clients: + self.is_wired = False + + @property + def available(self) -> bool: + """Return if controller is available.""" + return self.controller.available + + @property + def device_info(self): + """Return a device description for device registry.""" + return {"connections": {(CONNECTION_NETWORK_MAC, self.client.mac)}} + + +class UniFiRxBandwidthSensor(UniFiBandwidthSensor): + """Receiving bandwidth sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self.is_wired: + return self.client.wired_rx_bytes / 1000000 + return self.client.raw.get("rx_bytes", 0) / 1000000 + + @property + def name(self): + """Return the name of the client.""" + name = self.client.name or self.client.hostname + return f"{name} RX" + + @property + def unique_id(self): + """Return a unique identifier for this bandwidth sensor.""" + return f"rx-{self.client.mac}" + + +class UniFiTxBandwidthSensor(UniFiBandwidthSensor): + """Transmitting bandwidth sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + if self.is_wired: + return self.client.wired_tx_bytes / 1000000 + return self.client.raw.get("tx_bytes", 0) / 1000000 + + @property + def name(self): + """Return the name of the client.""" + name = self.client.name or self.client.hostname + return f"{name} TX" + + @property + def unique_id(self): + """Return a unique identifier for this bandwidth sensor.""" + return f"tx-{self.client.mac}" diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index c484bfbf09f..ce2f2345917 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -35,6 +35,11 @@ "track_devices": "Track network devices (Ubiquiti devices)", "track_wired_clients": "Include wired network clients" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Create bandwidth usage sensors for network clients" + } } } } diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index f0183a7ecb3..f8fad6dac8e 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -14,7 +14,6 @@ LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Component doesn't support configuration through configuration.yaml.""" - pass async def async_setup_entry(hass, config_entry, async_add_entities): @@ -231,8 +230,6 @@ class UniFiPOEClientSwitch(UniFiClient, SwitchDevice, RestoreEntity): """Return the device state attributes.""" attributes = { "power": self.port.poe_power, - "received": self.client.wired_rx_bytes / 1000000, - "sent": self.client.wired_tx_bytes / 1000000, "switch": self.client.sw_mac, "port": self.client.sw_port, "poe_mode": self.poe_mode, diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index e73719205f7..ae6f3776b4f 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -67,12 +67,18 @@ async def test_controller_setup(): assert await unifi_controller.async_setup() is True assert unifi_controller.api is api - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 2 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == len( + controller.SUPPORTED_PLATFORMS + ) assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == ( entry, "device_tracker", ) assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == ( + entry, + "sensor", + ) + assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == ( entry, "switch", ) @@ -214,12 +220,16 @@ async def test_reset_unloads_entry_if_setup(): with patch.object(controller, "get_controller", return_value=mock_coro(api)): assert await unifi_controller.async_setup() is True - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 2 + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == len( + controller.SUPPORTED_PLATFORMS + ) hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) assert await unifi_controller.async_reset() - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 2 + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == len( + controller.SUPPORTED_PLATFORMS + ) async def test_get_controller(hass): diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py new file mode 100644 index 00000000000..9064f1c9aba --- /dev/null +++ b/tests/components/unifi/test_sensor.py @@ -0,0 +1,207 @@ +"""UniFi sensor platform tests.""" +from collections import deque +from copy import deepcopy + +from asynctest import patch + +from homeassistant import config_entries +from homeassistant.components import unifi +from homeassistant.components.unifi.const import ( + CONF_CONTROLLER, + CONF_SITE_ID, + CONTROLLER_ID as CONF_CONTROLLER_ID, + UNIFI_CONFIG, + UNIFI_WIRELESS_CLIENTS, +) +from homeassistant.setup import async_setup_component +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +import homeassistant.components.sensor as sensor + +CLIENTS = [ + { + "hostname": "Wired client hostname", + "ip": "10.0.0.1", + "is_wired": True, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:01", + "name": "Wired client name", + "oui": "Producer", + "sw_mac": "00:00:00:00:01:01", + "sw_port": 1, + "wired-rx_bytes": 1234000000, + "wired-tx_bytes": 5678000000, + }, + { + "hostname": "Wireless client hostname", + "ip": "10.0.0.2", + "is_wired": False, + "last_seen": 1562600145, + "mac": "00:00:00:00:00:02", + "name": "Wireless client name", + "oui": "Producer", + "sw_mac": "00:00:00:00:01:01", + "sw_port": 2, + "rx_bytes": 1234000000, + "tx_bytes": 5678000000, + }, +] + +CONTROLLER_DATA = { + CONF_HOST: "mock-host", + CONF_USERNAME: "mock-user", + CONF_PASSWORD: "mock-pswd", + CONF_PORT: 1234, + CONF_SITE_ID: "mock-site", + CONF_VERIFY_SSL: False, +} + +ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} + +CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") + +SITES = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "admin"}} + + +async def setup_unifi_integration( + hass, + config, + options, + sites, + clients_response, + devices_response, + clients_all_response, +): + """Create the UniFi controller.""" + hass.data[UNIFI_CONFIG] = [] + hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) + config_entry = config_entries.ConfigEntry( + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, + options=options, + entry_id=1, + ) + + mock_client_responses = deque() + mock_client_responses.append(clients_response) + + mock_device_responses = deque() + mock_device_responses.append(devices_response) + + mock_client_all_responses = deque() + mock_client_all_responses.append(clients_all_response) + + mock_requests = [] + + async def mock_request(self, method, path, json=None): + mock_requests.append({"method": method, "path": path, "json": json}) + + if path == "s/{site}/stat/sta" and mock_client_responses: + return mock_client_responses.popleft() + if path == "s/{site}/stat/device" and mock_device_responses: + return mock_device_responses.popleft() + if path == "s/{site}/rest/user" and mock_client_all_responses: + return mock_client_all_responses.popleft() + return {} + + with patch("aiounifi.Controller.login", return_value=True), patch( + "aiounifi.Controller.sites", return_value=sites + ), patch("aiounifi.Controller.request", new=mock_request): + await unifi.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + controller_id = unifi.get_controller_id_from_config_entry(config_entry) + controller = hass.data[unifi.DOMAIN][controller_id] + + controller.mock_client_responses = mock_client_responses + controller.mock_device_responses = mock_device_responses + controller.mock_client_all_responses = mock_client_all_responses + controller.mock_requests = mock_requests + + return controller + + +async def test_platform_manually_configured(hass): + """Test that we do not discover anything or try to set up a controller.""" + assert ( + await async_setup_component( + hass, sensor.DOMAIN, {sensor.DOMAIN: {"platform": "unifi"}} + ) + is True + ) + assert unifi.DOMAIN not in hass.data + + +async def test_no_clients(hass): + """Test the update_clients function when no clients are found.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 2 + + +async def test_switches(hass): + """Test the update_items function with some clients.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True, + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=CLIENTS, + devices_response=[], + clients_all_response=[], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 6 + + wired_client_rx = hass.states.get("sensor.wired_client_name_rx") + assert wired_client_rx.state == "1234.0" + + wired_client_tx = hass.states.get("sensor.wired_client_name_tx") + assert wired_client_tx.state == "5678.0" + + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") + assert wireless_client_rx.state == "1234.0" + + wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") + assert wireless_client_tx.state == "5678.0" + + clients = deepcopy(CLIENTS) + clients[0]["is_wired"] = False + clients[1]["rx_bytes"] = 2345000000 + clients[1]["tx_bytes"] = 6789000000 + + controller.mock_client_responses.append(clients) + await controller.async_update() + await hass.async_block_till_done() + + wireless_client_rx = hass.states.get("sensor.wireless_client_name_rx") + assert wireless_client_rx.state == "2345.0" + + wireless_client_tx = hass.states.get("sensor.wireless_client_name_tx") + assert wireless_client_tx.state == "6789.0" diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 56a96b2b5b2..97dda441527 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -264,7 +264,7 @@ async def setup_unifi_integration( async def mock_request(self, method, path, json=None): mock_requests.append({"method": method, "path": path, "json": json}) - print(mock_requests, mock_client_responses, mock_device_responses) + if path == "s/{site}/stat/sta" and mock_client_responses: return mock_client_responses.popleft() if path == "s/{site}/stat/device" and mock_device_responses: @@ -386,8 +386,6 @@ async def test_switches(hass): assert switch_1 is not None assert switch_1.state == "on" assert switch_1.attributes["power"] == "2.56" - assert switch_1.attributes["received"] == 1234 - assert switch_1.attributes["sent"] == 5678 assert switch_1.attributes["switch"] == "00:00:00:00:01:01" assert switch_1.attributes["port"] == 1 assert switch_1.attributes["poe_mode"] == "auto" From 7a156059e9ef154a875518738951ca60e25d026b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robbert=20M=C3=BCller?= Date: Sun, 6 Oct 2019 17:23:12 +0200 Subject: [PATCH 062/639] Switch on/off all lights, and wait for the result (#27078) * Switch on/off all lights, and wait for the result Reuses the parallel_updates semaphore. This is a small crutch which serializes platforms which already do tis for updates. Platforms which can parallelize everything, this makes it go faster * Fix broken unittest With manual validation, with help from @frenck, we found out that the assertions are wrong and the test should be failing. The sequence requested is OFF ON without cancelation, this code should result in: off,off,off,on,on,on testable, by adding a `await hass.async_block_till_done()` between the off and on call. with cancelation. there should be less off call's so off,on,on,on * Adding tests for async_request_call * Process review feedback * Switch gather with wait * :shirt: running black --- homeassistant/components/light/__init__.py | 22 +++--- homeassistant/helpers/entity.py | 13 ++++ tests/components/rflink/test_light.py | 8 +-- tests/helpers/test_entity.py | 82 ++++++++++++++++++++++ 4 files changed, 113 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index ed61d961d88..fbd908a9e45 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -292,7 +292,8 @@ async def async_setup(hass, config): preprocess_turn_on_alternatives(params) turn_lights_off, off_params = preprocess_turn_off(params) - update_tasks = [] + poll_lights = [] + change_tasks = [] for light in target_lights: light.async_set_context(service.context) @@ -305,17 +306,22 @@ async def async_setup(hass, config): preprocess_turn_on_alternatives(pars) turn_light_off, off_pars = preprocess_turn_off(pars) if turn_light_off: - await light.async_turn_off(**off_pars) + task = light.async_request_call(light.async_turn_off(**off_pars)) else: - await light.async_turn_on(**pars) + task = light.async_request_call(light.async_turn_on(**pars)) - if not light.should_poll: - continue + change_tasks.append(task) - update_tasks.append(light.async_update_ha_state(True)) + if light.should_poll: + poll_lights.append(light) - if update_tasks: - await asyncio.wait(update_tasks) + if change_tasks: + await asyncio.wait(change_tasks) + + if poll_lights: + await asyncio.wait( + [light.async_update_ha_state(True) for light in poll_lights] + ) # Listen for light on and light off service calls. hass.services.async_register( diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 836ad954ae0..5754d99d9b2 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -551,6 +551,19 @@ class Entity: """Return the representation.""" return "".format(self.name, self.state) + # call an requests + async def async_request_call(self, coro): + """Process request batched.""" + + if self.parallel_updates: + await self.parallel_updates.acquire() + + try: + await coro + finally: + if self.parallel_updates: + self.parallel_updates.release() + class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index ba4122724ce..a22e7680ac8 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -313,10 +313,10 @@ async def test_signal_repetitions_cancelling(hass, monkeypatch): await hass.async_block_till_done() - assert protocol.send_command_ack.call_args_list[0][0][1] == "on" - assert protocol.send_command_ack.call_args_list[1][0][1] == "off" - assert protocol.send_command_ack.call_args_list[2][0][1] == "off" - assert protocol.send_command_ack.call_args_list[3][0][1] == "off" + assert protocol.send_command_ack.call_args_list[0][0][1] == "off" + assert protocol.send_command_ack.call_args_list[1][0][1] == "on" + assert protocol.send_command_ack.call_args_list[2][0][1] == "on" + assert protocol.send_command_ack.call_args_list[3][0][1] == "on" async def test_type_toggle(hass, monkeypatch): diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 18cedf1c46a..9d05920f78b 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -231,6 +231,88 @@ def test_async_schedule_update_ha_state(hass): assert update_call is True +async def test_async_async_request_call_without_lock(hass): + """Test for async_requests_call works without a lock.""" + updates = [] + + class AsyncEntity(entity.Entity): + def __init__(self, entity_id): + """Initialize Async test entity.""" + self.entity_id = entity_id + self.hass = hass + + async def testhelper(self, count): + """Helper function.""" + updates.append(count) + + ent_1 = AsyncEntity("light.test_1") + ent_2 = AsyncEntity("light.test_2") + try: + job1 = ent_1.async_request_call(ent_1.testhelper(1)) + job2 = ent_2.async_request_call(ent_2.testhelper(2)) + + await asyncio.wait([job1, job2]) + while True: + if len(updates) >= 2: + break + await asyncio.sleep(0) + finally: + pass + + assert len(updates) == 2 + updates.sort() + assert updates == [1, 2] + + +async def test_async_async_request_call_with_lock(hass): + """Test for async_requests_call works with a semaphore.""" + updates = [] + + test_semaphore = asyncio.Semaphore(1) + + class AsyncEntity(entity.Entity): + def __init__(self, entity_id, lock): + """Initialize Async test entity.""" + self.entity_id = entity_id + self.hass = hass + self.parallel_updates = lock + + async def testhelper(self, count): + """Helper function.""" + updates.append(count) + + ent_1 = AsyncEntity("light.test_1", test_semaphore) + ent_2 = AsyncEntity("light.test_2", test_semaphore) + + try: + assert test_semaphore.locked() is False + await test_semaphore.acquire() + assert test_semaphore.locked() + + job1 = ent_1.async_request_call(ent_1.testhelper(1)) + job2 = ent_2.async_request_call(ent_2.testhelper(2)) + + hass.async_create_task(job1) + hass.async_create_task(job2) + + assert len(updates) == 0 + assert updates == [] + assert test_semaphore._value == 0 + + test_semaphore.release() + + while True: + if len(updates) >= 2: + break + await asyncio.sleep(0) + finally: + test_semaphore.release() + + assert len(updates) == 2 + updates.sort() + assert updates == [1, 2] + + async def test_async_parallel_updates_with_zero(hass): """Test parallel updates with 0 (disabled).""" updates = [] From 1059cea28fa206a688eb2c8c1e924d02b654d5b3 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sun, 6 Oct 2019 19:24:56 +0200 Subject: [PATCH 063/639] Refactor IKEA Tradfri, part 2 (#27245) * Add more device info data * Add attributes to device_info * Refactor sensor * Filter devices * Update following review * Update following review * Add device_Class --- .../components/tradfri/base_class.py | 2 +- homeassistant/components/tradfri/sensor.py | 92 ++++--------------- 2 files changed, 20 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 5fce3c08510..aa8487b087e 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -61,9 +61,9 @@ class TradfriBaseDevice(Entity): return { "identifiers": {(TRADFRI_DOMAIN, self._device.id)}, - "name": self._name, "manufacturer": info.manufacturer, "model": info.model_number, + "name": self._name, "sw_version": info.firmware_version, "via_device": (TRADFRI_DOMAIN, self._gateway_id), } diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 4877dbbb541..7814daf8f7a 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,20 +1,17 @@ """Support for IKEA Tradfri sensors.""" import logging -from datetime import timedelta -from pytradfri.error import PytradfriError - -from homeassistant.core import callback -from homeassistant.helpers.entity import Entity +from homeassistant.components.tradfri.base_class import TradfriBaseDevice +from homeassistant.const import DEVICE_CLASS_BATTERY from . import KEY_API, KEY_GATEWAY +from .const import CONF_GATEWAY_ID _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=5) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Tradfri config entry.""" + gateway_id = config_entry.data[CONF_GATEWAY_ID] api = hass.data[KEY_API][config_entry.entry_id] gateway = hass.data[KEY_GATEWAY][config_entry.entry_id] @@ -23,84 +20,33 @@ async def async_setup_entry(hass, config_entry, async_add_entities): devices = ( dev for dev in all_devices - if not dev.has_light_control and not dev.has_socket_control + if not dev.has_light_control + and not dev.has_socket_control + and not dev.has_blind_control ) - async_add_entities(TradfriDevice(device, api) for device in devices) + if devices: + async_add_entities(TradfriSensor(device, api, gateway_id) for device in devices) -class TradfriDevice(Entity): +class TradfriSensor(TradfriBaseDevice): """The platform class required by Home Assistant.""" - def __init__(self, device, api): + def __init__(self, device, api, gateway_id): """Initialize the device.""" - self._api = api - self._device = None - self._name = None - - self._refresh(device) - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() + super().__init__(device, api, gateway_id) + self._unique_id = f"{gateway_id}-{device.id}" @property - def should_poll(self): - """No polling needed for tradfri.""" - return False - - @property - def name(self): - """Return the display name of this device.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit_of_measurement of the device.""" - return "%" - - @property - def device_state_attributes(self): + def device_class(self): """Return the devices' state attributes.""" - info = self._device.device_info - attrs = { - "manufacturer": info.manufacturer, - "model_number": info.model_number, - "serial": info.serial, - "firmware_version": info.firmware_version, - "power_source": info.power_source_str, - "battery_level": info.battery_level, - } - return attrs + return DEVICE_CLASS_BATTERY @property def state(self): """Return the current state of the device.""" return self._device.device_info.battery_level - @callback - def _async_start_observe(self, exc=None): - """Start observation of light.""" - if exc: - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._device.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, device): - """Refresh the device data.""" - self._device = device - self._name = device.name - - def _observe_update(self, tradfri_device): - """Receive new state data for this device.""" - self._refresh(tradfri_device) - - self.hass.async_create_task(self.async_update_ha_state()) + @property + def unit_of_measurement(self): + """Return the unit_of_measurement of the device.""" + return "%" From dae8cd880183d6e375cf229f6c6375d077507d65 Mon Sep 17 00:00:00 2001 From: Santobert Date: Sun, 6 Oct 2019 20:10:11 +0200 Subject: [PATCH 064/639] Bump pybotvac and use new exceptions (#27249) * Bump pybotvac * Fix tests * Remove super calls * Surround some more statements * Correct logger message for vacuum --- .../components/neato/.translations/en.json | 3 +- homeassistant/components/neato/__init__.py | 30 ++++--- homeassistant/components/neato/camera.py | 49 ++++++++--- homeassistant/components/neato/config_flow.py | 6 +- homeassistant/components/neato/const.py | 2 + homeassistant/components/neato/manifest.json | 2 +- homeassistant/components/neato/strings.json | 3 +- homeassistant/components/neato/switch.py | 41 +++++---- homeassistant/components/neato/vacuum.py | 83 ++++++++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/neato/test_config_flow.py | 32 ++++++- 12 files changed, 178 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/neato/.translations/en.json b/homeassistant/components/neato/.translations/en.json index dc13242cc1d..69cdb48a560 100644 --- a/homeassistant/components/neato/.translations/en.json +++ b/homeassistant/components/neato/.translations/en.json @@ -13,7 +13,8 @@ } }, "error": { - "invalid_credentials": "Invalid credentials" + "invalid_credentials": "Invalid credentials", + "unexpected_error": "Unexpected error" }, "create_entry": { "default": "See [Neato documentation]({docs_url})." diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 8fd545c59bb..feaffeaeb6d 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -3,7 +3,7 @@ import asyncio import logging from datetime import timedelta -from requests.exceptions import HTTPError, ConnectionError as ConnError +from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -20,6 +20,7 @@ from .const import ( NEATO_ROBOTS, NEATO_PERSISTENT_MAPS, NEATO_MAP_DATA, + SCAN_INTERVAL_MINUTES, VALID_VENDORS, ) @@ -103,7 +104,12 @@ async def async_setup_entry(hass, entry): _LOGGER.debug("Failed to login to Neato API") return False - await hass.async_add_executor_job(hub.update_robots) + try: + await hass.async_add_executor_job(hub.update_robots) + except NeatoRobotException: + _LOGGER.debug("Failed to connect to Neato API") + return False + for component in ("camera", "vacuum", "switch"): hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -144,17 +150,19 @@ class NeatoHub: self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor ) self.logged_in = True - except (HTTPError, ConnError): - _LOGGER.error("Unable to connect to Neato API") + + _LOGGER.debug("Successfully connected to Neato API") + self._hass.data[NEATO_ROBOTS] = self.my_neato.robots + self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps + self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps + except NeatoException as ex: + if isinstance(ex, NeatoLoginException): + _LOGGER.error("Invalid credentials") + else: + _LOGGER.error("Unable to connect to Neato API") self.logged_in = False - return - _LOGGER.debug("Successfully connected to Neato API") - self._hass.data[NEATO_ROBOTS] = self.my_neato.robots - self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps - self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps - - @Throttle(timedelta(seconds=300)) + @Throttle(timedelta(minutes=SCAN_INTERVAL_MINUTES)) def update_robots(self): """Update the robot states.""" _LOGGER.debug("Running HUB.update_robots %s", self._hass.data[NEATO_ROBOTS]) diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index c565fa3d9ac..2604c6276a5 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -2,13 +2,21 @@ from datetime import timedelta import logging +from pybotvac.exceptions import NeatoRobotException + from homeassistant.components.camera import Camera -from .const import NEATO_DOMAIN, NEATO_MAP_DATA, NEATO_ROBOTS, NEATO_LOGIN +from .const import ( + NEATO_DOMAIN, + NEATO_MAP_DATA, + NEATO_ROBOTS, + NEATO_LOGIN, + SCAN_INTERVAL_MINUTES, +) _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -37,9 +45,9 @@ class NeatoCleaningMap(Camera): """Initialize Neato cleaning map.""" super().__init__() self.robot = robot - self._robot_name = "{} {}".format(self.robot.name, "Cleaning Map") + self.neato = hass.data[NEATO_LOGIN] if NEATO_LOGIN in hass.data else None + self._robot_name = f"{self.robot.name} Cleaning Map" self._robot_serial = self.robot.serial - self.neato = hass.data[NEATO_LOGIN] self._image_url = None self._image = None @@ -50,16 +58,31 @@ class NeatoCleaningMap(Camera): def update(self): """Check the contents of the map list.""" - self.neato.update_robots() - image_url = None - map_data = self.hass.data[NEATO_MAP_DATA] - image_url = map_data[self._robot_serial]["maps"][0]["url"] - if image_url == self._image_url: - _LOGGER.debug("The map image_url is the same as old") + if self.neato is None: + _LOGGER.error("Error while updating camera") + self._image = None + self._image_url = None return - image = self.neato.download_map(image_url) - self._image = image.read() - self._image_url = image_url + + _LOGGER.debug("Running camera update") + try: + self.neato.update_robots() + + image_url = None + map_data = self.hass.data[NEATO_MAP_DATA][self._robot_serial]["maps"][0] + image_url = map_data["url"] + if image_url == self._image_url: + _LOGGER.debug("The map image_url is the same as old") + return + + image = self.neato.download_map(image_url) + self._image = image.read() + self._image_url = image_url + + except NeatoRobotException as ex: + _LOGGER.error("Neato camera connection error: %s", ex) + self._image = None + self._image_url = None @property def name(self): diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 0c71cdbd069..7ece3b8d300 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from requests.exceptions import HTTPError, ConnectionError as ConnError +from pybotvac.exceptions import NeatoLoginException, NeatoRobotException from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -106,7 +106,9 @@ class NeatoConfigFlow(config_entries.ConfigFlow, domain=NEATO_DOMAIN): try: Account(username, password, this_vendor) - except (HTTPError, ConnError): + except NeatoLoginException: return "invalid_credentials" + except NeatoRobotException: + return "unexpected_error" return None diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index 6fb41bda710..4d4178a6875 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -9,6 +9,8 @@ NEATO_CONFIG = "neato_config" NEATO_MAP_DATA = "neato_map_data" NEATO_PERSISTENT_MAPS = "neato_persistent_maps" +SCAN_INTERVAL_MINUTES = 5 + VALID_VENDORS = ["neato", "vorwerk"] MODE = {1: "Eco", 2: "Turbo"} diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 160f194cd63..a4d05e8849a 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", "requirements": [ - "pybotvac==0.0.15" + "pybotvac==0.0.16" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/neato/strings.json b/homeassistant/components/neato/strings.json index dc13242cc1d..69cdb48a560 100644 --- a/homeassistant/components/neato/strings.json +++ b/homeassistant/components/neato/strings.json @@ -13,7 +13,8 @@ } }, "error": { - "invalid_credentials": "Invalid credentials" + "invalid_credentials": "Invalid credentials", + "unexpected_error": "Unexpected error" }, "create_entry": { "default": "See [Neato documentation]({docs_url})." diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 3efee11853d..8e85bef23b2 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -2,16 +2,16 @@ from datetime import timedelta import logging -import requests +from pybotvac.exceptions import NeatoRobotException from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.helpers.entity import ToggleEntity -from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS +from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=10) +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) SWITCH_TYPE_SCHEDULE = "schedule" @@ -44,8 +44,8 @@ class NeatoConnectedSwitch(ToggleEntity): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self.neato = hass.data[NEATO_LOGIN] - self._robot_name = "{} {}".format(self.robot.name, SWITCH_TYPES[self.type][0]) + self.neato = hass.data[NEATO_LOGIN] if NEATO_LOGIN in hass.data else None + self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None self._schedule_state = None self._clean_state = None @@ -53,17 +53,20 @@ class NeatoConnectedSwitch(ToggleEntity): def update(self): """Update the states of Neato switches.""" - _LOGGER.debug("Running switch update") - self.neato.update_robots() - try: - self._state = self.robot.state - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as ex: - _LOGGER.warning("Neato connection error: %s", ex) + if self.neato is None: + _LOGGER.error("Error while updating switches") self._state = None return + + _LOGGER.debug("Running switch update") + try: + self.neato.update_robots() + self._state = self.robot.state + except NeatoRobotException as ex: + _LOGGER.error("Neato switch connection error: %s", ex) + self._state = None + return + _LOGGER.debug("self._state=%s", self._state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug("State: %s", self._state) @@ -104,9 +107,15 @@ class NeatoConnectedSwitch(ToggleEntity): def turn_on(self, **kwargs): """Turn the switch on.""" if self.type == SWITCH_TYPE_SCHEDULE: - self.robot.enable_schedule() + try: + self.robot.enable_schedule() + except NeatoRobotException as ex: + _LOGGER.error("Neato switch connection error: %s", ex) def turn_off(self, **kwargs): """Turn the switch off.""" if self.type == SWITCH_TYPE_SCHEDULE: - self.robot.disable_schedule() + try: + self.robot.disable_schedule() + except NeatoRobotException as ex: + _LOGGER.error("Neato switch connection error: %s", ex) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 96c4e8f3c5f..bdb8cd0875e 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -2,7 +2,8 @@ from datetime import timedelta import logging -import requests +from pybotvac.exceptions import NeatoRobotException + import voluptuous as vol from homeassistant.components.vacuum import ( @@ -41,11 +42,12 @@ from .const import ( NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, + SCAN_INTERVAL_MINUTES, ) _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(minutes=5) +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) SUPPORT_NEATO = ( SUPPORT_BATTERY @@ -154,22 +156,26 @@ class NeatoConnectedVacuum(StateVacuumDevice): def update(self): """Update the states of Neato Vacuums.""" - _LOGGER.debug("Running Neato Vacuums update") - if self._robot_stats is None: - self._robot_stats = self.robot.get_robot_info().json() - - self.neato.update_robots() - try: - self._state = self.robot.state - self._available = True - except ( - requests.exceptions.ConnectionError, - requests.exceptions.HTTPError, - ) as ex: - _LOGGER.warning("Neato connection error: %s", ex) + if self.neato is None: + _LOGGER.error("Error while updating vacuum") self._state = None self._available = False return + + try: + _LOGGER.debug("Running Neato Vacuums update") + if self._robot_stats is None: + self._robot_stats = self.robot.get_robot_info().json() + self.neato.update_robots() + self._state = self.robot.state + self._available = True + except NeatoRobotException as ex: + if self._available: # print only once when available + _LOGGER.error("Neato vacuum connection error: %s", ex) + self._state = None + self._available = False + return + _LOGGER.debug("self._state=%s", self._state) if "alert" in self._state: robot_alert = ALERTS.get(self._state["alert"]) @@ -313,33 +319,51 @@ class NeatoConnectedVacuum(StateVacuumDevice): def start(self): """Start cleaning or resume cleaning.""" - if self._state["state"] == 1: - self.robot.start_cleaning() - elif self._state["state"] == 3: - self.robot.resume_cleaning() + try: + if self._state["state"] == 1: + self.robot.start_cleaning() + elif self._state["state"] == 3: + self.robot.resume_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def pause(self): """Pause the vacuum.""" - self.robot.pause_cleaning() + try: + self.robot.pause_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" - if self._clean_state == STATE_CLEANING: - self.robot.pause_cleaning() - self._clean_state = STATE_RETURNING - self.robot.send_to_base() + try: + if self._clean_state == STATE_CLEANING: + self.robot.pause_cleaning() + self._clean_state = STATE_RETURNING + self.robot.send_to_base() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def stop(self, **kwargs): """Stop the vacuum cleaner.""" - self.robot.stop_cleaning() + try: + self.robot.stop_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def locate(self, **kwargs): """Locate the robot by making it emit a sound.""" - self.robot.locate() + try: + self.robot.locate() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def clean_spot(self, **kwargs): """Run a spot cleaning starting from the base.""" - self.robot.start_spot_cleaning() + try: + self.robot.start_spot_cleaning() + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def neato_custom_cleaning(self, mode, navigation, category, zone=None, **kwargs): """Zone cleaning service call.""" @@ -355,4 +379,7 @@ class NeatoConnectedVacuum(StateVacuumDevice): return self._clean_state = STATE_CLEANING - self.robot.start_cleaning(mode, navigation, category, boundary_id) + try: + self.robot.start_cleaning(mode, navigation, category, boundary_id) + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) diff --git a/requirements_all.txt b/requirements_all.txt index 39d4ddcda62..075884183ff 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1102,7 +1102,7 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.15 +pybotvac==0.0.16 # homeassistant.components.nissan_leaf pycarwings2==2.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 187e50c4691..62be2f035a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -297,7 +297,7 @@ pyMetno==0.4.6 pyblackbird==0.5 # homeassistant.components.neato -pybotvac==0.0.15 +pybotvac==0.0.16 # homeassistant.components.cast pychromecast==4.0.1 diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 99691c101a6..8eb67e5d3e1 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -103,11 +103,11 @@ async def test_abort_if_already_setup(hass, account): async def test_abort_on_invalid_credentials(hass): """Test when we have invalid credentials.""" - from requests.exceptions import HTTPError + from pybotvac.exceptions import NeatoLoginException flow = init_config_flow(hass) - with patch("pybotvac.Account", side_effect=HTTPError()): + with patch("pybotvac.Account", side_effect=NeatoLoginException()): result = await flow.async_step_user( { CONF_USERNAME: USERNAME, @@ -127,3 +127,31 @@ async def test_abort_on_invalid_credentials(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "invalid_credentials" + + +async def test_abort_on_unexpected_error(hass): + """Test when we have an unexpected error.""" + from pybotvac.exceptions import NeatoRobotException + + flow = init_config_flow(hass) + + with patch("pybotvac.Account", side_effect=NeatoRobotException()): + result = await flow.async_step_user( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unexpected_error"} + + result = await flow.async_step_import( + { + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_VENDOR: VENDOR_NEATO, + } + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unexpected_error" From 02c983d3329734de25028abed320ace8dc4a2bfa Mon Sep 17 00:00:00 2001 From: CQoute Date: Mon, 7 Oct 2019 06:19:31 +1030 Subject: [PATCH 065/639] Add 'flash_length' to esphome light async_turn_off (#27214) flash_length was overlooked when fixing the asyn_turn_on flash attribute. async_turn_off is now fixed with the flash attribute. --- homeassistant/components/esphome/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 1205521706e..334e7e645a7 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -91,7 +91,7 @@ class EsphomeLight(EsphomeEntity, Light): """Turn the entity off.""" data = {"key": self._static_info.key, "state": False} if ATTR_FLASH in kwargs: - data["flash"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] + data["flash_length"] = FLASH_LENGTHS[kwargs[ATTR_FLASH]] if ATTR_TRANSITION in kwargs: data["transition_length"] = kwargs[ATTR_TRANSITION] await self._client.light_command(**data) From 3b9f0062a2919d8726c5e557183038aeab84006f Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Sun, 6 Oct 2019 23:02:15 +0200 Subject: [PATCH 066/639] Add missing documentation for some Hassio services (#27215) * Add services doc * Add missing services doc and reformat * improve readability * content improvement * HassIO to Hass.io --- homeassistant/components/hassio/services.yaml | 89 ++++++++++++++----- 1 file changed, 68 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/hassio/services.yaml b/homeassistant/components/hassio/services.yaml index 33574c5dd71..30314c646b0 100644 --- a/homeassistant/components/hassio/services.yaml +++ b/homeassistant/components/hassio/services.yaml @@ -1,37 +1,84 @@ addon_install: - description: Install a HassIO docker addon. + description: Install a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} - version: {description: Optional or it will be use the latest version., example: '0.2'} + addon: + description: The add-on slug. + example: core_ssh + version: + description: Optional or it will be use the latest version. + example: "0.2" + addon_start: - description: Start a HassIO docker addon. + description: Start a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} + addon: + description: The add-on slug. + example: core_ssh + +addon_restart: + description: Restart a Hass.io docker add-on. + fields: + addon: + description: The add-on slug. + example: core_ssh + +addon_stdin: + description: Write data to a Hass.io docker add-on stdin . + fields: + addon: + description: The add-on slug. + example: core_ssh + addon_stop: - description: Stop a HassIO docker addon. + description: Stop a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} + addon: + description: The add-on slug. + example: core_ssh + addon_uninstall: - description: Uninstall a HassIO docker addon. + description: Uninstall a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} + addon: + description: The add-on slug. + example: core_ssh + addon_update: - description: Update a HassIO docker addon. + description: Update a Hass.io docker add-on. fields: - addon: {description: Name of addon., example: smb_config} - version: {description: Optional or it will be use the latest version., example: '0.2'} + addon: + description: The add-on slug. + example: core_ssh + version: + description: Optional or it will be use the latest version. + example: "0.2" + homeassistant_update: - description: Update HomeAssistant docker image. + description: Update the Home Assistant docker image. fields: - version: {description: Optional or it will be use the latest version., example: 0.40.1} -host_reboot: {description: Reboot host computer.} -host_shutdown: {description: Poweroff host computer.} + version: + description: Optional or it will be use the latest version. + example: 0.40.1 + +host_reboot: + description: Reboot the host system. + +host_shutdown: + description: Poweroff the host system. + host_update: - description: Update host computer. + description: Update the host system. fields: - version: {description: Optional or it will be use the latest version., example: '0.3'} -supervisor_reload: {description: Reload HassIO supervisor addons/updates/configs.} + version: + description: Optional or it will be use the latest version. + example: "0.3" + +supervisor_reload: + description: Reload the Hass.io supervisor. + supervisor_update: - description: Update HassIO supervisor. + description: Update the Hass.io supervisor. fields: - version: {description: Optional or it will be use the latest version., example: '0.3'} + version: + description: Optional or it will be use the latest version. + example: "0.3" From 073bdd672a53f526be548d914adc7fe98cec4485 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 7 Oct 2019 00:32:19 +0000 Subject: [PATCH 067/639] [ci skip] Translation update --- .../components/airly/.translations/de.json | 18 ++++++++++ .../components/airly/.translations/es.json | 22 ++++++++++++ .../components/airly/.translations/fr.json | 11 ++++++ .../components/deconz/.translations/de.json | 10 ++++++ .../components/deconz/.translations/es.json | 1 + .../components/light/.translations/de.json | 9 +++++ .../components/met/.translations/de.json | 2 +- .../components/neato/.translations/da.json | 22 ++++++++++++ .../components/neato/.translations/de.json | 26 ++++++++++++++ .../components/neato/.translations/en.json | 34 +++++++++---------- .../components/neato/.translations/es.json | 26 ++++++++++++++ .../components/neato/.translations/no.json | 26 ++++++++++++++ .../components/neato/.translations/ru.json | 26 ++++++++++++++ .../components/neato/.translations/sl.json | 10 ++++++ .../opentherm_gw/.translations/da.json | 20 +++++++++++ .../opentherm_gw/.translations/de.json | 19 +++++++++++ .../opentherm_gw/.translations/es.json | 23 +++++++++++++ .../opentherm_gw/.translations/fr.json | 13 +++++++ .../opentherm_gw/.translations/no.json | 11 +++++- .../components/plex/.translations/de.json | 9 +++++ .../components/plex/.translations/es.json | 1 + .../components/sensor/.translations/es.json | 26 ++++++++++++++ .../components/soma/.translations/es.json | 13 +++++++ .../components/unifi/.translations/da.json | 5 +++ 24 files changed, 364 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/airly/.translations/de.json create mode 100644 homeassistant/components/airly/.translations/es.json create mode 100644 homeassistant/components/airly/.translations/fr.json create mode 100644 homeassistant/components/neato/.translations/da.json create mode 100644 homeassistant/components/neato/.translations/de.json create mode 100644 homeassistant/components/neato/.translations/es.json create mode 100644 homeassistant/components/neato/.translations/no.json create mode 100644 homeassistant/components/neato/.translations/ru.json create mode 100644 homeassistant/components/neato/.translations/sl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/da.json create mode 100644 homeassistant/components/opentherm_gw/.translations/de.json create mode 100644 homeassistant/components/opentherm_gw/.translations/es.json create mode 100644 homeassistant/components/opentherm_gw/.translations/fr.json create mode 100644 homeassistant/components/plex/.translations/de.json create mode 100644 homeassistant/components/sensor/.translations/es.json create mode 100644 homeassistant/components/soma/.translations/es.json diff --git a/homeassistant/components/airly/.translations/de.json b/homeassistant/components/airly/.translations/de.json new file mode 100644 index 00000000000..cb290dc46c0 --- /dev/null +++ b/homeassistant/components/airly/.translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "Name existiert bereits" + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name der Integration" + }, + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/es.json b/homeassistant/components/airly/.translations/es.json new file mode 100644 index 00000000000..0c29ad0bc66 --- /dev/null +++ b/homeassistant/components/airly/.translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "La clave de la API no es correcta.", + "name_exists": "El nombre ya existe.", + "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona." + }, + "step": { + "user": { + "data": { + "api_key": "Clave API de Airly", + "latitude": "Latitud", + "longitude": "Longitud", + "name": "Nombre de la integraci\u00f3n" + }, + "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/fr.json b/homeassistant/components/airly/.translations/fr.json new file mode 100644 index 00000000000..19561130e15 --- /dev/null +++ b/homeassistant/components/airly/.translations/fr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nom de l'int\u00e9gration" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 97e25e28965..830ae0fd13f 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -41,6 +41,16 @@ }, "title": "deCONZ Zigbee Gateway" }, + "device_automation": { + "trigger_subtype": { + "close": "Schlie\u00dfen", + "left": "Links", + "open": "Offen", + "right": "Rechts", + "turn_off": "Ausschalten", + "turn_on": "Einschalten" + } + }, "options": { "step": { "async_step_deconz_devices": { diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index cb5db0b8348..04a08d185b3 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", "remote_button_quintuple_press": "Bot\u00f3n \"{subtype}\" pulsado cinco veces consecutivas", "remote_button_rotated": "Bot\u00f3n \"{subtype}\" girado", + "remote_button_rotation_stopped": "Bot\u00f3n rotativo \"{subtipo}\" detenido", "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", "remote_button_short_release": "Bot\u00f3n \"{subtype}\" liberado", "remote_button_triple_press": "Bot\u00f3n \"{subtype}\" pulsado cuatro veces consecutivas", diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json index e07adeb0a36..be8966d9556 100644 --- a/homeassistant/components/light/.translations/de.json +++ b/homeassistant/components/light/.translations/de.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "Schalte {entity_name} um.", + "turn_off": "Schalte {entity_name} aus.", + "turn_on": "Schalte {entity_name} ein." + }, + "condition_type": { + "is_off": "{entity_name} ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet" + }, "trigger_type": { "turned_off": "{entity_name} ausgeschaltet", "turned_on": "{entity_name} eingeschaltet" diff --git a/homeassistant/components/met/.translations/de.json b/homeassistant/components/met/.translations/de.json index b70d3f12a83..2fd772c8619 100644 --- a/homeassistant/components/met/.translations/de.json +++ b/homeassistant/components/met/.translations/de.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Name existiert bereits" + "name_exists": "Ort existiert bereits" }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json new file mode 100644 index 00000000000..61c594e1011 --- /dev/null +++ b/homeassistant/components/neato/.translations/da.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Allerede konfigureret", + "invalid_credentials": "Ugyldige legitimationsoplysninger" + }, + "error": { + "invalid_credentials": "Ugyldige legitimationsoplysninger" + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + }, + "description": "Se [Neato-dokumentation] ({docs_url}).", + "title": "Neato kontooplysninger" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/de.json b/homeassistant/components/neato/.translations/de.json new file mode 100644 index 00000000000..a3f54f9f69a --- /dev/null +++ b/homeassistant/components/neato/.translations/de.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Bereits konfiguriert", + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + }, + "create_entry": { + "default": "Siehe [Neato-Dokumentation]({docs_url})." + }, + "error": { + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername", + "vendor": "Hersteller" + }, + "description": "Siehe [Neato-Dokumentation]({docs_url}).", + "title": "Neato-Kontoinformationen" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/en.json b/homeassistant/components/neato/.translations/en.json index 69cdb48a560..73628c8646e 100644 --- a/homeassistant/components/neato/.translations/en.json +++ b/homeassistant/components/neato/.translations/en.json @@ -1,27 +1,27 @@ { "config": { - "title": "Neato", - "step": { - "user": { - "title": "Neato Account Info", - "data": { - "username": "Username", - "password": "Password", - "vendor": "Vendor" - }, - "description": "See [Neato documentation]({docs_url})." - } + "abort": { + "already_configured": "Already configured", + "invalid_credentials": "Invalid credentials" + }, + "create_entry": { + "default": "See [Neato documentation]({docs_url})." }, "error": { "invalid_credentials": "Invalid credentials", "unexpected_error": "Unexpected error" }, - "create_entry": { - "default": "See [Neato documentation]({docs_url})." + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username", + "vendor": "Vendor" + }, + "description": "See [Neato documentation]({docs_url}).", + "title": "Neato Account Info" + } }, - "abort": { - "already_configured": "Already configured", - "invalid_credentials": "Invalid credentials" - } + "title": "Neato" } } \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/es.json b/homeassistant/components/neato/.translations/es.json new file mode 100644 index 00000000000..d033b8af6a4 --- /dev/null +++ b/homeassistant/components/neato/.translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ya est\u00e1 configurado", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "create_entry": { + "default": "Ver [documentaci\u00f3n Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario", + "vendor": "Vendedor" + }, + "description": "Ver [documentaci\u00f3n Neato]({docs_url}).", + "title": "Informaci\u00f3n de la cuenta de Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/no.json b/homeassistant/components/neato/.translations/no.json new file mode 100644 index 00000000000..d869fa59a18 --- /dev/null +++ b/homeassistant/components/neato/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Allerede konfigurert", + "invalid_credentials": "Ugyldig brukerinformasjon" + }, + "create_entry": { + "default": "Se [Neato dokumentasjon]({docs_url})." + }, + "error": { + "invalid_credentials": "Ugyldig brukerinformasjon", + "unexpected_error": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn", + "vendor": "Leverand\u00f8r" + }, + "title": "Neato kontoinformasjon" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json new file mode 100644 index 00000000000..2d08deb2b13 --- /dev/null +++ b/homeassistant/components/neato/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + }, + "create_entry": { + "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." + }, + "error": { + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d", + "vendor": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c" + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438.", + "title": "Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/sl.json b/homeassistant/components/neato/.translations/sl.json new file mode 100644 index 00000000000..1d256918617 --- /dev/null +++ b/homeassistant/components/neato/.translations/sl.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Podatki o ra\u010dunu Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json new file mode 100644 index 00000000000..b8abb48af4e --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -0,0 +1,20 @@ +{ + "config": { + "error": { + "already_configured": "Gateway allerede konfigureret", + "id_exists": "Gateway-id findes allerede", + "serial_error": "Fejl ved tilslutning til enheden" + }, + "step": { + "init": { + "data": { + "device": "Sti eller URL", + "id": "ID", + "name": "Navn" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json new file mode 100644 index 00000000000..274dd46488b --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "Gateway bereits konfiguriert", + "id_exists": "Gateway-ID ist bereits vorhanden", + "serial_error": "Fehler beim Verbinden mit dem Ger\u00e4t", + "timeout": "Zeit\u00fcberschreitung beim Verbindungsversuch" + }, + "step": { + "init": { + "data": { + "device": "Pfad oder URL", + "id": "ID", + "name": "Name" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/es.json b/homeassistant/components/opentherm_gw/.translations/es.json new file mode 100644 index 00000000000..8ad9d89b07a --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/es.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ya configurado", + "id_exists": "El ID del Gateway ya existe", + "serial_error": "Error de conexi\u00f3n al dispositivo", + "timeout": "Intento de conexi\u00f3n agotado" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "floor_temperature": "Temperatura del suelo", + "id": "ID", + "name": "Nombre", + "precision": "Precisi\u00f3n de la temperatura clim\u00e1tica" + }, + "title": "Gateway OpenTherm" + } + }, + "title": "Gateway OpenTherm" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json new file mode 100644 index 00000000000..a9f9acd9045 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "init": { + "data": { + "device": "Chemin ou URL", + "id": "ID", + "name": "Nom" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json index a1df80f3b12..6104aa7de72 100644 --- a/homeassistant/components/opentherm_gw/.translations/no.json +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -1,10 +1,19 @@ { "config": { + "error": { + "already_configured": "Gateway er allerede konfigurert", + "id_exists": "Gateway-ID finnes allerede", + "serial_error": "Feil ved tilkobling til enhet", + "timeout": "Tilkoblingsfors\u00f8k ble tidsavbrutt" + }, "step": { "init": { "data": { + "device": "Bane eller URL-adresse", + "floor_temperature": "Gulv klimatemperatur", "id": "ID", - "name": "Navn" + "name": "Navn", + "precision": "Klima temperaturpresisjon" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json new file mode 100644 index 00000000000..210fe732360 --- /dev/null +++ b/homeassistant/components/plex/.translations/de.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json index 6d1ad1f62da..45417d09d02 100644 --- a/homeassistant/components/plex/.translations/es.json +++ b/homeassistant/components/plex/.translations/es.json @@ -5,6 +5,7 @@ "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "Plex se est\u00e1 configurando", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Fall\u00f3 por razones desconocidas" }, "error": { diff --git a/homeassistant/components/sensor/.translations/es.json b/homeassistant/components/sensor/.translations/es.json new file mode 100644 index 00000000000..a9039d2e410 --- /dev/null +++ b/homeassistant/components/sensor/.translations/es.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} nivel de bater\u00eda", + "is_humidity": "{entity_name} humedad", + "is_illuminance": "{entity_name} iluminancia", + "is_power": "{entity_name} alimentaci\u00f3n", + "is_pressure": "{entity_name} presi\u00f3n", + "is_signal_strength": "{entity_name} intensidad de la se\u00f1al", + "is_temperature": "{entity_name} temperatura", + "is_timestamp": "{entity_name} marca de tiempo", + "is_value": "{entity_name} valor" + }, + "trigger_type": { + "battery_level": "{entity_name} nivel de bater\u00eda", + "humidity": "{entity_name} humedad", + "illuminance": "{entity_name} iluminancia", + "power": "{entity_name} alimentaci\u00f3n", + "pressure": "{entity_name} presi\u00f3n", + "signal_strength": "{entity_name} intensidad de la se\u00f1al", + "temperature": "{entity_name} temperatura", + "timestamp": "{entity_name} marca de tiempo", + "value": "{entity_name} valor" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json new file mode 100644 index 00000000000..8126b6ea5ae --- /dev/null +++ b/homeassistant/components/soma/.translations/es.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "S\u00f3lo puede configurar una cuenta de Soma.", + "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, leer la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/da.json b/homeassistant/components/unifi/.translations/da.json index 53b794ed435..0d0315e49c7 100644 --- a/homeassistant/components/unifi/.translations/da.json +++ b/homeassistant/components/unifi/.translations/da.json @@ -32,6 +32,11 @@ "track_devices": "Spor netv\u00e6rksenheder (Ubiquiti-enheder)", "track_wired_clients": "Inkluder kablede netv\u00e6rksklienter" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Opret b\u00e5ndbredde sensorer for netv\u00e6rksklienter" + } } } } From 0915d927dfa6d52519cda84b64ab521ecc8f4c93 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 6 Oct 2019 23:02:58 -0500 Subject: [PATCH 068/639] Fix Plex media_player.play_media service (#27278) * First attempt to fix play_media * More changes to media playback * Use playqueues, clean up play_media * Use similar function name, add docstring --- homeassistant/components/plex/media_player.py | 139 ++++++++---------- homeassistant/components/plex/server.py | 14 ++ 2 files changed, 76 insertions(+), 77 deletions(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 356c7fe5741..a49e4c9c057 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -2,10 +2,9 @@ from datetime import timedelta import json import logging +from xml.etree.ElementTree import ParseError import plexapi.exceptions -import plexapi.playlist -import plexapi.playqueue import requests.exceptions from homeassistant.components.media_player import MediaPlayerDevice @@ -16,6 +15,7 @@ from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, SUPPORT_PREVIOUS_TRACK, SUPPORT_STOP, SUPPORT_TURN_OFF, @@ -543,9 +543,6 @@ class PlexClient(MediaPlayerDevice): @property def supported_features(self): """Flag media player features that are supported.""" - if not self._is_player_active: - return 0 - # force show all controls if self.plex_server.show_all_controls: return ( @@ -555,13 +552,11 @@ class PlexClient(MediaPlayerDevice): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE ) - # only show controls when we know what device is connecting - if not self._make: - return 0 # no mute support if self.make.lower() == "shield android tv": _LOGGER.debug( @@ -575,8 +570,10 @@ class PlexClient(MediaPlayerDevice): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF ) + # Only supports play,pause,stop (and off which really is stop) if self.make.lower().startswith("tivo"): _LOGGER.debug( @@ -585,8 +582,7 @@ class PlexClient(MediaPlayerDevice): self.entity_id, ) return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF - # Not all devices support playback functionality - # Playback includes volume, stop/play/pause, etc. + if self.device and "playback" in self._device_protocol_capabilities: return ( SUPPORT_PAUSE @@ -595,6 +591,7 @@ class PlexClient(MediaPlayerDevice): | SUPPORT_STOP | SUPPORT_VOLUME_SET | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA | SUPPORT_TURN_OFF | SUPPORT_VOLUME_MUTE ) @@ -682,49 +679,74 @@ class PlexClient(MediaPlayerDevice): return src = json.loads(media_id) + library = src.get("library_name") + shuffle = src.get("shuffle", 0) media = None + if media_type == "MUSIC": - media = ( - self.device.server.library.section(src["library_name"]) - .get(src["artist_name"]) - .album(src["album_name"]) - .get(src["track_name"]) - ) + media = self._get_music_media(library, src) elif media_type == "EPISODE": - media = self._get_tv_media( - src["library_name"], - src["show_name"], - src["season_number"], - src["episode_number"], - ) + media = self._get_tv_media(library, src) elif media_type == "PLAYLIST": - media = self.device.server.playlist(src["playlist_name"]) + media = self.plex_server.playlist(src["playlist_name"]) elif media_type == "VIDEO": - media = self.device.server.library.section(src["library_name"]).get( - src["video_name"] - ) + media = self.plex_server.library.section(library).get(src["video_name"]) - if ( - media - and media_type == "EPISODE" - and isinstance(media, plexapi.playlist.Playlist) - ): - # delete episode playlist after being loaded into a play queue - self._client_play_media(media=media, delete=True, shuffle=src["shuffle"]) - elif media: - self._client_play_media(media=media, shuffle=src["shuffle"]) + if media is None: + _LOGGER.error("Media could not be found: %s", media_id) + return - def _get_tv_media(self, library_name, show_name, season_number, episode_number): + playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) + try: + self.device.playMedia(playqueue) + except ParseError: + # Temporary workaround for Plexamp / plexapi issue + pass + except requests.exceptions.ConnectTimeout: + _LOGGER.error("Timed out playing on %s", self.name) + + self.update_devices() + + def _get_music_media(self, library_name, src): + """Find music media and return a Plex media object.""" + artist_name = src["artist_name"] + album_name = src.get("album_name") + track_name = src.get("track_name") + track_number = src.get("track_number") + + artist = self.plex_server.library.section(library_name).get(artist_name) + + if album_name: + album = artist.album(album_name) + + if track_name: + return album.track(track_name) + + if track_number: + for track in album.tracks(): + if int(track.index) == int(track_number): + return track + return None + + return album + + if track_name: + return artist.searchTracks(track_name, maxresults=1) + return artist + + def _get_tv_media(self, library_name, src): """Find TV media and return a Plex media object.""" + show_name = src["show_name"] + season_number = src.get("season_number") + episode_number = src.get("episode_number") target_season = None target_episode = None - show = self.device.server.library.section(library_name).get(show_name) + show = self.plex_server.library.section(library_name).get(show_name) if not season_number: - playlist_name = f"{self.entity_id} - {show_name} Episodes" - return self.device.server.createPlaylist(playlist_name, show.episodes()) + return show for season in show.seasons(): if int(season.seasonNumber) == int(season_number): @@ -741,12 +763,7 @@ class PlexClient(MediaPlayerDevice): ) else: if not episode_number: - playlist_name = "{} - {} Season {} Episodes".format( - self.entity_id, show_name, str(season_number) - ) - return self.device.server.createPlaylist( - playlist_name, target_season.episodes() - ) + return target_season for episode in target_season.episodes(): if int(episode.index) == int(episode_number): @@ -764,38 +781,6 @@ class PlexClient(MediaPlayerDevice): return target_episode - def _client_play_media(self, media, delete=False, **params): - """Instruct Plex client to play a piece of media.""" - if not (self.device and "playback" in self._device_protocol_capabilities): - _LOGGER.error("Client cannot play media: %s", self.entity_id) - return - - playqueue = plexapi.playqueue.PlayQueue.create( - self.device.server, media, **params - ) - - # Delete dynamic playlists used to build playqueue (ex. play tv season) - if delete: - media.delete() - - server_url = self.device.server.baseurl.split(":") - self.device.sendCommand( - "playback/playMedia", - **dict( - { - "machineIdentifier": self.device.server.machineIdentifier, - "address": server_url[1].strip("/"), - "port": server_url[-1], - "key": media.key, - "containerKey": "/playQueues/{}?window=100&own=1".format( - playqueue.playQueueID - ), - }, - **params, - ), - ) - self.update_devices() - @property def device_state_attributes(self): """Return the scene state attributes.""" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d4393d38c97..df9e9f9f6c3 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,5 +1,6 @@ """Shared class to maintain Plex server instances.""" import plexapi.myplex +import plexapi.playqueue import plexapi.server from requests import Session @@ -109,3 +110,16 @@ class PlexServer: def show_all_controls(self): """Return show_all_controls option.""" return self.options[MP_DOMAIN][CONF_SHOW_ALL_CONTROLS] + + @property + def library(self): + """Return library attribute from server object.""" + return self._plex_server.library + + def playlist(self, title): + """Return playlist from server object.""" + return self._plex_server.playlist(title) + + def create_playqueue(self, media, **kwargs): + """Create playqueue on Plex server.""" + return plexapi.playqueue.PlayQueue.create(self._plex_server, media, **kwargs) From 5d1dd6390d28e12d4cc60a947e17e41aef9009b7 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Oct 2019 06:06:16 +0200 Subject: [PATCH 069/639] Validate generated condition (#27263) --- .../components/binary_sensor/device_condition.py | 2 +- .../device_automation/toggle_entity.py | 6 ++---- .../components/light/device_condition.py | 2 +- .../components/sensor/device_condition.py | 16 +++++++--------- .../components/switch/device_condition.py | 2 +- 5 files changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index d686ef412c1..a38c0c09ee5 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -248,7 +248,7 @@ def async_condition_from_config( if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - return condition.state_from_config(state_config, config_validation) + return condition.state_from_config(state_config) async def async_get_condition_capabilities(hass: HomeAssistant, config: dict) -> dict: diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 47953dc5e81..af29625f3a1 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -119,9 +119,7 @@ async def async_call_action_from_config( ) -def async_condition_from_config( - config: ConfigType, config_validation: bool -) -> condition.ConditionCheckerType: +def async_condition_from_config(config: ConfigType) -> condition.ConditionCheckerType: """Evaluate state based on configuration.""" condition_type = config[CONF_TYPE] if condition_type == CONF_IS_ON: @@ -136,7 +134,7 @@ def async_condition_from_config( if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - return condition.state_from_config(state_config, config_validation) + return condition.state_from_config(state_config) async def async_attach_trigger( diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index 86f5761ddf5..0b3cecbea41 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -21,7 +21,7 @@ def async_condition_from_config( """Evaluate state based on configuration.""" if config_validation: config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) + return toggle_entity.async_condition_from_config(config) async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 76f1b3909ef..18aa46d78e1 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -3,14 +3,12 @@ from typing import List import voluptuous as vol from homeassistant.core import HomeAssistant -import homeassistant.components.automation.numeric_state as numeric_state_automation from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, CONF_ABOVE, CONF_BELOW, CONF_ENTITY_ID, - CONF_FOR, CONF_TYPE, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, @@ -132,12 +130,12 @@ def async_condition_from_config( if config_validation: config = CONDITION_SCHEMA(config) numeric_state_config = { - numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE), - numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW), - numeric_state_automation.CONF_FOR: config.get(CONF_FOR), + condition.CONF_CONDITION: "numeric_state", + condition.CONF_ENTITY_ID: config[CONF_ENTITY_ID], } + if CONF_ABOVE in config: + numeric_state_config[condition.CONF_ABOVE] = config[CONF_ABOVE] + if CONF_BELOW in config: + numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW] - return condition.async_numeric_state_from_config( - numeric_state_config, config_validation - ) + return condition.async_numeric_state_from_config(numeric_state_config) diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index f3d5903bcf3..7df972151c7 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -21,7 +21,7 @@ def async_condition_from_config( """Evaluate state based on configuration.""" if config_validation: config = CONDITION_SCHEMA(config) - return toggle_entity.async_condition_from_config(config, config_validation) + return toggle_entity.async_condition_from_config(config) async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: From 41242110957cbab8b27fb1bfeefa3a37f5cd92dd Mon Sep 17 00:00:00 2001 From: Santobert Date: Mon, 7 Oct 2019 08:30:49 +0200 Subject: [PATCH 070/639] Add attributes to neato integration (#27260) * inital commit * simplify self.neato --- homeassistant/components/neato/camera.py | 28 ++++++- homeassistant/components/neato/switch.py | 11 ++- homeassistant/components/neato/vacuum.py | 101 ++++++++++++----------- 3 files changed, 85 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 2604c6276a5..d1f86ea6637 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -17,6 +17,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) +ATTR_GENERATED_AT = "generated_at" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -45,9 +46,11 @@ class NeatoCleaningMap(Camera): """Initialize Neato cleaning map.""" super().__init__() self.robot = robot - self.neato = hass.data[NEATO_LOGIN] if NEATO_LOGIN in hass.data else None + self.neato = hass.data.get(NEATO_LOGIN) + self._available = self.neato.logged_in if self.neato is not None else False self._robot_name = f"{self.robot.name} Cleaning Map" self._robot_serial = self.robot.serial + self._generated_at = None self._image_url = None self._image = None @@ -62,6 +65,7 @@ class NeatoCleaningMap(Camera): _LOGGER.error("Error while updating camera") self._image = None self._image_url = None + self._available = False return _LOGGER.debug("Running camera update") @@ -78,11 +82,14 @@ class NeatoCleaningMap(Camera): image = self.neato.download_map(image_url) self._image = image.read() self._image_url = image_url - + self._generated_at = (map_data["generated_at"].strip("Z")).replace("T", " ") + self._available = True except NeatoRobotException as ex: - _LOGGER.error("Neato camera connection error: %s", ex) + if self._available: # Print only once when available + _LOGGER.error("Neato camera connection error: %s", ex) self._image = None self._image_url = None + self._available = False @property def name(self): @@ -94,7 +101,22 @@ class NeatoCleaningMap(Camera): """Return unique ID.""" return self._robot_serial + @property + def available(self): + """Return if the robot is available.""" + return self._available + @property def device_info(self): """Device info for neato robot.""" return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} + + @property + def device_state_attributes(self): + """Return the state attributes of the vacuum cleaner.""" + data = {} + + if self._generated_at is not None: + data[ATTR_GENERATED_AT] = self._generated_at + + return data diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 8e85bef23b2..94d92c857fe 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -44,7 +44,8 @@ class NeatoConnectedSwitch(ToggleEntity): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self.neato = hass.data[NEATO_LOGIN] if NEATO_LOGIN in hass.data else None + self.neato = hass.data.get(NEATO_LOGIN) + self._available = self.neato.logged_in if self.neato is not None else False self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None self._schedule_state = None @@ -56,15 +57,19 @@ class NeatoConnectedSwitch(ToggleEntity): if self.neato is None: _LOGGER.error("Error while updating switches") self._state = None + self._available = False return _LOGGER.debug("Running switch update") try: self.neato.update_robots() self._state = self.robot.state + self._available = True except NeatoRobotException as ex: - _LOGGER.error("Neato switch connection error: %s", ex) + if self._available: # Print only once when available + _LOGGER.error("Neato switch connection error: %s", ex) self._state = None + self._available = False return _LOGGER.debug("self._state=%s", self._state) @@ -84,7 +89,7 @@ class NeatoConnectedSwitch(ToggleEntity): @property def available(self): """Return True if entity is available.""" - return self._state + return self._available @property def unique_id(self): diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index bdb8cd0875e..bf30b31eee7 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -7,8 +7,6 @@ from pybotvac.exceptions import NeatoRobotException import voluptuous as vol from homeassistant.components.vacuum import ( - ATTR_BATTERY_ICON, - ATTR_BATTERY_LEVEL, ATTR_STATUS, DOMAIN, STATE_CLEANING, @@ -68,6 +66,9 @@ ATTR_CLEAN_BATTERY_START = "battery_level_at_clean_start" ATTR_CLEAN_BATTERY_END = "battery_level_at_clean_end" ATTR_CLEAN_SUSP_COUNT = "clean_suspension_count" ATTR_CLEAN_SUSP_TIME = "clean_suspension_time" +ATTR_CLEAN_PAUSE_TIME = "clean_pause_time" +ATTR_CLEAN_ERROR_TIME = "clean_error_time" +ATTR_LAUNCHED_FROM = "launched_from" ATTR_NAVIGATION = "navigation" ATTR_CATEGORY = "category" @@ -133,20 +134,23 @@ class NeatoConnectedVacuum(StateVacuumDevice): def __init__(self, hass, robot): """Initialize the Neato Connected Vacuum.""" self.robot = robot - self.neato = hass.data[NEATO_LOGIN] + self.neato = hass.data.get(NEATO_LOGIN) + self._available = self.neato.logged_in if self.neato is not None else False self._name = f"{self.robot.name}" self._status_state = None self._clean_state = None self._state = None self._mapdata = hass.data[NEATO_MAP_DATA] - self.clean_time_start = None - self.clean_time_stop = None - self.clean_area = None - self.clean_battery_start = None - self.clean_battery_end = None - self.clean_suspension_charge_count = None - self.clean_suspension_time = None - self._available = False + self._clean_time_start = None + self._clean_time_stop = None + self._clean_area = None + self._clean_battery_start = None + self._clean_battery_end = None + self._clean_susp_charge_count = None + self._clean_susp_time = None + self._clean_pause_time = None + self._clean_error_time = None + self._launched_from = None self._battery_level = None self._robot_serial = self.robot.serial self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS] @@ -218,25 +222,18 @@ class NeatoConnectedVacuum(StateVacuumDevice): if not self._mapdata.get(self._robot_serial, {}).get("maps", []): return - self.clean_time_start = ( - self._mapdata[self._robot_serial]["maps"][0]["start_at"].strip("Z") - ).replace("T", " ") - self.clean_time_stop = ( - self._mapdata[self._robot_serial]["maps"][0]["end_at"].strip("Z") - ).replace("T", " ") - self.clean_area = self._mapdata[self._robot_serial]["maps"][0]["cleaned_area"] - self.clean_suspension_charge_count = self._mapdata[self._robot_serial]["maps"][ - 0 - ]["suspended_cleaning_charging_count"] - self.clean_suspension_time = self._mapdata[self._robot_serial]["maps"][0][ - "time_in_suspended_cleaning" - ] - self.clean_battery_start = self._mapdata[self._robot_serial]["maps"][0][ - "run_charge_at_start" - ] - self.clean_battery_end = self._mapdata[self._robot_serial]["maps"][0][ - "run_charge_at_end" - ] + + mapdata = self._mapdata[self._robot_serial]["maps"][0] + self._clean_time_start = (mapdata["start_at"].strip("Z")).replace("T", " ") + self._clean_time_stop = (mapdata["end_at"].strip("Z")).replace("T", " ") + self._clean_area = mapdata["cleaned_area"] + self._clean_susp_charge_count = mapdata["suspended_cleaning_charging_count"] + self._clean_susp_time = mapdata["time_in_suspended_cleaning"] + self._clean_pause_time = mapdata["time_in_pause"] + self._clean_error_time = mapdata["time_in_error"] + self._clean_battery_start = mapdata["run_charge_at_start"] + self._clean_battery_end = mapdata["run_charge_at_end"] + self._launched_from = mapdata["launched_from"] if self._robot_has_map: if self._state["availableServices"]["maps"] != "basic-1": @@ -267,6 +264,11 @@ class NeatoConnectedVacuum(StateVacuumDevice): """Return if the robot is available.""" return self._available + @property + def icon(self): + """Return neato specific icon.""" + return "mdi:robot-vacuum-variant" + @property def state(self): """Return the status of the vacuum cleaner.""" @@ -284,25 +286,26 @@ class NeatoConnectedVacuum(StateVacuumDevice): if self._status_state is not None: data[ATTR_STATUS] = self._status_state - - if self.battery_level is not None: - data[ATTR_BATTERY_LEVEL] = self.battery_level - data[ATTR_BATTERY_ICON] = self.battery_icon - - if self.clean_time_start is not None: - data[ATTR_CLEAN_START] = self.clean_time_start - if self.clean_time_stop is not None: - data[ATTR_CLEAN_STOP] = self.clean_time_stop - if self.clean_area is not None: - data[ATTR_CLEAN_AREA] = self.clean_area - if self.clean_suspension_charge_count is not None: - data[ATTR_CLEAN_SUSP_COUNT] = self.clean_suspension_charge_count - if self.clean_suspension_time is not None: - data[ATTR_CLEAN_SUSP_TIME] = self.clean_suspension_time - if self.clean_battery_start is not None: - data[ATTR_CLEAN_BATTERY_START] = self.clean_battery_start - if self.clean_battery_end is not None: - data[ATTR_CLEAN_BATTERY_END] = self.clean_battery_end + if self._clean_time_start is not None: + data[ATTR_CLEAN_START] = self._clean_time_start + if self._clean_time_stop is not None: + data[ATTR_CLEAN_STOP] = self._clean_time_stop + if self._clean_area is not None: + data[ATTR_CLEAN_AREA] = self._clean_area + if self._clean_susp_charge_count is not None: + data[ATTR_CLEAN_SUSP_COUNT] = self._clean_susp_charge_count + if self._clean_susp_time is not None: + data[ATTR_CLEAN_SUSP_TIME] = self._clean_susp_time + if self._clean_pause_time is not None: + data[ATTR_CLEAN_PAUSE_TIME] = self._clean_pause_time + if self._clean_error_time is not None: + data[ATTR_CLEAN_ERROR_TIME] = self._clean_error_time + if self._clean_battery_start is not None: + data[ATTR_CLEAN_BATTERY_START] = self._clean_battery_start + if self._clean_battery_end is not None: + data[ATTR_CLEAN_BATTERY_END] = self._clean_battery_end + if self._launched_from is not None: + data[ATTR_LAUNCHED_FROM] = self._launched_from return data From f6b8cffeafd05a88ab34c2de7d364f7f54fc8aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Conde=20G=C3=B3mez?= Date: Mon, 7 Oct 2019 13:17:43 +0200 Subject: [PATCH 071/639] Add PTZ support to Foscam camera component (#27238) * Add PTZ support to Foscam camera component * Address review comments: - Move service to foscam domain - Use `dict[key]` for required schema keys or with defaults - Fix sync operations in async context - Remove excessive logging * Fix import order * Move all the initialization to setup_platform and fix motion detection status logic * Move function dictionary out of the function. * Change user input to lowercase snake case * Change user input to lowercase snake case * Fix service example value * Omit foscam const module from code coverage tests * Add myself to foscam codeowners --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/foscam/camera.py | 192 +++++++++++++++--- homeassistant/components/foscam/const.py | 5 + homeassistant/components/foscam/manifest.json | 2 +- homeassistant/components/foscam/services.yaml | 12 ++ 6 files changed, 185 insertions(+), 28 deletions(-) create mode 100644 homeassistant/components/foscam/const.py create mode 100644 homeassistant/components/foscam/services.yaml diff --git a/.coveragerc b/.coveragerc index 6f3dfbc94a8..8253b5522f7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -224,6 +224,7 @@ omit = homeassistant/components/fortios/device_tracker.py homeassistant/components/fortigate/* homeassistant/components/foscam/camera.py + homeassistant/components/foscam/const.py homeassistant/components/foursquare/* homeassistant/components/free_mobile/notify.py homeassistant/components/freebox/* diff --git a/CODEOWNERS b/CODEOWNERS index ba4058d5acf..d550f3e6924 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -98,6 +98,7 @@ homeassistant/components/flock/* @fabaff homeassistant/components/flunearyou/* @bachya homeassistant/components/fortigate/* @kifeo homeassistant/components/fortios/* @kimfrellsen +homeassistant/components/foscam/* @skgsergio homeassistant/components/foursquare/* @robbiet480 homeassistant/components/freebox/* @snoof85 homeassistant/components/fronius/* @nielstron diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index 63e9956d0df..0e2ca4073bf 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -1,11 +1,26 @@ """This component provides basic support for Foscam IP cameras.""" import logging +import asyncio + +from libpyfoscam import FoscamCamera import voluptuous as vol from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM -from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD, CONF_PORT +from homeassistant.const import ( + CONF_NAME, + CONF_USERNAME, + CONF_PASSWORD, + CONF_PORT, + ATTR_ENTITY_ID, +) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.service import async_extract_entity_ids + +from .const import DOMAIN as FOSCAM_DOMAIN +from .const import DATA as FOSCAM_DATA +from .const import ENTITIES as FOSCAM_ENTITIES + _LOGGER = logging.getLogger(__name__) @@ -15,7 +30,32 @@ CONF_RTSP_PORT = "rtsp_port" DEFAULT_NAME = "Foscam Camera" DEFAULT_PORT = 88 -FOSCAM_COMM_ERROR = -8 +SERVICE_PTZ = "ptz" +ATTR_MOVEMENT = "movement" +ATTR_TRAVELTIME = "travel_time" + +DEFAULT_TRAVELTIME = 0.125 + +DIR_UP = "up" +DIR_DOWN = "down" +DIR_LEFT = "left" +DIR_RIGHT = "right" + +DIR_TOPLEFT = "top_left" +DIR_TOPRIGHT = "top_right" +DIR_BOTTOMLEFT = "bottom_left" +DIR_BOTTOMRIGHT = "bottom_right" + +MOVEMENT_ATTRS = { + DIR_UP: "ptz_move_up", + DIR_DOWN: "ptz_move_down", + DIR_LEFT: "ptz_move_left", + DIR_RIGHT: "ptz_move_right", + DIR_TOPLEFT: "ptz_move_top_left", + DIR_TOPRIGHT: "ptz_move_top_right", + DIR_BOTTOMLEFT: "ptz_move_bottom_left", + DIR_BOTTOMRIGHT: "ptz_move_bottom_right", +} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -28,44 +68,114 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +SERVICE_PTZ_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_MOVEMENT): vol.In( + [ + DIR_UP, + DIR_DOWN, + DIR_LEFT, + DIR_RIGHT, + DIR_TOPLEFT, + DIR_TOPRIGHT, + DIR_BOTTOMLEFT, + DIR_BOTTOMRIGHT, + ] + ), + vol.Optional(ATTR_TRAVELTIME, default=DEFAULT_TRAVELTIME): cv.small_float, + } +) -def setup_platform(hass, config, add_entities, discovery_info=None): + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a Foscam IP Camera.""" - add_entities([FoscamCam(config)]) + + async def async_handle_ptz(service): + """Handle PTZ service call.""" + movement = service.data[ATTR_MOVEMENT] + travel_time = service.data[ATTR_TRAVELTIME] + entity_ids = await async_extract_entity_ids(hass, service) + + if not entity_ids: + return + + _LOGGER.debug("Moving '%s' camera(s): %s", movement, entity_ids) + + all_cameras = hass.data[FOSCAM_DATA][FOSCAM_ENTITIES] + target_cameras = [ + camera for camera in all_cameras if camera.entity_id in entity_ids + ] + + for camera in target_cameras: + await camera.async_perform_ptz(movement, travel_time) + + hass.services.async_register( + FOSCAM_DOMAIN, SERVICE_PTZ, async_handle_ptz, schema=SERVICE_PTZ_SCHEMA + ) + + camera = FoscamCamera( + config[CONF_IP], + config[CONF_PORT], + config[CONF_USERNAME], + config[CONF_PASSWORD], + verbose=False, + ) + + rtsp_port = config.get(CONF_RTSP_PORT) + if not rtsp_port: + ret, response = await hass.async_add_executor_job(camera.get_port_info) + + if ret == 0: + rtsp_port = response.get("rtspPort") or response.get("mediaPort") + + ret, response = await hass.async_add_executor_job(camera.get_motion_detect_config) + + motion_status = False + if ret != 0 and response == 1: + motion_status = True + + async_add_entities( + [ + HassFoscamCamera( + camera, + config[CONF_NAME], + config[CONF_USERNAME], + config[CONF_PASSWORD], + rtsp_port, + motion_status, + ) + ] + ) -class FoscamCam(Camera): +class HassFoscamCamera(Camera): """An implementation of a Foscam IP camera.""" - def __init__(self, device_info): + def __init__(self, camera, name, username, password, rtsp_port, motion_status): """Initialize a Foscam camera.""" - from libpyfoscam import FoscamCamera - super().__init__() - ip_address = device_info.get(CONF_IP) - port = device_info.get(CONF_PORT) - self._username = device_info.get(CONF_USERNAME) - self._password = device_info.get(CONF_PASSWORD) - self._name = device_info.get(CONF_NAME) - self._motion_status = False + self._foscam_session = camera + self._name = name + self._username = username + self._password = password + self._rtsp_port = rtsp_port + self._motion_status = motion_status - self._foscam_session = FoscamCamera( - ip_address, port, self._username, self._password, verbose=False + async def async_added_to_hass(self): + """Handle entity addition to hass.""" + entities = self.hass.data.setdefault(FOSCAM_DATA, {}).setdefault( + FOSCAM_ENTITIES, [] ) - - self._rtsp_port = device_info.get(CONF_RTSP_PORT) - if not self._rtsp_port: - result, response = self._foscam_session.get_port_info() - if result == 0: - self._rtsp_port = response.get("rtspPort") or response.get("mediaPort") + entities.append(self) def camera_image(self): """Return a still image response from the camera.""" # Send the request to snap a picture and return raw jpg data # Handle exception if host is not reachable or url failed result, response = self._foscam_session.snap_picture_2() - if result == FOSCAM_COMM_ERROR: + if result != 0: return None return response @@ -97,19 +207,47 @@ class FoscamCam(Camera): """Enable motion detection in camera.""" try: ret = self._foscam_session.enable_motion_detection() - self._motion_status = ret == FOSCAM_COMM_ERROR + + if ret != 0: + return + + self._motion_status = True except TypeError: _LOGGER.debug("Communication problem") - self._motion_status = False def disable_motion_detection(self): """Disable motion detection.""" try: ret = self._foscam_session.disable_motion_detection() - self._motion_status = ret == FOSCAM_COMM_ERROR + + if ret != 0: + return + + self._motion_status = False except TypeError: _LOGGER.debug("Communication problem") - self._motion_status = False + + async def async_perform_ptz(self, movement, travel_time): + """Perform a PTZ action on the camera.""" + _LOGGER.debug("PTZ action '%s' on %s", movement, self._name) + + movement_function = getattr(self._foscam_session, MOVEMENT_ATTRS[movement]) + + ret, _ = await self.hass.async_add_executor_job(movement_function) + + if ret != 0: + _LOGGER.error("Error moving %s '%s': %s", movement, self._name, ret) + return + + await asyncio.sleep(travel_time) + + ret, _ = await self.hass.async_add_executor_job( + self._foscam_session.ptz_stop_run + ) + + if ret != 0: + _LOGGER.error("Error stopping movement on '%s': %s", self._name, ret) + return @property def name(self): diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py new file mode 100644 index 00000000000..63b4b74a763 --- /dev/null +++ b/homeassistant/components/foscam/const.py @@ -0,0 +1,5 @@ +"""Constants for Foscam component.""" + +DOMAIN = "foscam" +DATA = "foscam" +ENTITIES = "entities" diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index b2c44c113ee..6a47012ef84 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -6,5 +6,5 @@ "libpyfoscam==1.0" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@skgsergio"] } diff --git a/homeassistant/components/foscam/services.yaml b/homeassistant/components/foscam/services.yaml new file mode 100644 index 00000000000..64e68dd5bc4 --- /dev/null +++ b/homeassistant/components/foscam/services.yaml @@ -0,0 +1,12 @@ +ptz: + description: Pan/Tilt service for Foscam camera. + fields: + entity_id: + description: Name(s) of entities to move. + example: 'camera.living_room_camera' + movement: + description: "Direction of the movement. Allowed values: up, down, left, right, top_left, top_right, bottom_left, bottom_right." + example: 'up' + travel_time: + description: "(Optional) Travel time in seconds. Allowed values: float from 0 to 1. Default: 0.125" + example: 0.125 From 3adac699c7cb0c79ac022c27daf92c38e3c9212b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 7 Oct 2019 18:16:26 +0300 Subject: [PATCH 072/639] Note snake_case state attribute name convention in entity docs (#27287) https://github.com/home-assistant/home-assistant/pull/26675#discussion_r331763063 --- homeassistant/helpers/entity.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 5754d99d9b2..0d2182f88e1 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -148,7 +148,8 @@ class Entity: def state_attributes(self) -> Optional[Dict[str, Any]]: """Return the state attributes. - Implemented by component base class. + Implemented by component base class. Convention for attribute names + is lowercase snake_case. """ return None @@ -156,7 +157,8 @@ class Entity: def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return device specific state attributes. - Implemented by platform classes. + Implemented by platform classes. Convention for attribute names + is lowercase snake_case. """ return None From 761d7f21e90026d4a38fb20ee125ae2594ad5a5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 7 Oct 2019 18:17:39 +0300 Subject: [PATCH 073/639] Upgrade pylint (#27279) * Upgrade pylint to 2.4.2 and astroid to 2.3.1 https://pylint.readthedocs.io/en/latest/whatsnew/2.4.html https://pylint.readthedocs.io/en/latest/whatsnew/changelog.html#what-s-new-in-pylint-2-4-1 https://pylint.readthedocs.io/en/latest/whatsnew/changelog.html#what-s-new-in-pylint-2-4-2 * unnecessary-comprehension fixes * invalid-name fixes * self-assigning-variable fixes * Re-enable not-an-iterable * used-before-assignment fix * invalid-overridden-method fixes * undefined-variable __class__ workarounds https://github.com/PyCQA/pylint/issues/3090 * no-member false positive disabling * Remove some no longer needed disables * using-constant-test fix * Disable import-outside-toplevel for now * Disable some apparent no-value-for-parameter false positives * invalid-overridden-method false positive disables https://github.com/PyCQA/pylint/issues/3150 * Fix unintentional Entity.force_update override in AfterShipSensor --- homeassistant/components/aftership/sensor.py | 4 ++-- homeassistant/components/axis/config_flow.py | 2 +- homeassistant/components/bayesian/binary_sensor.py | 2 +- homeassistant/components/bt_smarthub/device_tracker.py | 2 +- homeassistant/components/cert_expiry/helper.py | 3 ++- homeassistant/components/ddwrt/device_tracker.py | 2 +- homeassistant/components/deconz/config_flow.py | 2 +- homeassistant/components/esphome/climate.py | 3 +++ homeassistant/components/esphome/config_flow.py | 3 ++- homeassistant/components/esphome/cover.py | 3 +++ homeassistant/components/esphome/fan.py | 3 +++ homeassistant/components/esphome/light.py | 3 +++ homeassistant/components/esphome/sensor.py | 4 ++++ homeassistant/components/esphome/switch.py | 2 ++ .../components/homekit_controller/config_flow.py | 2 +- homeassistant/components/hue/config_flow.py | 2 ++ homeassistant/components/mqtt/__init__.py | 10 ++++++++-- homeassistant/components/onkyo/media_player.py | 4 ++-- homeassistant/components/rmvtransport/sensor.py | 2 +- homeassistant/components/sabnzbd/sensor.py | 1 + homeassistant/components/synology/camera.py | 1 + homeassistant/components/tplink/device_tracker.py | 4 ++-- homeassistant/components/tradfri/config_flow.py | 2 +- homeassistant/components/upnp/sensor.py | 1 - homeassistant/components/vacuum/__init__.py | 4 +--- homeassistant/components/vallox/__init__.py | 2 +- homeassistant/components/wink/climate.py | 4 ---- homeassistant/components/zha/core/device.py | 4 +--- homeassistant/components/zha/fan.py | 2 +- homeassistant/components/zha/lock.py | 2 +- homeassistant/core.py | 4 +++- homeassistant/helpers/config_entry_flow.py | 2 +- homeassistant/util/color.py | 4 ++-- homeassistant/util/dt.py | 2 +- pylintrc | 6 +++--- requirements_test.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 37 files changed, 67 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/aftership/sensor.py b/homeassistant/components/aftership/sensor.py index e54a48f7ee4..c41e5aec7b5 100644 --- a/homeassistant/components/aftership/sensor.py +++ b/homeassistant/components/aftership/sensor.py @@ -146,10 +146,10 @@ class AfterShipSensor(Entity): async def async_added_to_hass(self): """Register callbacks.""" self.hass.helpers.dispatcher.async_dispatcher_connect( - UPDATE_TOPIC, self.force_update + UPDATE_TOPIC, self._force_update ) - async def force_update(self): + async def _force_update(self): """Force update of data.""" await self.async_update(no_throttle=True) await self.async_update_ha_state() diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 3b5efe96760..3473eba3065 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -171,7 +171,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if discovery_info[CONF_HOST].startswith("169.254"): return self.async_abort(reason="link_local_address") - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["macaddress"] = serialnumber if any( diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index acefc5a3b26..ffa13a6288c 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -250,7 +250,7 @@ class BayesianBinarySensor(BinarySensorDevice): def device_state_attributes(self): """Return the state attributes of the sensor.""" return { - ATTR_OBSERVATIONS: [val for val in self.current_obs.values()], + ATTR_OBSERVATIONS: list(self.current_obs.values()), ATTR_PROBABILITY: round(self.probability, 2), ATTR_PROBABILITY_THRESHOLD: self._probability_threshold, } diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index 58f409c2d4b..ece67e3b635 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -69,7 +69,7 @@ class BTSmartHubScanner(DeviceScanner): _LOGGER.warning("Error scanning devices") return - clients = [client for client in data.values()] + clients = list(data.values()) self.last_results = clients def get_bt_smarthub_data(self): diff --git a/homeassistant/components/cert_expiry/helper.py b/homeassistant/components/cert_expiry/helper.py index 9c10887293a..cd49588ec89 100644 --- a/homeassistant/components/cert_expiry/helper.py +++ b/homeassistant/components/cert_expiry/helper.py @@ -11,5 +11,6 @@ def get_cert(host, port): address = (host, port) with socket.create_connection(address, timeout=TIMEOUT) as sock: with ctx.wrap_socket(sock, server_hostname=address[0]) as ssock: - cert = ssock.getpeercert() + # pylint disable: https://github.com/PyCQA/pylint/issues/3166 + cert = ssock.getpeercert() # pylint: disable=no-member return cert diff --git a/homeassistant/components/ddwrt/device_tracker.py b/homeassistant/components/ddwrt/device_tracker.py index 4e661376719..bd2728d03dc 100644 --- a/homeassistant/components/ddwrt/device_tracker.py +++ b/homeassistant/components/ddwrt/device_tracker.py @@ -165,4 +165,4 @@ class DdWrtDeviceScanner(DeviceScanner): def _parse_ddwrt_response(data_str): """Parse the DD-WRT data format.""" - return {key: val for key, val in _DDWRT_DATA_REGEX.findall(data_str)} + return dict(_DDWRT_DATA_REGEX.findall(data_str)) diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 66df687047f..91768584e8a 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -187,7 +187,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): return self.async_abort(reason="already_in_progress") - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context[CONF_BRIDGEID] = bridgeid self.deconz_config = { diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index 7337aec4541..fa840078aa4 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -126,6 +126,9 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): features |= SUPPORT_PRESET_MODE return features + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def hvac_mode(self) -> Optional[str]: """Return current operation ie. heat, cool, idle.""" diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 9680ed46acd..47c00f43463 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -44,11 +44,12 @@ class EsphomeFlowHandler(config_entries.ConfigFlow): @property def _name(self): + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 return self.context.get("name") @_name.setter def _name(self, value): - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["name"] = value self.context["title_placeholders"] = {"name": self._name} diff --git a/homeassistant/components/esphome/cover.py b/homeassistant/components/esphome/cover.py index 31b895b4eb2..980fc936940 100644 --- a/homeassistant/components/esphome/cover.py +++ b/homeassistant/components/esphome/cover.py @@ -70,6 +70,9 @@ class EsphomeCover(EsphomeEntity, CoverDevice): def _state(self) -> Optional[CoverState]: return super()._state + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def is_closed(self) -> Optional[bool]: """Return if the cover is closed or not.""" diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index 44059673f15..cddb75b41bf 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -92,6 +92,9 @@ class EsphomeFan(EsphomeEntity, FanEntity): key=self._static_info.key, oscillating=oscillating ) + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def is_on(self) -> Optional[bool]: """Return true if the entity is on.""" diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 334e7e645a7..9a2a0ccd0bc 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -61,6 +61,9 @@ class EsphomeLight(EsphomeEntity, Light): def _state(self) -> Optional[LightState]: return super()._state + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property + # pylint: disable=invalid-overridden-method + @esphome_state_property def is_on(self) -> Optional[bool]: """Return true if the switch is on.""" diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 3168bae7ec8..2b7a8b94f1e 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -37,6 +37,10 @@ async def async_setup_entry( ) +# https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property +# pylint: disable=invalid-overridden-method + + class EsphomeSensor(EsphomeEntity): """A sensor implementation for esphome.""" diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index f66bfaa39f3..b52d630e1b4 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -49,6 +49,8 @@ class EsphomeSwitch(EsphomeEntity, SwitchDevice): """Return true if we do optimistic updates.""" return self._static_info.assumed_state + # https://github.com/PyCQA/pylint/issues/3150 for @esphome_state_property + # pylint: disable=invalid-overridden-method @esphome_state_property def is_on(self) -> Optional[bool]: """Return true if the switch is on.""" diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 008e0f8566d..40bf87d6f0a 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -122,7 +122,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["hkid"] = hkid self.context["title_placeholders"] = {"name": name} diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 9c0e94bc3bd..ebd71ba7c1c 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -50,6 +50,8 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + def __init__(self): """Initialize the Hue flow.""" self.host = None diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 9b25a6ef6e4..e3605cb8664 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -776,7 +776,9 @@ class MQTT: self._mqttc.on_message = self._mqtt_on_message if will_message is not None: - self._mqttc.will_set(*attr.astuple(will_message)) + self._mqttc.will_set( # pylint: disable=no-value-for-parameter + *attr.astuple(will_message) + ) async def async_publish( self, topic: str, payload: PublishPayloadType, qos: int, retain: bool @@ -909,7 +911,11 @@ class MQTT: self.hass.add_job(self._async_perform_subscription, topic, max_qos) if self.birth_message: - self.hass.add_job(self.async_publish(*attr.astuple(self.birth_message))) + self.hass.add_job( + self.async_publish( # pylint: disable=no-value-for-parameter + *attr.astuple(self.birth_message) + ) + ) def _mqtt_on_message(self, _mqttc, _userdata, msg) -> None: """Message received callback.""" diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index af92f6c5f05..92e5f01d486 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -264,7 +264,7 @@ class OnkyoDevice(MediaPlayerDevice): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join(current_source_tuples[1]) if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] elif ATTR_PRESET in self._attributes: @@ -413,7 +413,7 @@ class OnkyoDeviceZone(OnkyoDevice): if source in self._source_mapping: self._current_source = self._source_mapping[source] break - self._current_source = "_".join([i for i in current_source_tuples[1]]) + self._current_source = "_".join(current_source_tuples[1]) self._muted = bool(mute_raw[1] == "on") if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index d7d075f48f7..f66f22dda17 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -157,7 +157,7 @@ class RMVDepartureSensor(Entity): """Return the state attributes.""" try: return { - "next_departures": [val for val in self.data.departures[1:]], + "next_departures": self.data.departures[1:], "direction": self.data.departures[0].get("direction"), "line": self.data.departures[0].get("line"), "minutes": self.data.departures[0].get("minutes"), diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 58624c758d9..21ac9eefdb2 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -49,6 +49,7 @@ class SabnzbdSensor(Entity): """Return the state of the sensor.""" return self._state + @property def should_poll(self): """Don't poll. Will be updated by dispatcher signal.""" return False diff --git a/homeassistant/components/synology/camera.py b/homeassistant/components/synology/camera.py index 5594a4b3c9a..8c176f48803 100644 --- a/homeassistant/components/synology/camera.py +++ b/homeassistant/components/synology/camera.py @@ -105,6 +105,7 @@ class SynologyCamera(Camera): """Return true if the device is recording.""" return self._camera.is_recording + @property def should_poll(self): """Update the recording state periodically.""" return True diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py index e7f87074cb4..f6921efed91 100644 --- a/homeassistant/components/tplink/device_tracker.py +++ b/homeassistant/components/tplink/device_tracker.py @@ -102,7 +102,7 @@ class TplinkDeviceScanner(DeviceScanner): self.success_init = self._update_info() except requests.exceptions.RequestException: - _LOGGER.debug("RequestException in %s", __class__.__name__) + _LOGGER.debug("RequestException in %s", self.__class__.__name__) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" @@ -150,7 +150,7 @@ class Tplink1DeviceScanner(DeviceScanner): try: self.success_init = self._update_info() except requests.exceptions.RequestException: - _LOGGER.debug("RequestException in %s", __class__.__name__) + _LOGGER.debug("RequestException in %s", self.__class__.__name__) def scan_devices(self): """Scan for new devices and return a list with found device IDs.""" diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 6266766f394..9da381deb75 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -83,7 +83,7 @@ class FlowHandler(config_entries.ConfigFlow): """Handle zeroconf discovery.""" host = user_input["host"] - # pylint: disable=unsupported-assignment-operation + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["host"] = host if any(host == flow["context"]["host"] for flow in self._async_in_progress()): diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index b721fa29cdd..40cb7ef2032 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -164,7 +164,6 @@ class PerSecondUPnPIGDSensor(UpnpSensor): """Get unit we are measuring in.""" raise NotImplementedError() - @property def _async_fetch_value(self): """Fetch a value from the IGD.""" raise NotImplementedError() diff --git a/homeassistant/components/vacuum/__init__.py b/homeassistant/components/vacuum/__init__.py index 9bc376916c6..55e56415b0d 100644 --- a/homeassistant/components/vacuum/__init__.py +++ b/homeassistant/components/vacuum/__init__.py @@ -6,7 +6,7 @@ import logging import voluptuous as vol from homeassistant.components import group -from homeassistant.const import ( +from homeassistant.const import ( # noqa: F401 # STATE_PAUSED/IDLE are API ATTR_BATTERY_LEVEL, ATTR_COMMAND, SERVICE_TOGGLE, @@ -68,8 +68,6 @@ VACUUM_SEND_COMMAND_SERVICE_SCHEMA = ENTITY_SERVICE_SCHEMA.extend( STATE_CLEANING = "cleaning" STATE_DOCKED = "docked" -STATE_IDLE = STATE_IDLE -STATE_PAUSED = STATE_PAUSED STATE_RETURNING = "returning" STATE_ERROR = "error" diff --git a/homeassistant/components/vallox/__init__.py b/homeassistant/components/vallox/__init__.py index c107e4f8894..eb5edfe7fcf 100644 --- a/homeassistant/components/vallox/__init__.py +++ b/homeassistant/components/vallox/__init__.py @@ -252,7 +252,7 @@ class ValloxServiceHandler: async def async_handle(self, service): """Dispatch a service call.""" method = SERVICE_TO_METHOD.get(service.service) - params = {key: value for key, value in service.data.items()} + params = service.data.copy() if not hasattr(self, method["method"]): _LOGGER.error("Service not implemented: %s", method["method"]) diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index 38f25ef0a83..6323fa7bbfe 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -283,10 +283,6 @@ class WinkThermostat(WinkDevice, ClimateDevice): target_temp_high = target_temp if self.hvac_mode == HVAC_MODE_HEAT: target_temp_low = target_temp - if target_temp_low is not None: - target_temp_low = target_temp_low - if target_temp_high is not None: - target_temp_high = target_temp_high self.wink.set_temperature(target_temp_low, target_temp_high) def set_hvac_mode(self, hvac_mode): diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index e9e2c3b7ea6..f4a3a2c3d48 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -348,7 +348,6 @@ class ZHADevice(LogMixin): zdo_task = None for channel in channels: if channel.name == CHANNEL_ZDO: - # pylint: disable=E1111 if zdo_task is None: # We only want to do this once zdo_task = self._async_create_task( semaphore, channel, task_name, *args @@ -373,8 +372,7 @@ class ZHADevice(LogMixin): @callback def async_unsub_dispatcher(self): """Unsubscribe the dispatcher.""" - if self._unsub: - self._unsub() + self._unsub() @callback def async_update_last_seen(self, last_seen): diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 1f119ef6657..43ad2291cb7 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -43,7 +43,7 @@ SPEED_LIST = [ SPEED_SMART, ] -VALUE_TO_SPEED = {i: speed for i, speed in enumerate(SPEED_LIST)} +VALUE_TO_SPEED = dict(enumerate(SPEED_LIST)) SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index afc4618343c..a2151b4bdcb 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) STATE_LIST = [STATE_UNLOCKED, STATE_LOCKED, STATE_UNLOCKED] -VALUE_TO_STATE = {i: state for i, state in enumerate(STATE_LIST)} +VALUE_TO_STATE = dict(enumerate(STATE_LIST)) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/core.py b/homeassistant/core.py index feb4445d36d..90d197906cb 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -186,7 +186,9 @@ class HomeAssistant: self.data: dict = {} self.state = CoreState.not_running self.exit_code = 0 - self.config_entries: Optional[ConfigEntries] = None + self.config_entries: Optional[ + ConfigEntries # pylint: disable=used-before-assignment + ] = None # If not None, use to signal end-of-loop self._stopped: Optional[asyncio.Event] = None diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 922878fb324..88aae3721b1 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -38,7 +38,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): if user_input is None: return self.async_show_form(step_id="confirm") - if ( + if ( # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context and self.context.get("source") != config_entries.SOURCE_DISCOVERY ): diff --git a/homeassistant/util/color.py b/homeassistant/util/color.py index 89d1dcfc4c1..640e5c5540a 100644 --- a/homeassistant/util/color.py +++ b/homeassistant/util/color.py @@ -167,8 +167,8 @@ COLORS = { class XYPoint: """Represents a CIE 1931 XY coordinate pair.""" - x = attr.ib(type=float) - y = attr.ib(type=float) + x = attr.ib(type=float) # pylint: disable=invalid-name + y = attr.ib(type=float) # pylint: disable=invalid-name @attr.s() diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index a948c4407ae..1abb4294398 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -220,7 +220,7 @@ def get_age(date: dt.datetime) -> str: def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> List[int]: """Parse the time expression part and return a list of times to match.""" if parameter is None or parameter == MATCH_ALL: - res = [x for x in range(min_value, max_value + 1)] + res = list(range(min_value, max_value + 1)) elif isinstance(parameter, str) and parameter.startswith("/"): parameter = int(parameter[1:]) res = [x for x in range(min_value, max_value + 1) if x % parameter == 0] diff --git a/pylintrc b/pylintrc index bb4f1fe96d0..3d69800e5c3 100644 --- a/pylintrc +++ b/pylintrc @@ -2,7 +2,7 @@ ignore=tests [BASIC] -good-names=i,j,k,ex,Run,_,fp +good-names=id,i,j,k,ex,Run,_,fp [MESSAGES CONTROL] # Reasons disabled: @@ -18,8 +18,8 @@ good-names=i,j,k,ex,Run,_,fp # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise -# not-an-iterable - https://github.com/PyCQA/pylint/issues/2311 # unnecessary-pass - readability for functions which only contain pass +# import-outside-toplevel - TODO disable= format, abstract-class-little-used, @@ -27,9 +27,9 @@ disable= cyclic-import, duplicate-code, global-statement, + import-outside-toplevel, inconsistent-return-statements, locally-disabled, - not-an-iterable, not-context-manager, redefined-variable-type, too-few-public-methods, diff --git a/requirements_test.txt b/requirements_test.txt index 9da375b33c8..d0b7880d78d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,8 +12,8 @@ mock-open==1.3.1 mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 -pylint==2.3.1 -astroid==2.2.5 +pylint==2.4.2 +astroid==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62be2f035a5..425170d168d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -13,8 +13,8 @@ mock-open==1.3.1 mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 -pylint==2.3.1 -astroid==2.2.5 +pylint==2.4.2 +astroid==2.3.1 pytest-aiohttp==0.3.0 pytest-cov==2.7.1 pytest-sugar==0.9.2 From eb10f8dcd3b95e95db474a44eac449f220289643 Mon Sep 17 00:00:00 2001 From: Chandan Rai Date: Mon, 7 Oct 2019 22:55:36 +0530 Subject: [PATCH 074/639] fixed minor typo in docs/source/api/helpers.rst (#27282) --- docs/source/api/helpers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/api/helpers.rst b/docs/source/api/helpers.rst index 28f4059d60d..8ad645b7977 100644 --- a/docs/source/api/helpers.rst +++ b/docs/source/api/helpers.rst @@ -56,7 +56,7 @@ homeassistant.helpers.data_entry_flow module homeassistant.helpers.deprecation module ---------------------------------------- -.. automodule:: homeassistant.helpers.depracation +.. automodule:: homeassistant.helpers.deprecation :members: :undoc-members: :show-inheritance: From feb1986459f5c123471d2ab66a7190bead94f81e Mon Sep 17 00:00:00 2001 From: Aaron Godfrey Date: Mon, 7 Oct 2019 10:40:52 -0700 Subject: [PATCH 075/639] Fix the todoist integration (#27273) * Fixed the todoist integration. * Removing unused import * Flake8 fixes. * Added username to codeowners. * Updated global codeowners --- CODEOWNERS | 1 + homeassistant/components/todoist/calendar.py | 40 +++++++++++-------- .../components/todoist/manifest.json | 4 +- .../components/todoist/services.yaml | 25 ++++++++++++ requirements_all.txt | 2 +- 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index d550f3e6924..6e343e91533 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -291,6 +291,7 @@ homeassistant/components/threshold/* @fabaff homeassistant/components/tibber/* @danielhiversen homeassistant/components/tile/* @bachya homeassistant/components/time_date/* @fabaff +homeassistant/components/todoist/* @boralyl homeassistant/components/toon/* @frenck homeassistant/components/tplink/* @rytilahti homeassistant/components/traccar/* @ludeeus diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 75aec037a25..1179fd90868 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -36,6 +36,7 @@ CONTENT = "content" DESCRIPTION = "description" # Calendar Platform: Used in the '_get_date()' method DATETIME = "dateTime" +DUE = "due" # Service Call: When is this task due (in natural language)? DUE_DATE_STRING = "due_date_string" # Service Call: The language of DUE_DATE_STRING @@ -206,7 +207,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): project_id = project_id_lookup[project_name] # Create the task - item = api.items.add(call.data[CONTENT], project_id) + item = api.items.add(call.data[CONTENT], project_id=project_id) if LABELS in call.data: task_labels = call.data[LABELS] @@ -216,11 +217,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if PRIORITY in call.data: item.update(priority=call.data[PRIORITY]) + _due: dict = {} if DUE_DATE_STRING in call.data: - item.update(date_string=call.data[DUE_DATE_STRING]) + _due["string"] = call.data[DUE_DATE_STRING] if DUE_DATE_LANG in call.data: - item.update(date_lang=call.data[DUE_DATE_LANG]) + _due["lang"] = call.data[DUE_DATE_LANG] if DUE_DATE in call.data: due_date = dt.parse_datetime(call.data[DUE_DATE]) @@ -231,7 +233,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): due_date = dt.as_utc(due_date) date_format = "%Y-%m-%dT%H:%M" due_date = datetime.strftime(due_date, date_format) - item.update(due_date_utc=due_date) + _due["date"] = due_date + + if _due: + item.update(due=_due) + # Commit changes api.commit() _LOGGER.debug("Created Todoist task: %s", call.data[CONTENT]) @@ -241,6 +247,17 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) +def _parse_due_date(data: dict) -> datetime: + """Parse the due date dict into a datetime object.""" + # Add time information to date only strings. + if len(data["date"]) == 10: + data["date"] += "T00:00:00" + # If there is no timezone provided, use UTC. + if data["timezone"] is None: + data["date"] += "Z" + return dt.parse_datetime(data["date"]) + + class TodoistProjectDevice(CalendarEventDevice): """A device for getting the next Task from a Todoist Project.""" @@ -412,16 +429,8 @@ class TodoistProjectData: # complete the task. # Generally speaking, that means right now. task[START] = dt.utcnow() - if data[DUE_DATE_UTC] is not None: - due_date = data[DUE_DATE_UTC] - - # Due dates are represented in RFC3339 format, in UTC. - # Home Assistant exclusively uses UTC, so it'll - # handle the conversion. - time_format = "%a %d %b %Y %H:%M:%S %z" - # HASS' built-in parse time function doesn't like - # Todoist's time format; strptime has to be used. - task[END] = datetime.strptime(due_date, time_format) + if data[DUE] is not None: + task[END] = _parse_due_date(data[DUE]) if self._latest_due_date is not None and ( task[END] > self._latest_due_date @@ -540,9 +549,8 @@ class TodoistProjectData: project_task_data = project_data[TASKS] events = [] - time_format = "%a %d %b %Y %H:%M:%S %z" for task in project_task_data: - due_date = datetime.strptime(task["due_date_utc"], time_format) + due_date = _parse_due_date(task["due"]) if start_date < due_date < end_date: event = { "uid": task["id"], diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index dbf1a941e00..e7876c953cc 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -3,8 +3,8 @@ "name": "Todoist", "documentation": "https://www.home-assistant.io/integrations/todoist", "requirements": [ - "todoist-python==7.0.17" + "todoist-python==8.0.0" ], "dependencies": [], - "codeowners": [] + "codeowners": ["@boralyl"] } diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index e69de29bb2d..c2d23cc4bec 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -0,0 +1,25 @@ +new_task: + description: Create a new task and add it to a project. + fields: + content: + description: The name of the task. + example: Pick up the mail. + project: + description: The name of the project this task should belong to. Defaults to Inbox. + example: Errands + labels: + description: Any labels that you want to apply to this task, separated by a comma. + example: Chores,Delivieries + priority: + description: The priority of this task, from 1 (normal) to 4 (urgent). + example: 2 + due_date_string: + description: The day this task is due, in natural language. + example: Tomorrow + due_date_lang: + description: The language of due_date_string. + example: en + due_date: + description: The day this task is due, in format YYYY-MM-DD. + example: 2019-10-22 + diff --git a/requirements_all.txt b/requirements_all.txt index 075884183ff..17f39ac1d8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1891,7 +1891,7 @@ thingspeak==0.4.1 tikteck==0.4 # homeassistant.components.todoist -todoist-python==7.0.17 +todoist-python==8.0.0 # homeassistant.components.toon toonapilib==3.2.4 From 7cdb76eedb58fd55b56e07d90d8e57fa0d1ba005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edgardo=20Ram=C3=ADrez?= Date: Mon, 7 Oct 2019 12:41:26 -0500 Subject: [PATCH 076/639] FIX: Typo (#27267) --- homeassistant/components/fan/services.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fan/services.yaml b/homeassistant/components/fan/services.yaml index 16d3742d9ab..0e3978690e6 100644 --- a/homeassistant/components/fan/services.yaml +++ b/homeassistant/components/fan/services.yaml @@ -42,7 +42,7 @@ toggle: fields: entity_id: description: Name(s) of the entities to toggle - exampl: 'fan.living_room' + example: 'fan.living_room' set_direction: description: Set the fan rotation. From fe155faf6a12c462947155d86dba4157e74421d4 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Mon, 7 Oct 2019 19:43:47 +0200 Subject: [PATCH 077/639] Refactor tradfri light (#27259) * Refactor light file * Update following review --- homeassistant/components/tradfri/light.py | 149 ++++++---------------- 1 file changed, 39 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 615899a98c8..f5d61f0aaed 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -16,8 +16,9 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, Light, ) +from homeassistant.components.tradfri.base_class import TradfriBaseDevice from homeassistant.core import callback -from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY +from . import KEY_API, KEY_GATEWAY from .const import CONF_GATEWAY_ID, CONF_IMPORT_GROUPS _LOGGER = logging.getLogger(__name__) @@ -143,99 +144,55 @@ class TradfriGroup(Light): await self._api(self._group.update()) -class TradfriLight(Light): +class TradfriLight(TradfriBaseDevice, Light): """The platform class required by Home Assistant.""" - def __init__(self, light, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a Light.""" - self._api = api - self._unique_id = f"light-{gateway_id}-{light.id}" - self._light = None - self._light_control = None - self._light_data = None - self._name = None + super().__init__(device, api, gateway_id) + self._unique_id = f"light-{gateway_id}-{device.id}" self._hs_color = None self._features = SUPPORTED_FEATURES - self._available = True - self._gateway_id = gateway_id - self._refresh(light) - - @property - def unique_id(self): - """Return unique ID for light.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - info = self._light.device_info - - return { - "identifiers": {(TRADFRI_DOMAIN, self._light.id)}, - "name": self._name, - "manufacturer": info.manufacturer, - "model": info.model_number, - "sw_version": info.firmware_version, - "via_device": (TRADFRI_DOMAIN, self._gateway_id), - } + self._refresh(device) @property def min_mireds(self): """Return the coldest color_temp that this light supports.""" - return self._light_control.min_mireds + return self._device_control.min_mireds @property def max_mireds(self): """Return the warmest color_temp that this light supports.""" - return self._light_control.max_mireds - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def should_poll(self): - """No polling needed for tradfri light.""" - return False + return self._device_control.max_mireds @property def supported_features(self): """Flag supported features.""" return self._features - @property - def name(self): - """Return the display name of this light.""" - return self._name - @property def is_on(self): """Return true if light is on.""" - return self._light_data.state + return self._device_data.state @property def brightness(self): """Return the brightness of the light.""" - return self._light_data.dimmer + return self._device_data.dimmer @property def color_temp(self): """Return the color temp value in mireds.""" - return self._light_data.color_temp + return self._device_data.color_temp @property def hs_color(self): """HS color of the light.""" - if self._light_control.can_set_color: - hsbxy = self._light_data.hsb_xy_color - hue = hsbxy[0] / (self._light_control.max_hue / 360) - sat = hsbxy[1] / (self._light_control.max_saturation / 100) + if self._device_control.can_set_color: + hsbxy = self._device_data.hsb_xy_color + hue = hsbxy[0] / (self._device_control.max_hue / 360) + sat = hsbxy[1] / (self._device_control.max_saturation / 100) if hue is not None and sat is not None: return hue, sat @@ -248,9 +205,9 @@ class TradfriLight(Light): transition_time = int(kwargs[ATTR_TRANSITION]) * 10 dimmer_data = {ATTR_DIMMER: 0, ATTR_TRANSITION_TIME: transition_time} - await self._api(self._light_control.set_dimmer(**dimmer_data)) + await self._api(self._device_control.set_dimmer(**dimmer_data)) else: - await self._api(self._light_control.set_state(False)) + await self._api(self._device_control.set_state(False)) async def async_turn_on(self, **kwargs): """Instruct the light to turn on.""" @@ -267,32 +224,32 @@ class TradfriLight(Light): ATTR_DIMMER: brightness, ATTR_TRANSITION_TIME: transition_time, } - dimmer_command = self._light_control.set_dimmer(**dimmer_data) + dimmer_command = self._device_control.set_dimmer(**dimmer_data) transition_time = None else: - dimmer_command = self._light_control.set_state(True) + dimmer_command = self._device_control.set_state(True) color_command = None - if ATTR_HS_COLOR in kwargs and self._light_control.can_set_color: - hue = int(kwargs[ATTR_HS_COLOR][0] * (self._light_control.max_hue / 360)) + if ATTR_HS_COLOR in kwargs and self._device_control.can_set_color: + hue = int(kwargs[ATTR_HS_COLOR][0] * (self._device_control.max_hue / 360)) sat = int( - kwargs[ATTR_HS_COLOR][1] * (self._light_control.max_saturation / 100) + kwargs[ATTR_HS_COLOR][1] * (self._device_control.max_saturation / 100) ) color_data = { ATTR_HUE: hue, ATTR_SAT: sat, ATTR_TRANSITION_TIME: transition_time, } - color_command = self._light_control.set_hsb(**color_data) + color_command = self._device_control.set_hsb(**color_data) transition_time = None temp_command = None if ATTR_COLOR_TEMP in kwargs and ( - self._light_control.can_set_temp or self._light_control.can_set_color + self._device_control.can_set_temp or self._device_control.can_set_color ): temp = kwargs[ATTR_COLOR_TEMP] # White Spectrum bulb - if self._light_control.can_set_temp: + if self._device_control.can_set_temp: if temp > self.max_mireds: temp = self.max_mireds elif temp < self.min_mireds: @@ -301,21 +258,21 @@ class TradfriLight(Light): ATTR_COLOR_TEMP: temp, ATTR_TRANSITION_TIME: transition_time, } - temp_command = self._light_control.set_color_temp(**temp_data) + temp_command = self._device_control.set_color_temp(**temp_data) transition_time = None # Color bulb (CWS) # color_temp needs to be set with hue/saturation - elif self._light_control.can_set_color: + elif self._device_control.can_set_color: temp_k = color_util.color_temperature_mired_to_kelvin(temp) hs_color = color_util.color_temperature_to_hs(temp_k) - hue = int(hs_color[0] * (self._light_control.max_hue / 360)) - sat = int(hs_color[1] * (self._light_control.max_saturation / 100)) + hue = int(hs_color[0] * (self._device_control.max_hue / 360)) + sat = int(hs_color[1] * (self._device_control.max_saturation / 100)) color_data = { ATTR_HUE: hue, ATTR_SAT: sat, ATTR_TRANSITION_TIME: transition_time, } - color_command = self._light_control.set_hsb(**color_data) + color_command = self._device_control.set_hsb(**color_data) transition_time = None # HSB can always be set, but color temp + brightness is bulb dependant @@ -325,7 +282,7 @@ class TradfriLight(Light): else: command = color_command - if self._light_control.can_combine_commands: + if self._device_control.can_combine_commands: await self._api(command + temp_command) else: if temp_command is not None: @@ -333,46 +290,18 @@ class TradfriLight(Light): if command is not None: await self._api(command) - @callback - def _async_start_observe(self, exc=None): - """Start observation of light.""" - - if exc: - self._available = False - self.async_schedule_update_ha_state() - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._light.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, light): + def _refresh(self, device): """Refresh the light data.""" - self._light = light + super()._refresh(device) # Caching of LightControl and light object - self._available = light.reachable - self._light_control = light.light_control - self._light_data = light.light_control.lights[0] - self._name = light.name + self._device_control = device.light_control + self._device_data = device.light_control.lights[0] self._features = SUPPORTED_FEATURES - if light.light_control.can_set_dimmer: + if device.light_control.can_set_dimmer: self._features |= SUPPORT_BRIGHTNESS - if light.light_control.can_set_color: + if device.light_control.can_set_color: self._features |= SUPPORT_COLOR - if light.light_control.can_set_temp: + if device.light_control.can_set_temp: self._features |= SUPPORT_COLOR_TEMP - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this light.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() From 35bca702b4835bf6029897d210ab07949accb728 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Mon, 7 Oct 2019 11:09:08 -0700 Subject: [PATCH 078/639] Neato battery sensor (#27286) * initial commit * Pring log only once if available * Update coverage * Review comments * Move variables --- .coveragerc | 3 +- homeassistant/components/neato/__init__.py | 3 +- homeassistant/components/neato/sensor.py | 104 +++++++++++++++++++++ 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/neato/sensor.py diff --git a/.coveragerc b/.coveragerc index 8253b5522f7..3de008439de 100644 --- a/.coveragerc +++ b/.coveragerc @@ -422,8 +422,9 @@ omit = homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/camera.py - homeassistant/components/neato/vacuum.py + homeassistant/components/neato/sensor.py homeassistant/components/neato/switch.py + homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py homeassistant/components/nest/* diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index feaffeaeb6d..c1fb128a1d1 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -110,7 +110,7 @@ async def async_setup_entry(hass, entry): _LOGGER.debug("Failed to connect to Neato API") return False - for component in ("camera", "vacuum", "switch"): + for component in ("camera", "vacuum", "switch", "sensor"): hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) @@ -125,6 +125,7 @@ async def async_unload_entry(hass, entry): hass.config_entries.async_forward_entry_unload(entry, "camera"), hass.config_entries.async_forward_entry_unload(entry, "vacuum"), hass.config_entries.async_forward_entry_unload(entry, "switch"), + hass.config_entries.async_forward_entry_unload(entry, "sensor"), ) return True diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py new file mode 100644 index 00000000000..0201012cc37 --- /dev/null +++ b/homeassistant/components/neato/sensor.py @@ -0,0 +1,104 @@ +"""Support for Neato sensors.""" +import logging + +from datetime import timedelta +from pybotvac.exceptions import NeatoRobotException + +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY +from homeassistant.helpers.entity import Entity + +from .const import NEATO_ROBOTS, NEATO_LOGIN, NEATO_DOMAIN, SCAN_INTERVAL_MINUTES + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=SCAN_INTERVAL_MINUTES) + +BATTERY = "Battery" + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Neato sensor.""" + pass + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Neato sensor using config entry.""" + dev = [] + neato = hass.data.get(NEATO_LOGIN) + for robot in hass.data[NEATO_ROBOTS]: + dev.append(NeatoSensor(neato, robot)) + + if not dev: + return + + _LOGGER.debug("Adding robots for sensors %s", dev) + async_add_entities(dev, True) + + +class NeatoSensor(Entity): + """Neato sensor.""" + + def __init__(self, neato, robot): + """Initialize Neato sensor.""" + self.robot = robot + self.neato = neato + self._available = self.neato.logged_in if self.neato is not None else False + self._robot_name = f"{self.robot.name} {BATTERY}" + self._robot_serial = self.robot.serial + self._state = None + + def update(self): + """Update Neato Sensor.""" + if self.neato is None: + _LOGGER.error("Error while updating sensor") + self._state = None + self._available = False + return + + try: + self.neato.update_robots() + self._state = self.robot.state + except NeatoRobotException as ex: + if self._available: + _LOGGER.error("Neato sensor connection error: %s", ex) + self._state = None + self._available = False + return + + self._available = True + _LOGGER.debug("self._state=%s", self._state) + + @property + def name(self): + """Return the name of this sensor.""" + return self._robot_name + + @property + def unique_id(self): + """Return unique ID.""" + return self._robot_serial + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_BATTERY + + @property + def available(self): + """Return availability.""" + return self._available + + @property + def state(self): + """Return the state.""" + return self._state["details"]["charge"] + + @property + def unit_of_measurement(self): + """Return unit of measurement.""" + return "%" + + @property + def device_info(self): + """Device info for neato robot.""" + return {"identifiers": {(NEATO_DOMAIN, self._robot_serial)}} From a3c98440e0e208a162f90ad67a5f843e5011f153 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 7 Oct 2019 13:29:12 -0500 Subject: [PATCH 079/639] Remove manual config flow step (#27291) --- homeassistant/components/plex/config_flow.py | 59 +----- homeassistant/components/plex/strings.json | 18 +- tests/components/plex/test_config_flow.py | 188 ++++++------------- 3 files changed, 66 insertions(+), 199 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index dd5401950e9..38727ccff06 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -12,14 +12,7 @@ from homeassistant.components.http.view import HomeAssistantView from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_URL, - CONF_TOKEN, - CONF_SSL, - CONF_VERIFY_SSL, -) +from homeassistant.const import CONF_URL, CONF_TOKEN, CONF_SSL, CONF_VERIFY_SSL from homeassistant.core import callback from homeassistant.util.json import load_json @@ -30,8 +23,6 @@ from .const import ( # pylint: disable=unused-import CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, CONF_SHOW_ALL_CONTROLS, - DEFAULT_PORT, - DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, PLEX_CONFIG_FILE, @@ -44,8 +35,6 @@ from .const import ( # pylint: disable=unused-import from .errors import NoServersFound, ServerNotSpecified from .server import PlexServer -USER_SCHEMA = vol.Schema({vol.Optional("manual_setup"): bool}) - _LOGGER = logging.getLogger(__package__) @@ -73,23 +62,17 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Plex flow.""" self.current_login = {} - self.discovery_info = {} self.available_servers = None self.plexauth = None self.token = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - errors = {} - if user_input is not None: - if user_input.pop("manual_setup", False): - return await self.async_step_manual_setup(user_input) + return self.async_show_form(step_id="start_website_auth") - return await self.async_step_plex_website_auth() - - return self.async_show_form( - step_id="user", data_schema=USER_SCHEMA, errors=errors - ) + async def async_step_start_website_auth(self, user_input=None): + """Show a form before starting external authentication.""" + return await self.async_step_plex_website_auth() async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" @@ -120,9 +103,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_abort(reason="unknown") if errors: - return self.async_show_form( - step_id="user", data_schema=USER_SCHEMA, errors=errors - ) + return self.async_show_form(step_id="start_website_auth", errors=errors) server_id = plex_server.machine_identifier @@ -152,30 +133,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_manual_setup(self, user_input=None): - """Begin manual configuration.""" - if len(user_input) > 1: - host = user_input.pop(CONF_HOST) - port = user_input.pop(CONF_PORT) - prefix = "https" if user_input.pop(CONF_SSL) else "http" - user_input[CONF_URL] = f"{prefix}://{host}:{port}" - return await self.async_step_server_validate(user_input) - - data_schema = vol.Schema( - { - vol.Required( - CONF_HOST, default=self.discovery_info.get(CONF_HOST) - ): str, - vol.Required( - CONF_PORT, default=self.discovery_info.get(CONF_PORT, DEFAULT_PORT) - ): int, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, - vol.Optional(CONF_TOKEN, default=user_input.get(CONF_TOKEN, "")): str, - } - ) - return self.async_show_form(step_id="manual_setup", data_schema=data_schema) - async def async_step_select_server(self, user_input=None): """Use selected Plex server.""" config = dict(self.current_login) @@ -210,8 +167,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # Skip discovery if a config already exists or is in progress. return self.async_abort(reason="already_configured") - discovery_info[CONF_PORT] = int(discovery_info[CONF_PORT]) - self.discovery_info = discovery_info json_file = self.hass.config.path(PLEX_CONFIG_FILE) file_config = await self.hass.async_add_executor_job(load_json, json_file) @@ -227,7 +182,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.info("Imported legacy config, file can be removed: %s", json_file) return await self.async_step_server_validate(server_config) - return await self.async_step_user() + return self.async_abort(reason="discovery_no_file") async def async_step_import(self, import_config): """Import from Plex configuration.""" diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 6538d8e887e..aff79acc2ed 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -2,16 +2,6 @@ "config": { "title": "Plex", "step": { - "manual_setup": { - "title": "Plex server", - "data": { - "host": "Host", - "port": "Port", - "ssl": "Use SSL", - "verify_ssl": "Verify SSL certificate", - "token": "Token (if required)" - } - }, "select_server": { "title": "Select Plex server", "description": "Multiple servers available, select one:", @@ -19,12 +9,9 @@ "server": "Server" } }, - "user": { + "start_website_auth": { "title": "Connect Plex server", - "description": "Continue to authorize at plex.tv or manually configure a server.", - "data": { - "manual_setup": "Manual setup" - } + "description": "Continue to authorize at plex.tv." } }, "error": { @@ -36,6 +23,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", + "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 753d565a82b..e9f48f6a4f8 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -6,14 +6,7 @@ import plexapi.exceptions import requests.exceptions from homeassistant.components.plex import config_flow -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_VERIFY_SSL, - CONF_TOKEN, - CONF_URL, -) +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -48,34 +41,32 @@ def init_config_flow(hass): async def test_bad_credentials(hass): """Test when provided credentials are rejected.""" + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" - - with patch( + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( "plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value="BAD TOKEN" ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: False, - CONF_VERIFY_SSL: False, - CONF_TOKEN: "BAD TOKEN", - }, + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "faulty_credentials" @@ -123,8 +114,8 @@ async def test_discovery(hass): context={"source": "discovery"}, data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, ) - assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["type"] == "abort" + assert result["reason"] == "discovery_no_file" async def test_discovery_while_in_progress(hass): @@ -201,7 +192,7 @@ async def test_import_bad_hostname(hass): }, ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "not_found" @@ -212,26 +203,25 @@ async def test_unknown_exception(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" + mock_connections = MockConnections() + mm_plex_account = MagicMock() + mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) + mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.server.PlexServer", side_effect=Exception): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: True, - CONF_VERIFY_SSL: True, - CONF_TOKEN: MOCK_TOKEN, - }, - ) + with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( + "plexapi.server.PlexServer", side_effect=Exception + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value="MOCK_TOKEN" + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external_done" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "abort" assert result["reason"] == "unknown" @@ -245,7 +235,7 @@ async def test_no_servers_found(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mm_plex_account = MagicMock() mm_plex_account.resources = Mock(return_value=[]) @@ -256,9 +246,7 @@ async def test_no_servers_found(hass): "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -266,7 +254,7 @@ async def test_no_servers_found(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" assert result["errors"]["base"] == "no_servers" @@ -279,7 +267,7 @@ async def test_single_available_server(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() @@ -304,9 +292,7 @@ async def test_single_available_server(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -336,7 +322,7 @@ async def test_multiple_servers_with_selection(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -360,9 +346,7 @@ async def test_multiple_servers_with_selection(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -406,7 +390,7 @@ async def test_adding_last_unconfigured_server(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -430,9 +414,7 @@ async def test_adding_last_unconfigured_server(hass): mock_plex_server.return_value )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -512,7 +494,7 @@ async def test_all_available_servers_configured(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" mock_connections = MockConnections() mm_plex_account = MagicMock() @@ -525,9 +507,7 @@ async def test_all_available_servers_configured(hass): "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -538,58 +518,6 @@ async def test_all_available_servers_configured(hass): assert result["reason"] == "all_configured" -async def test_manual_config(hass): - """Test creating via manual configuration.""" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": True} - ) - assert result["type"] == "form" - assert result["step_id"] == "manual_setup" - - mock_connections = MockConnections(ssl=True) - - with patch("plexapi.server.PlexServer") as mock_plex_server: - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: MOCK_HOST_1, - CONF_PORT: MOCK_PORT_1, - CONF_SSL: True, - CONF_VERIFY_SSL: True, - CONF_TOKEN: MOCK_TOKEN, - }, - ) - assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name - assert ( - result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier - ) - assert ( - result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri - ) - assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN - - async def test_option_flow(hass): """Test config flow selection of one of two bridges.""" @@ -627,15 +555,13 @@ async def test_external_timed_out(hass): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=None ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -655,14 +581,12 @@ async def test_callback_view(hass, aiohttp_client): config_flow.DOMAIN, context={"source": "user"} ) assert result["type"] == "form" - assert result["step_id"] == "user" + assert result["step_id"] == "start_website_auth" with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={"manual_setup": False} - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external" client = await aiohttp_client(hass.http.app) From 1febb32dd953ed9cda612aae235744b643756e7e Mon Sep 17 00:00:00 2001 From: Santobert Date: Mon, 7 Oct 2019 21:49:54 +0200 Subject: [PATCH 080/639] Neato clean up (#27294) * Replace hass with neato * Clean up try-except blocks * Add some new try-except blocks * Clean up vacuum * Minor fix * Another fix --- homeassistant/components/neato/camera.py | 45 ++++++++++++++-------- homeassistant/components/neato/switch.py | 9 +++-- homeassistant/components/neato/vacuum.py | 48 +++++++++++++++--------- 3 files changed, 64 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index d1f86ea6637..98b48dd7225 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -28,9 +28,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up Neato camera with config entry.""" dev = [] + neato = hass.data.get(NEATO_LOGIN) + mapdata = hass.data.get(NEATO_MAP_DATA) for robot in hass.data[NEATO_ROBOTS]: if "maps" in robot.traits: - dev.append(NeatoCleaningMap(hass, robot)) + dev.append(NeatoCleaningMap(neato, robot, mapdata)) if not dev: return @@ -42,11 +44,12 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoCleaningMap(Camera): """Neato cleaning map for last clean.""" - def __init__(self, hass, robot): + def __init__(self, neato, robot, mapdata): """Initialize Neato cleaning map.""" super().__init__() self.robot = robot - self.neato = hass.data.get(NEATO_LOGIN) + self.neato = neato + self._mapdata = mapdata self._available = self.neato.logged_in if self.neato is not None else False self._robot_name = f"{self.robot.name} Cleaning Map" self._robot_serial = self.robot.serial @@ -71,25 +74,35 @@ class NeatoCleaningMap(Camera): _LOGGER.debug("Running camera update") try: self.neato.update_robots() - - image_url = None - map_data = self.hass.data[NEATO_MAP_DATA][self._robot_serial]["maps"][0] - image_url = map_data["url"] - if image_url == self._image_url: - _LOGGER.debug("The map image_url is the same as old") - return - - image = self.neato.download_map(image_url) - self._image = image.read() - self._image_url = image_url - self._generated_at = (map_data["generated_at"].strip("Z")).replace("T", " ") - self._available = True except NeatoRobotException as ex: if self._available: # Print only once when available _LOGGER.error("Neato camera connection error: %s", ex) self._image = None self._image_url = None self._available = False + return + + image_url = None + map_data = self._mapdata[self._robot_serial]["maps"][0] + image_url = map_data["url"] + if image_url == self._image_url: + _LOGGER.debug("The map image_url is the same as old") + return + + try: + image = self.neato.download_map(image_url) + except NeatoRobotException as ex: + if self._available: # Print only once when available + _LOGGER.error("Neato camera connection error: %s", ex) + self._image = None + self._image_url = None + self._available = False + return + + self._image = image.read() + self._image_url = image_url + self._generated_at = (map_data["generated_at"].strip("Z")).replace("T", " ") + self._available = True @property def name(self): diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 94d92c857fe..8536af63945 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -26,9 +26,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up Neato switch with config entry.""" dev = [] + neato = hass.data.get(NEATO_LOGIN) for robot in hass.data[NEATO_ROBOTS]: for type_name in SWITCH_TYPES: - dev.append(NeatoConnectedSwitch(hass, robot, type_name)) + dev.append(NeatoConnectedSwitch(neato, robot, type_name)) if not dev: return @@ -40,11 +41,11 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoConnectedSwitch(ToggleEntity): """Neato Connected Switches.""" - def __init__(self, hass, robot, switch_type): + def __init__(self, neato, robot, switch_type): """Initialize the Neato Connected switches.""" self.type = switch_type self.robot = robot - self.neato = hass.data.get(NEATO_LOGIN) + self.neato = neato self._available = self.neato.logged_in if self.neato is not None else False self._robot_name = f"{self.robot.name} {SWITCH_TYPES[self.type][0]}" self._state = None @@ -64,7 +65,6 @@ class NeatoConnectedSwitch(ToggleEntity): try: self.neato.update_robots() self._state = self.robot.state - self._available = True except NeatoRobotException as ex: if self._available: # Print only once when available _LOGGER.error("Neato switch connection error: %s", ex) @@ -72,6 +72,7 @@ class NeatoConnectedSwitch(ToggleEntity): self._available = False return + self._available = True _LOGGER.debug("self._state=%s", self._state) if self.type == SWITCH_TYPE_SCHEDULE: _LOGGER.debug("State: %s", self._state) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index bf30b31eee7..5d8fd42a5f7 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -95,8 +95,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, entry, async_add_entities): """Set up Neato vacuum with config entry.""" dev = [] + neato = hass.data.get(NEATO_LOGIN) + mapdata = hass.data.get(NEATO_MAP_DATA) + persistent_maps = hass.data.get(NEATO_PERSISTENT_MAPS) for robot in hass.data[NEATO_ROBOTS]: - dev.append(NeatoConnectedVacuum(hass, robot)) + dev.append(NeatoConnectedVacuum(neato, robot, mapdata, persistent_maps)) if not dev: return @@ -112,7 +115,10 @@ async def async_setup_entry(hass, entry, async_add_entities): navigation = call.data.get(ATTR_NAVIGATION) category = call.data.get(ATTR_CATEGORY) zone = call.data.get(ATTR_ZONE) - robot.neato_custom_cleaning(mode, navigation, category, zone) + try: + robot.neato_custom_cleaning(mode, navigation, category, zone) + except NeatoRobotException as ex: + _LOGGER.error("Neato vacuum connection error: %s", ex) def service_to_entities(call): """Return the known devices that a service call mentions.""" @@ -131,16 +137,19 @@ async def async_setup_entry(hass, entry, async_add_entities): class NeatoConnectedVacuum(StateVacuumDevice): """Representation of a Neato Connected Vacuum.""" - def __init__(self, hass, robot): + def __init__(self, neato, robot, mapdata, persistent_maps): """Initialize the Neato Connected Vacuum.""" self.robot = robot - self.neato = hass.data.get(NEATO_LOGIN) + self.neato = neato self._available = self.neato.logged_in if self.neato is not None else False + self._mapdata = mapdata self._name = f"{self.robot.name}" + self._robot_has_map = self.robot.has_persistent_maps + self._robot_maps = persistent_maps + self._robot_serial = self.robot.serial self._status_state = None self._clean_state = None self._state = None - self._mapdata = hass.data[NEATO_MAP_DATA] self._clean_time_start = None self._clean_time_stop = None self._clean_area = None @@ -152,10 +161,7 @@ class NeatoConnectedVacuum(StateVacuumDevice): self._clean_error_time = None self._launched_from = None self._battery_level = None - self._robot_serial = self.robot.serial - self._robot_maps = hass.data[NEATO_PERSISTENT_MAPS] self._robot_boundaries = {} - self._robot_has_map = self.robot.has_persistent_maps self._robot_stats = None def update(self): @@ -166,13 +172,12 @@ class NeatoConnectedVacuum(StateVacuumDevice): self._available = False return + _LOGGER.debug("Running Neato Vacuums update") try: - _LOGGER.debug("Running Neato Vacuums update") if self._robot_stats is None: self._robot_stats = self.robot.get_robot_info().json() self.neato.update_robots() self._state = self.robot.state - self._available = True except NeatoRobotException as ex: if self._available: # print only once when available _LOGGER.error("Neato vacuum connection error: %s", ex) @@ -180,6 +185,7 @@ class NeatoConnectedVacuum(StateVacuumDevice): self._available = False return + self._available = True _LOGGER.debug("self._state=%s", self._state) if "alert" in self._state: robot_alert = ALERTS.get(self._state["alert"]) @@ -235,14 +241,20 @@ class NeatoConnectedVacuum(StateVacuumDevice): self._clean_battery_end = mapdata["run_charge_at_end"] self._launched_from = mapdata["launched_from"] - if self._robot_has_map: - if self._state["availableServices"]["maps"] != "basic-1": - if self._robot_maps[self._robot_serial]: - allmaps = self._robot_maps[self._robot_serial] - for maps in allmaps: - self._robot_boundaries = self.robot.get_map_boundaries( - maps["id"] - ).json() + if ( + self._robot_has_map + and self._state["availableServices"]["maps"] != "basic-1" + and self._robot_maps[self._robot_serial] + ): + allmaps = self._robot_maps[self._robot_serial] + for maps in allmaps: + try: + self._robot_boundaries = self.robot.get_map_boundaries( + maps["id"] + ).json() + except NeatoRobotException as ex: + _LOGGER.error("Could not fetch map boundaries: %s", ex) + self._robot_boundaries = {} @property def name(self): From 6565c17828daf683cd7833e76a638f90bc121bf0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 7 Oct 2019 21:55:35 +0200 Subject: [PATCH 081/639] UniFi - Improve controller tests (#27261) * Improve controller tests and harmonize setup_unifi_integration to one * Store listeners to dispatchers to be used during reset --- homeassistant/components/unifi/__init__.py | 5 +- homeassistant/components/unifi/controller.py | 25 +- .../components/unifi/device_tracker.py | 10 +- homeassistant/components/unifi/sensor.py | 10 +- homeassistant/components/unifi/switch.py | 4 +- tests/components/unifi/test_controller.py | 476 +++++++++++------- tests/components/unifi/test_device_tracker.py | 122 +---- tests/components/unifi/test_init.py | 9 +- tests/components/unifi/test_sensor.py | 101 +--- tests/components/unifi/test_switch.py | 222 ++------ 10 files changed, 401 insertions(+), 583 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 5b43289e403..4f3edf9ce79 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -77,12 +77,13 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = {} controller = UniFiController(hass, config_entry) - controller_id = get_controller_id_from_config_entry(config_entry) - hass.data[DOMAIN][controller_id] = controller if not await controller.async_setup(): return False + controller_id = get_controller_id_from_config_entry(config_entry) + hass.data[DOMAIN][controller_id] = controller + if controller.mac is None: return True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index fa1164166bd..3deb2e9040a 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -57,6 +57,7 @@ class UniFiController: self.progress = None self.wireless_clients = None + self.listeners = [] self._site_name = None self._site_role = None @@ -258,13 +259,14 @@ class UniFiController: def import_configuration(self): """Import configuration to config entry options.""" - unifi_config = {} + import_config = {} + for config in self.hass.data[UNIFI_CONFIG]: if ( self.host == config[CONF_HOST] and self.site_name == config[CONF_SITE_ID] ): - unifi_config = config + import_config = config break old_options = dict(self.config_entry.options) @@ -278,16 +280,17 @@ class UniFiController: (CONF_DETECTION_TIME, CONF_DETECTION_TIME), (CONF_SSID_FILTER, CONF_SSID_FILTER), ): - if config in unifi_config: - if config == option and unifi_config[ + if config in import_config: + print(config) + if config == option and import_config[ config ] != self.config_entry.options.get(option): - new_options[option] = unifi_config[config] + new_options[option] = import_config[config] elif config != option and ( option not in self.config_entry.options - or unifi_config[config] == self.config_entry.options.get(option) + or import_config[config] == self.config_entry.options.get(option) ): - new_options[option] = not unifi_config[config] + new_options[option] = not import_config[config] if new_options: options = {**old_options, **new_options} @@ -301,15 +304,15 @@ class UniFiController: Will cancel any scheduled setup retry and will unload the config entry. """ - # If the authentication was wrong. - if self.api is None: - return True - for platform in SUPPORTED_PLATFORMS: await self.hass.config_entries.async_forward_entry_unload( self.config_entry, platform ) + for unsub_dispatcher in self.listeners: + unsub_dispatcher() + self.listeners = [] + return True diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 48b19d7bada..b92211c4eae 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -67,7 +67,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Update the values of the controller.""" update_items(controller, async_add_entities, tracked) - async_dispatcher_connect(hass, controller.signal_update, update_controller) + controller.listeners.append( + async_dispatcher_connect(hass, controller.signal_update, update_controller) + ) @callback def update_disable_on_entities(): @@ -82,8 +84,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity.registry_entry.entity_id, disabled_by=disabled_by ) - async_dispatcher_connect( - hass, controller.signal_options_update, update_disable_on_entities + controller.listeners.append( + async_dispatcher_connect( + hass, controller.signal_options_update, update_disable_on_entities + ) ) update_controller() diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index aad013970d1..e4f9b0df6c9 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -31,7 +31,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Update the values of the controller.""" update_items(controller, async_add_entities, sensors) - async_dispatcher_connect(hass, controller.signal_update, update_controller) + controller.listeners.append( + async_dispatcher_connect(hass, controller.signal_update, update_controller) + ) @callback def update_disable_on_entities(): @@ -46,8 +48,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity.registry_entry.entity_id, disabled_by=disabled_by ) - async_dispatcher_connect( - hass, controller.signal_options_update, update_disable_on_entities + controller.listeners.append( + async_dispatcher_connect( + hass, controller.signal_options_update, update_disable_on_entities + ) ) update_controller() diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index f8fad6dac8e..82aa6f0384d 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -53,7 +53,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Update the values of the controller.""" update_items(controller, async_add_entities, switches, switches_off) - async_dispatcher_connect(hass, controller.signal_update, update_controller) + controller.listeners.append( + async_dispatcher_connect(hass, controller.signal_update, update_controller) + ) update_controller() switches_off.clear() diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index ae6f3776b4f..2b64e56cd99 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -1,9 +1,14 @@ """Test UniFi Controller.""" -from unittest.mock import Mock, patch +from collections import deque +from datetime import timedelta + +from asynctest import Mock, patch import pytest +from homeassistant import config_entries from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.components import unifi from homeassistant.components.unifi.const import ( CONF_CONTROLLER, CONF_SITE_ID, @@ -17,269 +22,362 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) -from homeassistant.components.unifi import controller, errors +import aiounifi -from tests.common import mock_coro - -CONTROLLER_SITES = {"site1": {"desc": "nice name", "name": "site", "role": "admin"}} +CONTROLLER_HOST = { + "hostname": "controller_host", + "ip": "1.2.3.4", + "is_wired": True, + "last_seen": 1562600145, + "mac": "10:00:00:00:00:01", + "name": "Controller host", + "oui": "Producer", + "sw_mac": "00:00:00:00:01:01", + "sw_port": 1, + "wired-rx_bytes": 1234000000, + "wired-tx_bytes": 5678000000, +} CONTROLLER_DATA = { CONF_HOST: "1.2.3.4", CONF_USERNAME: "username", CONF_PASSWORD: "password", CONF_PORT: 1234, - CONF_SITE_ID: "site", - CONF_VERIFY_SSL: True, + CONF_SITE_ID: "site_id", + CONF_VERIFY_SSL: False, } ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} +SITES = {"Site name": {"desc": "Site name", "name": "site_id", "role": "admin"}} -async def test_controller_setup(): + +async def setup_unifi_integration( + hass, + config, + options, + sites, + clients_response, + devices_response, + clients_all_response, +): + """Create the UniFi controller.""" + if UNIFI_CONFIG not in hass.data: + hass.data[UNIFI_CONFIG] = [] + hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) + config_entry = config_entries.ConfigEntry( + version=1, + domain=unifi.DOMAIN, + title="Mock Title", + data=config, + source="test", + connection_class=config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, + options=options, + entry_id=1, + ) + + mock_client_responses = deque() + mock_client_responses.append(clients_response) + + mock_device_responses = deque() + mock_device_responses.append(devices_response) + + mock_client_all_responses = deque() + mock_client_all_responses.append(clients_all_response) + + mock_requests = [] + + async def mock_request(self, method, path, json=None): + mock_requests.append({"method": method, "path": path, "json": json}) + + if path == "s/{site}/stat/sta" and mock_client_responses: + return mock_client_responses.popleft() + if path == "s/{site}/stat/device" and mock_device_responses: + return mock_device_responses.popleft() + if path == "s/{site}/rest/user" and mock_client_all_responses: + return mock_client_all_responses.popleft() + return {} + + with patch("aiounifi.Controller.login", return_value=True), patch( + "aiounifi.Controller.sites", return_value=sites + ), patch("aiounifi.Controller.request", new=mock_request): + await unifi.async_setup_entry(hass, config_entry) + await hass.async_block_till_done() + hass.config_entries._entries.append(config_entry) + + controller_id = unifi.get_controller_id_from_config_entry(config_entry) + if controller_id not in hass.data[unifi.DOMAIN]: + return None + controller = hass.data[unifi.DOMAIN][controller_id] + + controller.mock_client_responses = mock_client_responses + controller.mock_device_responses = mock_device_responses + controller.mock_client_all_responses = mock_client_all_responses + controller.mock_requests = mock_requests + + return controller + + +async def test_controller_setup(hass): """Successful setup.""" - hass = Mock() - hass.data = { - UNIFI_CONFIG: [ - { - CONF_HOST: CONTROLLER_DATA[CONF_HOST], - CONF_SITE_ID: "nice name", - controller.CONF_BLOCK_CLIENT: ["mac"], - controller.CONF_DONT_TRACK_CLIENTS: True, - controller.CONF_DONT_TRACK_DEVICES: True, - controller.CONF_DONT_TRACK_WIRED_CLIENTS: True, - controller.CONF_DETECTION_TIME: 30, - controller.CONF_SSID_FILTER: ["ssid"], - } - ], - UNIFI_WIRELESS_CLIENTS: Mock(), - } - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = {} - api = Mock() - api.initialize.return_value = mock_coro(True) - api.sites.return_value = mock_coro(CONTROLLER_SITES) - api.clients = [] + with patch( + "homeassistant.config_entries.ConfigEntries.async_forward_entry_setup", + return_value=True, + ) as forward_entry_setup: + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) - unifi_controller = controller.UniFiController(hass, entry) + entry = controller.config_entry + assert len(forward_entry_setup.mock_calls) == len( + unifi.controller.SUPPORTED_PLATFORMS + ) + assert forward_entry_setup.mock_calls[0][1] == (entry, "device_tracker") + assert forward_entry_setup.mock_calls[1][1] == (entry, "sensor") + assert forward_entry_setup.mock_calls[2][1] == (entry, "switch") - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True + assert controller.host == CONTROLLER_DATA[CONF_HOST] + assert controller.site == CONTROLLER_DATA[CONF_SITE_ID] + assert controller.site_name in SITES + assert controller.site_role == SITES[controller.site_name]["role"] - assert unifi_controller.api is api - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == len( - controller.SUPPORTED_PLATFORMS + assert ( + controller.option_allow_bandwidth_sensors + == unifi.const.DEFAULT_ALLOW_BANDWIDTH_SENSORS ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == ( - entry, - "device_tracker", + assert controller.option_block_clients == unifi.const.DEFAULT_BLOCK_CLIENTS + assert controller.option_track_clients == unifi.const.DEFAULT_TRACK_CLIENTS + assert controller.option_track_devices == unifi.const.DEFAULT_TRACK_DEVICES + assert ( + controller.option_track_wired_clients == unifi.const.DEFAULT_TRACK_WIRED_CLIENTS ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == ( - entry, - "sensor", - ) - assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == ( - entry, - "switch", + assert controller.option_detection_time == timedelta( + seconds=unifi.const.DEFAULT_DETECTION_TIME ) + assert controller.option_ssid_filter == unifi.const.DEFAULT_SSID_FILTER + + assert controller.mac is None + + assert controller.signal_update == "unifi-update-1.2.3.4-site_id" + assert controller.signal_options_update == "unifi-options-1.2.3.4-site_id" -async def test_controller_host(): - """Config entry host and controller host are the same.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - - unifi_controller = controller.UniFiController(hass, entry) - - assert unifi_controller.host == CONTROLLER_DATA[CONF_HOST] - - -async def test_controller_site(): - """Config entry site and controller site are the same.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - - unifi_controller = controller.UniFiController(hass, entry) - - assert unifi_controller.site == CONTROLLER_DATA[CONF_SITE_ID] - - -async def test_controller_mac(): +async def test_controller_mac(hass): """Test that it is possible to identify controller mac.""" - hass = Mock() - hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} - hass.data[UNIFI_WIRELESS_CLIENTS].get_data.return_value = set() - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = {} - client = Mock() - client.ip = "1.2.3.4" - client.mac = "00:11:22:33:44:55" - api = Mock() - api.initialize.return_value = mock_coro(True) - api.clients = {"client1": client} - api.sites.return_value = mock_coro(CONTROLLER_SITES) - - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True - - assert unifi_controller.mac == "00:11:22:33:44:55" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[CONTROLLER_HOST], + devices_response=[], + clients_all_response=[], + ) + assert controller.mac == "10:00:00:00:00:01" -async def test_controller_no_mac(): - """Test that it works to not find the controllers mac.""" - hass = Mock() - hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = {} - client = Mock() - client.ip = "5.6.7.8" - api = Mock() - api.initialize.return_value = mock_coro(True) - api.clients = {"client1": client} - api.sites.return_value = mock_coro(CONTROLLER_SITES) - api.clients = {} +async def test_controller_import_config(hass): + """Test that import configuration.yaml instructions work.""" + hass.data[UNIFI_CONFIG] = [ + { + CONF_HOST: "1.2.3.4", + CONF_SITE_ID: "Site name", + unifi.const.CONF_ALLOW_BANDWIDTH_SENSORS: True, + unifi.CONF_BLOCK_CLIENT: ["random mac"], + unifi.CONF_DONT_TRACK_CLIENTS: True, + unifi.CONF_DONT_TRACK_DEVICES: True, + unifi.CONF_DONT_TRACK_WIRED_CLIENTS: True, + unifi.CONF_DETECTION_TIME: 150, + unifi.CONF_SSID_FILTER: ["SSID"], + } + ] + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True - - assert unifi_controller.mac is None + assert controller.option_allow_bandwidth_sensors is False + assert controller.option_block_clients == ["random mac"] + assert controller.option_track_clients is False + assert controller.option_track_devices is False + assert controller.option_track_wired_clients is False + assert controller.option_detection_time == timedelta(seconds=150) + assert controller.option_ssid_filter == ["SSID"] -async def test_controller_not_accessible(): +async def test_controller_not_accessible(hass): """Retry to login gets scheduled when connection fails.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - api = Mock() - api.initialize.return_value = mock_coro(True) - - unifi_controller = controller.UniFiController(hass, entry) - with patch.object( - controller, "get_controller", side_effect=errors.CannotConnect + unifi.controller, "get_controller", side_effect=unifi.errors.CannotConnect ), pytest.raises(ConfigEntryNotReady): - await unifi_controller.async_setup() + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) -async def test_controller_unknown_error(): +async def test_controller_unknown_error(hass): """Unknown errors are handled.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG - api = Mock() - api.initialize.return_value = mock_coro(True) - - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", side_effect=Exception): - assert await unifi_controller.async_setup() is False - - assert not hass.helpers.event.async_call_later.mock_calls + with patch.object(unifi.controller, "get_controller", side_effect=Exception): + await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + assert hass.data[unifi.DOMAIN] == {} -async def test_reset_if_entry_had_wrong_auth(): - """Calling reset when the entry contains wrong auth.""" - hass = Mock() - entry = Mock() - entry.data = ENTRY_CONFIG +async def test_reset_after_successful_setup(hass): + """Calling reset when the entry has been setup.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) - unifi_controller = controller.UniFiController(hass, entry) + assert len(controller.listeners) == 5 + + result = await controller.async_reset() + await hass.async_block_till_done() + + assert result is True + assert len(controller.listeners) == 0 + + +async def test_failed_update_failed_login(hass): + """Running update can handle a failed login.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) with patch.object( - controller, "get_controller", side_effect=errors.AuthenticationRequired + controller.api.clients, "update", side_effect=aiounifi.LoginRequired + ), patch.object(controller.api, "login", side_effect=aiounifi.AiounifiException): + await controller.async_update() + await hass.async_block_till_done() + + assert controller.available is False + + +async def test_failed_update_successful_login(hass): + """Running update can login when requested.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + with patch.object( + controller.api.clients, "update", side_effect=aiounifi.LoginRequired + ), patch.object(controller.api, "login", return_value=Mock(True)): + await controller.async_update() + await hass.async_block_till_done() + + assert controller.available is True + + +async def test_failed_update(hass): + """Running update can login when requested.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], + ) + + with patch.object( + controller.api.clients, "update", side_effect=aiounifi.AiounifiException ): - assert await unifi_controller.async_setup() is False + await controller.async_update() + await hass.async_block_till_done() - assert not hass.async_add_job.mock_calls + assert controller.available is False - assert await unifi_controller.async_reset() - - -async def test_reset_unloads_entry_if_setup(): - """Calling reset when the entry has been setup.""" - hass = Mock() - hass.data = {UNIFI_CONFIG: {}, UNIFI_WIRELESS_CLIENTS: Mock()} - entry = Mock() - entry.data = ENTRY_CONFIG - entry.options = {} - api = Mock() - api.initialize.return_value = mock_coro(True) - api.sites.return_value = mock_coro(CONTROLLER_SITES) - api.clients = [] - - unifi_controller = controller.UniFiController(hass, entry) - - with patch.object(controller, "get_controller", return_value=mock_coro(api)): - assert await unifi_controller.async_setup() is True - - assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == len( - controller.SUPPORTED_PLATFORMS - ) - - hass.config_entries.async_forward_entry_unload.return_value = mock_coro(True) - assert await unifi_controller.async_reset() - - assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == len( - controller.SUPPORTED_PLATFORMS - ) + await controller.async_update() + await hass.async_block_till_done() + assert controller.available is True async def test_get_controller(hass): """Successful call.""" - with patch("aiounifi.Controller.login", return_value=mock_coro()): - assert await controller.get_controller(hass, **CONTROLLER_DATA) + with patch("aiounifi.Controller.login", return_value=Mock()): + assert await unifi.controller.get_controller(hass, **CONTROLLER_DATA) async def test_get_controller_verify_ssl_false(hass): """Successful call with verify ssl set to false.""" controller_data = dict(CONTROLLER_DATA) controller_data[CONF_VERIFY_SSL] = False - with patch("aiounifi.Controller.login", return_value=mock_coro()): - assert await controller.get_controller(hass, **controller_data) + with patch("aiounifi.Controller.login", return_value=Mock()): + assert await unifi.controller.get_controller(hass, **controller_data) async def test_get_controller_login_failed(hass): """Check that get_controller can handle a failed login.""" - import aiounifi - result = None with patch("aiounifi.Controller.login", side_effect=aiounifi.Unauthorized): try: - result = await controller.get_controller(hass, **CONTROLLER_DATA) - except errors.AuthenticationRequired: + result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + except unifi.errors.AuthenticationRequired: pass assert result is None async def test_get_controller_controller_unavailable(hass): """Check that get_controller can handle controller being unavailable.""" - import aiounifi - result = None with patch("aiounifi.Controller.login", side_effect=aiounifi.RequestError): try: - result = await controller.get_controller(hass, **CONTROLLER_DATA) - except errors.CannotConnect: + result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + except unifi.errors.CannotConnect: pass assert result is None async def test_get_controller_unknown_error(hass): """Check that get_controller can handle unkown errors.""" - import aiounifi - result = None with patch("aiounifi.Controller.login", side_effect=aiounifi.AiounifiException): try: - result = await controller.get_controller(hass, **CONTROLLER_DATA) - except errors.AuthenticationRequired: + result = await unifi.controller.get_controller(hass, **CONTROLLER_DATA) + except unifi.errors.AuthenticationRequired: pass assert result is None diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index d2cedb91d8d..29b16553757 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,37 +1,23 @@ """The tests for the UniFi device tracker platform.""" -from collections import deque from copy import copy - from datetime import timedelta -from asynctest import patch - from homeassistant import config_entries from homeassistant.components import unifi from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, - CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_DEVICES, CONF_TRACK_WIRED_CLIENTS, - CONTROLLER_ID as CONF_CONTROLLER_ID, - UNIFI_CONFIG, - UNIFI_WIRELESS_CLIENTS, -) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, - STATE_UNAVAILABLE, ) +from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component import homeassistant.components.device_tracker as device_tracker import homeassistant.util.dt as dt_util +from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration + DEFAULT_DETECTION_TIME = timedelta(seconds=300) CLIENT_1 = { @@ -88,77 +74,6 @@ DEVICE_2 = { "version": "4.0.42.10433", } -CONTROLLER_DATA = { - CONF_HOST: "mock-host", - CONF_USERNAME: "mock-user", - CONF_PASSWORD: "mock-pswd", - CONF_PORT: 1234, - CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: False, -} - -ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} - -CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") - - -async def setup_unifi_integration( - hass, config, options, clients_response, devices_response, clients_all_response -): - """Create the UniFi controller.""" - hass.data[UNIFI_CONFIG] = [] - hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) - config_entry = config_entries.ConfigEntry( - version=1, - domain=unifi.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, - options=options, - entry_id=1, - ) - - sites = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "viewer"}} - - mock_client_responses = deque() - mock_client_responses.append(clients_response) - - mock_device_responses = deque() - mock_device_responses.append(devices_response) - - mock_client_all_responses = deque() - mock_client_all_responses.append(clients_all_response) - - mock_requests = [] - - async def mock_request(self, method, path, json=None): - mock_requests.append({"method": method, "path": path, "json": json}) - if path == "s/{site}/stat/sta" and mock_client_responses: - return mock_client_responses.popleft() - if path == "s/{site}/stat/device" and mock_device_responses: - return mock_device_responses.popleft() - if path == "s/{site}/rest/user" and mock_client_all_responses: - return mock_client_all_responses.popleft() - return {} - - with patch("aiounifi.Controller.login", return_value=True), patch( - "aiounifi.Controller.sites", return_value=sites - ), patch("aiounifi.Controller.request", new=mock_request): - await unifi.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - - controller_id = unifi.get_controller_id_from_config_entry(config_entry) - controller = hass.data[unifi.DOMAIN][controller_id] - - controller.mock_client_responses = mock_client_responses - controller.mock_device_responses = mock_device_responses - controller.mock_client_all_responses = mock_client_all_responses - - return controller - async def test_platform_manually_configured(hass): """Test that nothing happens when configuring unifi through device tracker platform.""" @@ -177,9 +92,10 @@ async def test_no_clients(hass): hass, ENTRY_CONFIG, options={}, - clients_response={}, - devices_response={}, - clients_all_response={}, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[], ) assert len(hass.states.async_all()) == 2 @@ -191,6 +107,7 @@ async def test_tracked_devices(hass): hass, ENTRY_CONFIG, options={CONF_SSID_FILTER: ["ssid"]}, + sites=SITES, clients_response=[CLIENT_1, CLIENT_2, CLIENT_3], devices_response=[DEVICE_1, DEVICE_2], clients_all_response={}, @@ -267,9 +184,10 @@ async def test_wireless_client_go_wired_issue(hass): hass, ENTRY_CONFIG, options={}, + sites=SITES, clients_response=[client_1_client], - devices_response={}, - clients_all_response={}, + devices_response=[], + clients_all_response=[], ) assert len(hass.states.async_all()) == 3 @@ -316,14 +234,14 @@ async def test_restoring_client(hass): registry.async_get_or_create( device_tracker.DOMAIN, unifi.DOMAIN, - "{}-mock-site".format(CLIENT_1["mac"]), + "{}-site_id".format(CLIENT_1["mac"]), suggested_object_id=CLIENT_1["hostname"], config_entry=config_entry, ) registry.async_get_or_create( device_tracker.DOMAIN, unifi.DOMAIN, - "{}-mock-site".format(CLIENT_2["mac"]), + "{}-site_id".format(CLIENT_2["mac"]), suggested_object_id=CLIENT_2["hostname"], config_entry=config_entry, ) @@ -332,8 +250,9 @@ async def test_restoring_client(hass): hass, ENTRY_CONFIG, options={unifi.CONF_BLOCK_CLIENT: True}, + sites=SITES, clients_response=[CLIENT_2], - devices_response={}, + devices_response=[], clients_all_response=[CLIENT_1], ) assert len(hass.states.async_all()) == 4 @@ -348,9 +267,10 @@ async def test_dont_track_clients(hass): hass, ENTRY_CONFIG, options={unifi.controller.CONF_TRACK_CLIENTS: False}, + sites=SITES, clients_response=[CLIENT_1], devices_response=[DEVICE_1], - clients_all_response={}, + clients_all_response=[], ) assert len(hass.states.async_all()) == 3 @@ -368,9 +288,10 @@ async def test_dont_track_devices(hass): hass, ENTRY_CONFIG, options={unifi.controller.CONF_TRACK_DEVICES: False}, + sites=SITES, clients_response=[CLIENT_1], devices_response=[DEVICE_1], - clients_all_response={}, + clients_all_response=[], ) assert len(hass.states.async_all()) == 3 @@ -388,9 +309,10 @@ async def test_dont_track_wired_clients(hass): hass, ENTRY_CONFIG, options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False}, + sites=SITES, clients_response=[CLIENT_1, CLIENT_2], - devices_response={}, - clients_all_response={}, + devices_response=[], + clients_all_response=[], ) assert len(hass.states.async_all()) == 3 diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index ffd6d97e5b3..845954d8134 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -4,11 +4,7 @@ from unittest.mock import Mock, patch from homeassistant.components import unifi from homeassistant.components.unifi import config_flow from homeassistant.setup import async_setup_component -from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, - CONF_SITE_ID, - CONTROLLER_ID as CONF_CONTROLLER_ID, -) +from homeassistant.components.unifi.const import CONF_CONTROLLER, CONF_SITE_ID from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -117,8 +113,7 @@ async def test_controller_fail_setup(hass): mock_cntrlr.return_value.async_setup.return_value = mock_coro(False) assert await unifi.async_setup_entry(hass, entry) is False - controller_id = CONF_CONTROLLER_ID.format(host="0.0.0.0", site="default") - assert controller_id in hass.data[unifi.DOMAIN] + assert hass.data[unifi.DOMAIN] == {} async def test_controller_no_mac(hass): diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 9064f1c9aba..f591801a966 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -1,29 +1,13 @@ """UniFi sensor platform tests.""" -from collections import deque from copy import deepcopy -from asynctest import patch - -from homeassistant import config_entries from homeassistant.components import unifi -from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, - CONF_SITE_ID, - CONTROLLER_ID as CONF_CONTROLLER_ID, - UNIFI_CONFIG, - UNIFI_WIRELESS_CLIENTS, -) from homeassistant.setup import async_setup_component -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) import homeassistant.components.sensor as sensor +from .test_controller import ENTRY_CONFIG, SITES, setup_unifi_integration + CLIENTS = [ { "hostname": "Wired client hostname", @@ -53,85 +37,6 @@ CLIENTS = [ }, ] -CONTROLLER_DATA = { - CONF_HOST: "mock-host", - CONF_USERNAME: "mock-user", - CONF_PASSWORD: "mock-pswd", - CONF_PORT: 1234, - CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: False, -} - -ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} - -CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") - -SITES = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "admin"}} - - -async def setup_unifi_integration( - hass, - config, - options, - sites, - clients_response, - devices_response, - clients_all_response, -): - """Create the UniFi controller.""" - hass.data[UNIFI_CONFIG] = [] - hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) - config_entry = config_entries.ConfigEntry( - version=1, - domain=unifi.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, - options=options, - entry_id=1, - ) - - mock_client_responses = deque() - mock_client_responses.append(clients_response) - - mock_device_responses = deque() - mock_device_responses.append(devices_response) - - mock_client_all_responses = deque() - mock_client_all_responses.append(clients_all_response) - - mock_requests = [] - - async def mock_request(self, method, path, json=None): - mock_requests.append({"method": method, "path": path, "json": json}) - - if path == "s/{site}/stat/sta" and mock_client_responses: - return mock_client_responses.popleft() - if path == "s/{site}/stat/device" and mock_device_responses: - return mock_device_responses.popleft() - if path == "s/{site}/rest/user" and mock_client_all_responses: - return mock_client_all_responses.popleft() - return {} - - with patch("aiounifi.Controller.login", return_value=True), patch( - "aiounifi.Controller.sites", return_value=sites - ), patch("aiounifi.Controller.request", new=mock_request): - await unifi.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - - controller_id = unifi.get_controller_id_from_config_entry(config_entry) - controller = hass.data[unifi.DOMAIN][controller_id] - - controller.mock_client_responses = mock_client_responses - controller.mock_device_responses = mock_device_responses - controller.mock_client_all_responses = mock_client_all_responses - controller.mock_requests = mock_requests - - return controller - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a controller.""" @@ -160,7 +65,7 @@ async def test_no_clients(hass): assert len(hass.states.async_all()) == 2 -async def test_switches(hass): +async def test_sensors(hass): """Test the update_items function with some clients.""" controller = await setup_unifi_integration( hass, diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index 97dda441527..3d754fb5dff 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -1,32 +1,20 @@ """UniFi POE control platform tests.""" -from collections import deque from copy import deepcopy -from asynctest import Mock, patch - -import aiounifi - from homeassistant import config_entries from homeassistant.components import unifi -from homeassistant.components.unifi.const import ( - CONF_CONTROLLER, - CONF_SITE_ID, - CONTROLLER_ID as CONF_CONTROLLER_ID, - UNIFI_CONFIG, - UNIFI_WIRELESS_CLIENTS, -) from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) import homeassistant.components.switch as switch +from .test_controller import ( + CONTROLLER_HOST, + ENTRY_CONFIG, + SITES, + setup_unifi_integration, +) + CLIENT_1 = { "hostname": "client_1", "ip": "10.0.0.1", @@ -79,19 +67,6 @@ CLIENT_4 = { "wired-rx_bytes": 1234000000, "wired-tx_bytes": 5678000000, } -CLOUDKEY = { - "hostname": "client_1", - "ip": "mock-host", - "is_wired": True, - "last_seen": 1562600145, - "mac": "10:00:00:00:00:01", - "name": "Cloud key", - "oui": "Producer", - "sw_mac": "00:00:00:00:01:01", - "sw_port": 1, - "wired-rx_bytes": 1234000000, - "wired-tx_bytes": 5678000000, -} POE_SWITCH_CLIENTS = [ { "hostname": "client_1", @@ -211,85 +186,6 @@ UNBLOCKED = { "oui": "Producer", } -CONTROLLER_DATA = { - CONF_HOST: "mock-host", - CONF_USERNAME: "mock-user", - CONF_PASSWORD: "mock-pswd", - CONF_PORT: 1234, - CONF_SITE_ID: "mock-site", - CONF_VERIFY_SSL: False, -} - -ENTRY_CONFIG = {CONF_CONTROLLER: CONTROLLER_DATA} - -CONTROLLER_ID = CONF_CONTROLLER_ID.format(host="mock-host", site="mock-site") - -SITES = {"Site name": {"desc": "Site name", "name": "mock-site", "role": "admin"}} - - -async def setup_unifi_integration( - hass, - config, - options, - sites, - clients_response, - devices_response, - clients_all_response, -): - """Create the UniFi controller.""" - hass.data[UNIFI_CONFIG] = [] - hass.data[UNIFI_WIRELESS_CLIENTS] = unifi.UnifiWirelessClients(hass) - config_entry = config_entries.ConfigEntry( - version=1, - domain=unifi.DOMAIN, - title="Mock Title", - data=config, - source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, - options=options, - entry_id=1, - ) - - mock_client_responses = deque() - mock_client_responses.append(clients_response) - - mock_device_responses = deque() - mock_device_responses.append(devices_response) - - mock_client_all_responses = deque() - mock_client_all_responses.append(clients_all_response) - - mock_requests = [] - - async def mock_request(self, method, path, json=None): - mock_requests.append({"method": method, "path": path, "json": json}) - - if path == "s/{site}/stat/sta" and mock_client_responses: - return mock_client_responses.popleft() - if path == "s/{site}/stat/device" and mock_device_responses: - return mock_device_responses.popleft() - if path == "s/{site}/rest/user" and mock_client_all_responses: - return mock_client_all_responses.popleft() - return {} - - with patch("aiounifi.Controller.login", return_value=True), patch( - "aiounifi.Controller.sites", return_value=sites - ), patch("aiounifi.Controller.request", new=mock_request): - await unifi.async_setup_entry(hass, config_entry) - await hass.async_block_till_done() - hass.config_entries._entries.append(config_entry) - - controller_id = unifi.get_controller_id_from_config_entry(config_entry) - controller = hass.data[unifi.DOMAIN][controller_id] - - controller.mock_client_responses = mock_client_responses - controller.mock_device_responses = mock_device_responses - controller.mock_client_all_responses = mock_client_all_responses - controller.mock_requests = mock_requests - - return controller - async def test_platform_manually_configured(hass): """Test that we do not discover anything or try to set up a controller.""" @@ -331,7 +227,7 @@ async def test_controller_not_client(hass): unifi.const.CONF_TRACK_DEVICES: False, }, sites=SITES, - clients_response=[CLOUDKEY], + clients_response=[CONTROLLER_HOST], devices_response=[DEVICE_1], clients_all_response=[], ) @@ -402,7 +298,51 @@ async def test_switches(hass): assert unblocked.state == "on" -async def test_new_client_discovered(hass): +async def test_new_client_discovered_on_block_control(hass): + """Test if 2nd update has a new client.""" + controller = await setup_unifi_integration( + hass, + ENTRY_CONFIG, + options={ + unifi.CONF_BLOCK_CLIENT: [BLOCKED["mac"]], + unifi.const.CONF_TRACK_CLIENTS: False, + unifi.const.CONF_TRACK_DEVICES: False, + }, + sites=SITES, + clients_response=[], + devices_response=[], + clients_all_response=[BLOCKED], + ) + + assert len(controller.mock_requests) == 3 + assert len(hass.states.async_all()) == 4 + + controller.mock_client_all_responses.append([BLOCKED]) + + # Calling a service will trigger the updates to run + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.block_client_1"}, blocking=True + ) + assert len(controller.mock_requests) == 7 + assert len(hass.states.async_all()) == 4 + assert controller.mock_requests[3] == { + "json": {"mac": "00:00:00:00:01:01", "cmd": "block-sta"}, + "method": "post", + "path": "s/{site}/cmd/stamgr/", + } + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.block_client_1"}, blocking=True + ) + assert len(controller.mock_requests) == 11 + assert controller.mock_requests[7] == { + "json": {"mac": "00:00:00:00:01:01", "cmd": "unblock-sta"}, + "method": "post", + "path": "s/{site}/cmd/stamgr/", + } + + +async def test_new_client_discovered_on_poe_control(hass): """Test if 2nd update has a new client.""" controller = await setup_unifi_integration( hass, @@ -530,59 +470,3 @@ async def test_restoring_client(hass): device_1 = hass.states.get("switch.client_1") assert device_1 is not None - - -async def test_failed_update_failed_login(hass): - """Running update can handle a failed login.""" - controller = await setup_unifi_integration( - hass, - ENTRY_CONFIG, - options={ - unifi.CONF_BLOCK_CLIENT: ["random mac"], - unifi.const.CONF_TRACK_CLIENTS: False, - unifi.const.CONF_TRACK_DEVICES: False, - }, - sites=SITES, - clients_response=[], - devices_response=[], - clients_all_response=[], - ) - - assert len(controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 2 - - with patch.object( - controller.api.clients, "update", side_effect=aiounifi.LoginRequired - ), patch.object(controller.api, "login", side_effect=aiounifi.AiounifiException): - await controller.async_update() - await hass.async_block_till_done() - - assert controller.available is False - - -async def test_failed_update_successful_login(hass): - """Running update can login when requested.""" - controller = await setup_unifi_integration( - hass, - ENTRY_CONFIG, - options={ - unifi.CONF_BLOCK_CLIENT: ["random mac"], - unifi.const.CONF_TRACK_CLIENTS: False, - unifi.const.CONF_TRACK_DEVICES: False, - }, - sites=SITES, - clients_response=[], - devices_response=[], - clients_all_response=[], - ) - - assert len(controller.mock_requests) == 3 - assert len(hass.states.async_all()) == 2 - - with patch.object( - controller.api.clients, "update", side_effect=aiounifi.LoginRequired - ), patch.object(controller.api, "login", return_value=Mock(True)): - await controller.async_update() - await hass.async_block_till_done() - - assert controller.available is True From dabdf8b577516bd04168234671b6ada335c7c763 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 7 Oct 2019 22:09:48 +0200 Subject: [PATCH 082/639] Validate generated device triggers (#27264) * Validate generated trigger * Update scaffold --- .../binary_sensor/device_trigger.py | 2 ++ .../components/deconz/device_trigger.py | 5 ++-- .../device_automation/toggle_entity.py | 15 +++++++---- .../components/sensor/device_trigger.py | 9 ++++--- .../components/zha/device_trigger.py | 2 ++ .../integration/device_trigger.py | 26 ++++++++++--------- .../tests/test_device_trigger.py | 7 ++--- 7 files changed, 41 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index e05713b5c67..5d58131fde9 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -191,6 +191,7 @@ async def async_attach_trigger(hass, config, action, automation_info): to_state = "off" state_config = { + state_automation.CONF_PLATFORM: "state", state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], state_automation.CONF_FROM: from_state, state_automation.CONF_TO: to_state, @@ -198,6 +199,7 @@ async def async_attach_trigger(hass, config, action, automation_info): if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] + state_config = state_automation.TRIGGER_SCHEMA(state_config) return await state_automation.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index badbe8b8651..9f66cf156aa 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -222,13 +222,14 @@ async def async_attach_trigger(hass, config, action, automation_info): event_id = deconz_event.serial - state_config = { + event_config = { event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, } + event_config = event.TRIGGER_SCHEMA(event_config) return await event.async_attach_trigger( - hass, state_config, action, automation_info, platform_type="device" + hass, event_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index af29625f3a1..29110144c14 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -3,7 +3,10 @@ from typing import Any, Dict, List import voluptuous as vol from homeassistant.core import Context, HomeAssistant, CALLBACK_TYPE -from homeassistant.components.automation import state, AutomationActionType +from homeassistant.components.automation import ( + state as state_automation, + AutomationActionType, +) from homeassistant.components.device_automation.const import ( CONF_IS_OFF, CONF_IS_ON, @@ -152,14 +155,16 @@ async def async_attach_trigger( from_state = "on" to_state = "off" state_config = { - state.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, - state.CONF_TO: to_state, + state_automation.CONF_PLATFORM: "state", + state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state_automation.CONF_FROM: from_state, + state_automation.CONF_TO: to_state, } if CONF_FOR in config: state_config[CONF_FOR] = config[CONF_FOR] - return await state.async_attach_trigger( + state_config = state_automation.TRIGGER_SCHEMA(state_config) + return await state_automation.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 0d9e8f5af80..bd53dca0c9d 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -87,14 +87,17 @@ TRIGGER_SCHEMA = vol.All( async def async_attach_trigger(hass, config, action, automation_info): """Listen for state changes based on configuration.""" numeric_state_config = { + numeric_state_automation.CONF_PLATFORM: "numeric_state", numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], - numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE), - numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW), - numeric_state_automation.CONF_FOR: config.get(CONF_FOR), } + if CONF_ABOVE in config: + numeric_state_config[numeric_state_automation.CONF_ABOVE] = config[CONF_ABOVE] + if CONF_BELOW in config: + numeric_state_config[numeric_state_automation.CONF_BELOW] = config[CONF_BELOW] if CONF_FOR in config: numeric_state_config[CONF_FOR] = config[CONF_FOR] + numeric_state_config = numeric_state_automation.TRIGGER_SCHEMA(numeric_state_config) return await numeric_state_automation.async_attach_trigger( hass, numeric_state_config, action, automation_info, platform_type="device" ) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index c1ea3c2b761..ddf7465e0c0 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -35,10 +35,12 @@ async def async_attach_trigger(hass, config, action, automation_info): trigger = zha_device.device_automation_triggers[trigger] event_config = { + event.CONF_PLATFORM: "event", event.CONF_EVENT_TYPE: ZHA_EVENT, event.CONF_EVENT_DATA: {DEVICE_IEEE: str(zha_device.ieee), **trigger}, } + event_config = event.TRIGGER_SCHEMA(event_config) return await event.async_attach_trigger( hass, event_config, action, automation_info, platform_type="device" ) diff --git a/script/scaffold/templates/device_trigger/integration/device_trigger.py b/script/scaffold/templates/device_trigger/integration/device_trigger.py index f7e9fc091f8..e0741734d5f 100644 --- a/script/scaffold/templates/device_trigger/integration/device_trigger.py +++ b/script/scaffold/templates/device_trigger/integration/device_trigger.py @@ -12,7 +12,7 @@ from homeassistant.const import ( STATE_OFF, ) from homeassistant.core import HomeAssistant, CALLBACK_TYPE -from homeassistant.helpers import entity_registry +from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType from homeassistant.components.automation import state, AutomationActionType from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA @@ -22,7 +22,10 @@ from . import DOMAIN TRIGGER_TYPES = {"turned_on", "turned_off"} TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( - {vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES)} + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES), + } ) @@ -87,14 +90,13 @@ async def async_attach_trigger( from_state = STATE_ON to_state = STATE_OFF - return state.async_attach_trigger( - hass, - { - CONF_ENTITY_ID: config[CONF_ENTITY_ID], - state.CONF_FROM: from_state, - state.CONF_TO: to_state, - }, - action, - automation_info, - platform_type="device", + state_config = { + state.CONF_PLATFORM: "state", + CONF_ENTITY_ID: config[CONF_ENTITY_ID], + state.CONF_FROM: from_state, + state.CONF_TO: to_state, + } + state_config = state.TRIGGER_SCHEMA(state_config) + return await state.async_attach_trigger( + hass, state_config, action, automation_info, platform_type="device" ) diff --git a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py index c22197bb136..99e1f8937af 100644 --- a/script/scaffold/templates/device_trigger/tests/test_device_trigger.py +++ b/script/scaffold/templates/device_trigger/tests/test_device_trigger.py @@ -1,7 +1,7 @@ """The tests for NEW_NAME device triggers.""" import pytest -from homeassistant.components.switch import DOMAIN +from homeassistant.components.NEW_DOMAIN import DOMAIN from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation @@ -9,6 +9,7 @@ from homeassistant.helpers import device_registry from tests.common import ( MockConfigEntry, + assert_lists_same, async_mock_service, mock_device_registry, mock_registry, @@ -35,7 +36,7 @@ def calls(hass): async def test_get_triggers(hass, device_reg, entity_reg): - """Test we get the expected triggers from a switch.""" + """Test we get the expected triggers from a NEW_DOMAIN.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -60,7 +61,7 @@ async def test_get_triggers(hass, device_reg, entity_reg): }, ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert triggers == expected_triggers + assert_lists_same(triggers, expected_triggers) async def test_if_fires_on_state_change(hass, calls): From 1087abd3b52f73640b189946e9cabdc0887e2410 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 8 Oct 2019 00:32:12 +0000 Subject: [PATCH 083/639] [ci skip] Translation update --- .../components/airly/.translations/fr.json | 14 ++++- .../components/airly/.translations/lb.json | 4 +- .../components/airly/.translations/sl.json | 22 ++++++++ .../airly/.translations/zh-Hant.json | 22 ++++++++ .../binary_sensor/.translations/fr.json | 54 +++++++++++++++++++ .../components/deconz/.translations/fr.json | 1 + .../components/deconz/.translations/no.json | 18 +++---- .../components/deconz/.translations/sl.json | 1 + .../components/ecobee/.translations/fr.json | 24 +++++++++ .../components/met/.translations/pl.json | 2 +- .../components/neato/.translations/ca.json | 27 ++++++++++ .../components/neato/.translations/da.json | 3 +- .../components/neato/.translations/fr.json | 27 ++++++++++ .../components/neato/.translations/it.json | 27 ++++++++++ .../components/neato/.translations/lb.json | 27 ++++++++++ .../components/neato/.translations/no.json | 1 + .../components/neato/.translations/pl.json | 27 ++++++++++ .../components/neato/.translations/ru.json | 3 +- .../components/neato/.translations/sl.json | 17 ++++++ .../neato/.translations/zh-Hant.json | 27 ++++++++++ .../opentherm_gw/.translations/fr.json | 15 ++++-- .../opentherm_gw/.translations/lb.json | 23 ++++++++ .../opentherm_gw/.translations/pl.json | 12 +++++ .../opentherm_gw/.translations/sl.json | 23 ++++++++ .../opentherm_gw/.translations/zh-Hant.json | 23 ++++++++ .../components/plex/.translations/ca.json | 4 ++ .../components/plex/.translations/en.json | 5 ++ .../components/plex/.translations/fr.json | 23 +++++++- .../components/plex/.translations/it.json | 5 ++ .../components/plex/.translations/pl.json | 1 + .../components/plex/.translations/ru.json | 2 +- .../components/plex/.translations/sl.json | 1 + .../rainmachine/.translations/pl.json | 2 +- .../components/sensor/.translations/fr.json | 26 +++++++++ .../components/sensor/.translations/pl.json | 9 ++++ .../components/sensor/.translations/sl.json | 13 +++++ .../components/soma/.translations/fr.json | 13 +++++ .../components/soma/.translations/pl.json | 13 +++++ .../transmission/.translations/fr.json | 40 ++++++++++++++ .../transmission/.translations/ru.json | 2 +- .../components/unifi/.translations/ca.json | 5 ++ .../components/unifi/.translations/fr.json | 5 ++ .../components/unifi/.translations/it.json | 5 ++ .../components/unifi/.translations/lb.json | 5 ++ .../components/unifi/.translations/no.json | 5 ++ .../components/unifi/.translations/pl.json | 5 ++ .../components/unifi/.translations/ru.json | 5 ++ .../components/unifi/.translations/sl.json | 5 ++ .../unifi/.translations/zh-Hant.json | 5 ++ .../components/zha/.translations/fr.json | 27 ++++++++++ .../components/zha/.translations/no.json | 12 ++--- 51 files changed, 658 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/airly/.translations/sl.json create mode 100644 homeassistant/components/airly/.translations/zh-Hant.json create mode 100644 homeassistant/components/binary_sensor/.translations/fr.json create mode 100644 homeassistant/components/ecobee/.translations/fr.json create mode 100644 homeassistant/components/neato/.translations/ca.json create mode 100644 homeassistant/components/neato/.translations/fr.json create mode 100644 homeassistant/components/neato/.translations/it.json create mode 100644 homeassistant/components/neato/.translations/lb.json create mode 100644 homeassistant/components/neato/.translations/pl.json create mode 100644 homeassistant/components/neato/.translations/zh-Hant.json create mode 100644 homeassistant/components/opentherm_gw/.translations/lb.json create mode 100644 homeassistant/components/opentherm_gw/.translations/pl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/sl.json create mode 100644 homeassistant/components/opentherm_gw/.translations/zh-Hant.json create mode 100644 homeassistant/components/sensor/.translations/fr.json create mode 100644 homeassistant/components/sensor/.translations/pl.json create mode 100644 homeassistant/components/sensor/.translations/sl.json create mode 100644 homeassistant/components/soma/.translations/fr.json create mode 100644 homeassistant/components/soma/.translations/pl.json create mode 100644 homeassistant/components/transmission/.translations/fr.json diff --git a/homeassistant/components/airly/.translations/fr.json b/homeassistant/components/airly/.translations/fr.json index 19561130e15..cf756a9f492 100644 --- a/homeassistant/components/airly/.translations/fr.json +++ b/homeassistant/components/airly/.translations/fr.json @@ -1,11 +1,21 @@ { "config": { + "error": { + "auth": "La cl\u00e9 API n'est pas correcte.", + "name_exists": "Le nom existe d\u00e9j\u00e0.", + "wrong_location": "Aucune station de mesure Airly dans cette zone." + }, "step": { "user": { "data": { + "api_key": "Cl\u00e9 API Airly", + "latitude": "Latitude", + "longitude": "Longitude", "name": "Nom de l'int\u00e9gration" - } + }, + "title": "Airly" } - } + }, + "title": "Airly" } } \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/lb.json b/homeassistant/components/airly/.translations/lb.json index ca71c2e647f..08aac57d162 100644 --- a/homeassistant/components/airly/.translations/lb.json +++ b/homeassistant/components/airly/.translations/lb.json @@ -2,7 +2,8 @@ "config": { "error": { "auth": "Api Schl\u00ebssel ass net korrekt.", - "name_exists": "Numm g\u00ebtt et schonn" + "name_exists": "Numm g\u00ebtt et schonn", + "wrong_location": "Keng Airly Moos Statioun an d\u00ebsem Ber\u00e4ich" }, "step": { "user": { @@ -12,6 +13,7 @@ "longitude": "L\u00e4ngegrad", "name": "Numm vun der Installatioun" }, + "description": "Airly Loft Qualit\u00e9it Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle gitt op https://developer.airly.eu/register", "title": "Airly" } }, diff --git a/homeassistant/components/airly/.translations/sl.json b/homeassistant/components/airly/.translations/sl.json new file mode 100644 index 00000000000..08f57d88bcb --- /dev/null +++ b/homeassistant/components/airly/.translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "Klju\u010d API ni pravilen.", + "name_exists": "Ime \u017ee obstaja", + "wrong_location": "Na tem obmo\u010dju ni merilnih postaj Airly." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API klju\u010d", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "name": "Ime integracije" + }, + "description": "Nastavite Airly integracijo za kakovost zraka. \u010ce \u017eelite ustvariti API klju\u010d pojdite na https://developer.airly.eu/register", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/zh-Hant.json b/homeassistant/components/airly/.translations/zh-Hant.json new file mode 100644 index 00000000000..bb38d2b9b8c --- /dev/null +++ b/homeassistant/components/airly/.translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002", + "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", + "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" + }, + "step": { + "user": { + "data": { + "api_key": "Airly API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "name": "\u6574\u5408\u540d\u7a31" + }, + "description": "\u6b32\u8a2d\u5b9a Airly \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://developer.airly.eu/register \u7522\u751f API \u5bc6\u9470", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json new file mode 100644 index 00000000000..80792f16635 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -0,0 +1,54 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batterie faible", + "is_cold": "{entity_name} est froid", + "is_connected": "{entity_name} est connect\u00e9", + "is_gas": "{entity_name} d\u00e9tecte du gaz", + "is_hot": "{entity_name} est chaud", + "is_light": "{entity_name} d\u00e9tecte de la lumi\u00e8re", + "is_locked": "{entity_name} est verrouill\u00e9", + "is_moist": "{entity_name} est humide", + "is_motion": "{entity_name} d\u00e9tecte un mouvement", + "is_moving": "{entity_name} se d\u00e9place", + "is_no_gas": "{entity_name} ne d\u00e9tecte pas de gaz", + "is_no_light": "{entity_name} ne d\u00e9tecte pas de lumi\u00e8re", + "is_no_motion": "{entity_name} ne d\u00e9tecte pas de mouvement", + "is_no_problem": "{entity_name} ne d\u00e9tecte pas de probl\u00e8me", + "is_no_smoke": "{entity_name} ne d\u00e9tecte pas de fum\u00e9e", + "is_no_sound": "{entity_name} ne d\u00e9tecte pas de son", + "is_no_vibration": "{entity_name} ne d\u00e9tecte pas de vibration", + "is_not_bat_low": "{entity_name} batterie normale", + "is_not_cold": "{entity_name} n'est pas froid", + "is_not_connected": "{entity_name} est d\u00e9connect\u00e9", + "is_not_hot": "{entity_name} n'est pas chaud", + "is_not_locked": "{entity_name} est d\u00e9verrouill\u00e9", + "is_not_moist": "{entity_name} est sec", + "is_not_moving": "{entity_name} ne bouge pas", + "is_not_occupied": "{entity_name} n'est pas occup\u00e9", + "is_not_open": "{entity_name} est ferm\u00e9", + "is_not_plugged_in": "{entity_name} est d\u00e9branch\u00e9", + "is_not_powered": "{entity_name} n'est pas aliment\u00e9", + "is_not_present": "{entity_name} n'est pas pr\u00e9sent", + "is_not_unsafe": "{entity_name} est en s\u00e9curit\u00e9", + "is_occupied": "{entity_name} est occup\u00e9", + "is_off": "{entity_name} est d\u00e9sactiv\u00e9", + "is_on": "{entity_name} est activ\u00e9", + "is_open": "{entity_name} est ouvert", + "is_plugged_in": "{entity_name} est branch\u00e9", + "is_powered": "{entity_name} est aliment\u00e9", + "is_present": "{entity_name} est pr\u00e9sent", + "is_problem": "{entity_name} d\u00e9tecte un probl\u00e8me", + "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", + "is_sound": "{entity_name} d\u00e9tecte du son" + }, + "trigger_type": { + "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", + "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", + "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_on": "{entity_name} activ\u00e9", + "unsafe": "{entity_name} est devenu dangereux", + "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index cc6d22945dc..3729f7f556a 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "Bouton \"{subtype}\" quadruple cliqu\u00e9", "remote_button_quintuple_press": "Bouton \"{subtype}\" quintuple cliqu\u00e9", "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9", + "remote_button_rotation_stopped": "La rotation du bouton \" {subtype} \" s'est arr\u00eat\u00e9e", "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index f779f0918fe..7d05a366cf2 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -58,16 +58,16 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { - "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", - "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", - "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\" {subtype} \" - knappen femdobbelt klikket", - "remote_button_rotated": "Knappen roterte \" {subtype} \"", - "remote_button_rotation_stopped": "Knappe rotasjon \"{under type}\" stoppet", - "remote_button_short_press": "\" {subtype} \" -knappen ble trykket", + "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt klikket", + "remote_button_rotated": "Knappen roterte \"{subtype}\"", + "remote_button_rotation_stopped": "Knappe rotasjon \"{subtype}\" stoppet", + "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\" {subtype} \"-knappen trippel klikket", + "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", "remote_gyro_activated": "Enhet er ristet" } }, diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 9aebb2a556f..0717bcfc39f 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", "remote_button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", "remote_button_rotated": "Gumb \"{subtype}\" zasukan", + "remote_button_rotation_stopped": "Vrtenje \"{subtype}\" gumba se je ustavilo", "remote_button_short_press": "Pritisnjen \"{subtype}\" gumb", "remote_button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", "remote_button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen", diff --git a/homeassistant/components/ecobee/.translations/fr.json b/homeassistant/components/ecobee/.translations/fr.json new file mode 100644 index 00000000000..85da5b3a4ec --- /dev/null +++ b/homeassistant/components/ecobee/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "one_instance_only": "Cette int\u00e9gration ne prend actuellement en charge qu'une seule instance ecobee." + }, + "error": { + "pin_request_failed": "Erreur lors de la demande du code PIN \u00e0 ecobee; veuillez v\u00e9rifier que la cl\u00e9 API est correcte.", + "token_request_failed": "Erreur lors de la demande de jetons \u00e0 ecobee; Veuillez r\u00e9essayer." + }, + "step": { + "authorize": { + "title": "Autoriser l'application sur ecobee.com" + }, + "user": { + "data": { + "api_key": "Cl\u00e9 API" + }, + "description": "Veuillez entrer la cl\u00e9 API obtenue aupr\u00e8s d'ecobee.com.", + "title": "Cl\u00e9 API ecobee" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/pl.json b/homeassistant/components/met/.translations/pl.json index d44142213bf..f647dcf7b45 100644 --- a/homeassistant/components/met/.translations/pl.json +++ b/homeassistant/components/met/.translations/pl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Nazwa ju\u017c istnieje" + "name_exists": "Lokalizacja ju\u017c istnieje" }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/ca.json b/homeassistant/components/neato/.translations/ca.json new file mode 100644 index 00000000000..d30f9e5ad4b --- /dev/null +++ b/homeassistant/components/neato/.translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Ja configurat", + "invalid_credentials": "Credencials inv\u00e0lides" + }, + "create_entry": { + "default": "Consulta la [documentaci\u00f3 de Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credencials inv\u00e0lides", + "unexpected_error": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari", + "vendor": "Venedor" + }, + "description": "Consulta la [documentaci\u00f3 de Neato]({docs_url}).", + "title": "Informaci\u00f3 del compte Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json index 61c594e1011..7f0d122f38b 100644 --- a/homeassistant/components/neato/.translations/da.json +++ b/homeassistant/components/neato/.translations/da.json @@ -5,7 +5,8 @@ "invalid_credentials": "Ugyldige legitimationsoplysninger" }, "error": { - "invalid_credentials": "Ugyldige legitimationsoplysninger" + "invalid_credentials": "Ugyldige legitimationsoplysninger", + "unexpected_error": "Uventet fejl" }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/fr.json b/homeassistant/components/neato/.translations/fr.json new file mode 100644 index 00000000000..941ed18660e --- /dev/null +++ b/homeassistant/components/neato/.translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00e9j\u00e0 configur\u00e9", + "invalid_credentials": "Informations d'identification invalides" + }, + "create_entry": { + "default": "Voir [Documentation Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Informations d'identification invalides", + "unexpected_error": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur", + "vendor": "Vendeur" + }, + "description": "Voir [Documentation Neato] ( {docs_url} ).", + "title": "Informations compte Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/it.json b/homeassistant/components/neato/.translations/it.json new file mode 100644 index 00000000000..d5615815dc7 --- /dev/null +++ b/homeassistant/components/neato/.translations/it.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Gi\u00e0 configurato", + "invalid_credentials": "Credenziali non valide" + }, + "create_entry": { + "default": "Vedere la [Documentazione di Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Credenziali non valide", + "unexpected_error": "Errore inaspettato" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente", + "vendor": "Fornitore" + }, + "description": "Vedere la [Documentazione di Neato]({docs_url}).", + "title": "Informazioni sull'account Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/lb.json b/homeassistant/components/neato/.translations/lb.json new file mode 100644 index 00000000000..3043ec6ec37 --- /dev/null +++ b/homeassistant/components/neato/.translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Scho konfigur\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune" + }, + "create_entry": { + "default": "Kuckt [Neato Dokumentatioun]({docs_url})." + }, + "error": { + "invalid_credentials": "Ong\u00eblteg Login Informatioune", + "unexpected_error": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm", + "vendor": "Hiersteller" + }, + "description": "Kuckt [Neato Dokumentatioun]({docs_url}).", + "title": "Neato Kont Informatiounen" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/no.json b/homeassistant/components/neato/.translations/no.json index d869fa59a18..084c4b50e45 100644 --- a/homeassistant/components/neato/.translations/no.json +++ b/homeassistant/components/neato/.translations/no.json @@ -18,6 +18,7 @@ "username": "Brukernavn", "vendor": "Leverand\u00f8r" }, + "description": "Se [Neato dokumentasjon]({docs_url}).", "title": "Neato kontoinformasjon" } }, diff --git a/homeassistant/components/neato/.translations/pl.json b/homeassistant/components/neato/.translations/pl.json new file mode 100644 index 00000000000..caea115b7d5 --- /dev/null +++ b/homeassistant/components/neato/.translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" + }, + "create_entry": { + "default": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url})." + }, + "error": { + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce", + "unexpected_error": "Niespodziewany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika", + "vendor": "Dostawca" + }, + "description": "Zapoznaj si\u0119 z [dokumentacj\u0105 Neato]({docs_url}).", + "title": "Informacje o koncie Neato" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json index 2d08deb2b13..1a206258e24 100644 --- a/homeassistant/components/neato/.translations/ru.json +++ b/homeassistant/components/neato/.translations/ru.json @@ -8,7 +8,8 @@ "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "error": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/sl.json b/homeassistant/components/neato/.translations/sl.json index 1d256918617..7acbb718d17 100644 --- a/homeassistant/components/neato/.translations/sl.json +++ b/homeassistant/components/neato/.translations/sl.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "already_configured": "\u017de konfigurirano", + "invalid_credentials": "Neveljavne poverilnice" + }, + "create_entry": { + "default": "Glejte [neato dokumentacija] ({docs_url})." + }, + "error": { + "invalid_credentials": "Neveljavne poverilnice", + "unexpected_error": "Nepri\u010dakovana napaka" + }, "step": { "user": { + "data": { + "password": "Geslo", + "username": "Uporabni\u0161ko ime", + "vendor": "Prodajalec" + }, + "description": "Glejte [neato dokumentacija] ({docs_url}).", "title": "Podatki o ra\u010dunu Neato" } }, diff --git a/homeassistant/components/neato/.translations/zh-Hant.json b/homeassistant/components/neato/.translations/zh-Hant.json new file mode 100644 index 00000000000..61f49cd5da0 --- /dev/null +++ b/homeassistant/components/neato/.translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "invalid_credentials": "\u6191\u8b49\u7121\u6548" + }, + "create_entry": { + "default": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002" + }, + "error": { + "invalid_credentials": "\u6191\u8b49\u7121\u6548", + "unexpected_error": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "vendor": "\u5ee0\u5546" + }, + "description": "\u8acb\u53c3\u95b1 [Neato \u6587\u4ef6]({docs_url})\u3002", + "title": "Neato \u5e33\u865f\u8cc7\u8a0a" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json index a9f9acd9045..82b9a7aee88 100644 --- a/homeassistant/components/opentherm_gw/.translations/fr.json +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -1,13 +1,22 @@ { "config": { + "error": { + "already_configured": "Passerelle d\u00e9j\u00e0 configur\u00e9e", + "id_exists": "L'identifiant de la passerelle existe d\u00e9j\u00e0", + "serial_error": "Erreur de connexion \u00e0 l'appareil", + "timeout": "La tentative de connexion a expir\u00e9" + }, "step": { "init": { "data": { "device": "Chemin ou URL", "id": "ID", - "name": "Nom" - } + "name": "Nom", + "precision": "Pr\u00e9cision de la temp\u00e9rature climatique" + }, + "title": "Passerelle OpenTherm" } - } + }, + "title": "Passerelle OpenTherm" } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/lb.json b/homeassistant/components/opentherm_gw/.translations/lb.json new file mode 100644 index 00000000000..ec1f719a6cc --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/lb.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ass scho konfigur\u00e9iert", + "id_exists": "Gateway ID g\u00ebtt et schonn", + "serial_error": "Feeler beim verbannen", + "timeout": "Z\u00e4it Iwwerschreidung beim Verbindungs Versuch" + }, + "step": { + "init": { + "data": { + "device": "Pfad oder URL", + "floor_temperature": "Buedem Klima Temperatur", + "id": "ID", + "name": "Numm", + "precision": "Klima Temperatur Prezisioun" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json new file mode 100644 index 00000000000..7e4a0eed013 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "device": "\u015acie\u017cka lub adres URL", + "name": "Nazwa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/sl.json b/homeassistant/components/opentherm_gw/.translations/sl.json new file mode 100644 index 00000000000..5de551d5d0c --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/sl.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "Prehod je \u017ee konfiguriran", + "id_exists": "ID prehoda \u017ee obstaja", + "serial_error": "Napaka pri povezovanju z napravo", + "timeout": "Poskus povezave je potekel" + }, + "step": { + "init": { + "data": { + "device": "Pot ali URL", + "floor_temperature": "Temperatura nadstropja", + "id": "ID", + "name": "Ime", + "precision": "Natan\u010dnost temperature " + }, + "title": "OpenTherm Prehod" + } + }, + "title": "OpenTherm Prehod" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json new file mode 100644 index 00000000000..648f156e864 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "\u9598\u9053\u5668\u5df2\u8a2d\u5b9a\u5b8c\u6210", + "id_exists": "\u9598\u9053\u5668 ID \u5df2\u5b58\u5728", + "serial_error": "\u9023\u7dda\u81f3\u88dd\u7f6e\u932f\u8aa4", + "timeout": "\u9023\u7dda\u5617\u8a66\u903e\u6642" + }, + "step": { + "init": { + "data": { + "device": "\u8def\u5f91\u6216 URL", + "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", + "id": "ID", + "name": "\u540d\u7a31", + "precision": "\u6eab\u63a7\u7cbe\u6e96\u5ea6" + }, + "title": "OpenTherm \u9598\u9053\u5668" + } + }, + "title": "OpenTherm \u9598\u9053\u5668" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index 11e11ebc6fe..a3ba5185371 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -32,6 +32,10 @@ "description": "Hi ha diversos servidors disponibles, selecciona'n un:", "title": "Selecciona servidor Plex" }, + "start_website_auth": { + "description": "Continua l'autoritzaci\u00f3 a plex.tv.", + "title": "Connexi\u00f3 amb el servidor Plex" + }, "user": { "data": { "manual_setup": "Configuraci\u00f3 manual", diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index efdd75b1481..bf927b7f1be 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -4,6 +4,7 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", + "discovery_no_file": "No legacy config file found", "invalid_import": "Imported configuration is invalid", "token_request_timeout": "Timed out obtaining token", "unknown": "Failed for unknown reason" @@ -32,6 +33,10 @@ "description": "Multiple servers available, select one:", "title": "Select Plex server" }, + "start_website_auth": { + "description": "Continue to authorize at plex.tv.", + "title": "Connect Plex server" + }, "user": { "data": { "manual_setup": "Manual setup", diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 812de425ef4..c9e61dcf2e9 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -5,18 +5,25 @@ "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "Plex en cours de configuration", "invalid_import": "La configuration import\u00e9e est invalide", + "token_request_timeout": "D\u00e9lai d'obtention du jeton", "unknown": "\u00c9chec pour une raison inconnue" }, "error": { "faulty_credentials": "L'autorisation \u00e0 \u00e9chou\u00e9e", "no_servers": "Aucun serveur li\u00e9 au compte", + "no_token": "Fournir un jeton ou s\u00e9lectionner l'installation manuelle", "not_found": "Serveur Plex introuvable" }, "step": { "manual_setup": { "data": { - "port": "Port" - } + "host": "H\u00f4te", + "port": "Port", + "ssl": "Utiliser SSL", + "token": "Jeton (si n\u00e9cessaire)", + "verify_ssl": "V\u00e9rifier le certificat SSL" + }, + "title": "Serveur Plex" }, "select_server": { "data": { @@ -27,6 +34,7 @@ }, "user": { "data": { + "manual_setup": "Installation manuelle", "token": "Jeton plex" }, "description": "Entrez un jeton Plex pour la configuration automatique.", @@ -34,5 +42,16 @@ } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Afficher tous les contr\u00f4les", + "use_episode_art": "Utiliser l'art de l'\u00e9pisode" + }, + "description": "Options pour lecteurs multim\u00e9dia Plex" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index 99a6d13e0d4..8f61f968dba 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -4,6 +4,7 @@ "all_configured": "Tutti i server collegati sono gi\u00e0 configurati", "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", "already_in_progress": "Plex \u00e8 in fase di configurazione", + "discovery_no_file": "Nessun file di configurazione legacy trovato", "invalid_import": "La configurazione importata non \u00e8 valida", "token_request_timeout": "Timeout per l'ottenimento del token", "unknown": "Non riuscito per motivo sconosciuto" @@ -32,6 +33,10 @@ "description": "Sono disponibili pi\u00f9 server, selezionarne uno:", "title": "Selezionare il server Plex" }, + "start_website_auth": { + "description": "Continuare ad autorizzare su plex.tv.", + "title": "Collegare il server Plex" + }, "user": { "data": { "manual_setup": "Configurazione manuale", diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index ce9d2e1e88d..9b75a0061e8 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -5,6 +5,7 @@ "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", "already_in_progress": "Plex jest konfigurowany", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", + "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena", "unknown": "Nieznany b\u0142\u0105d" }, "error": { diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 48cacacddfe..53b4bfd9bb5 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -37,7 +37,7 @@ "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "token": "\u0422\u043e\u043a\u0435\u043d" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", + "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", "title": "Plex" } }, diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 49ed34baf76..d6bc85725eb 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -5,6 +5,7 @@ "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", "already_in_progress": "Plex se konfigurira", "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", + "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", "unknown": "Ni uspelo iz neznanega razloga" }, "error": { diff --git a/homeassistant/components/rainmachine/.translations/pl.json b/homeassistant/components/rainmachine/.translations/pl.json index 9ab6156549d..cf842efe9f6 100644 --- a/homeassistant/components/rainmachine/.translations/pl.json +++ b/homeassistant/components/rainmachine/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "Konto jest ju\u017c zarejestrowane", - "invalid_credentials": "Nieprawid\u0142owe po\u015bwiadczenia" + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" }, "step": { "user": { diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json new file mode 100644 index 00000000000..676a5aa413f --- /dev/null +++ b/homeassistant/components/sensor/.translations/fr.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} niveau batterie", + "is_humidity": "{entity_name} humidit\u00e9", + "is_illuminance": "{entity_name} \u00e9clairement", + "is_power": "{entity_name} puissance", + "is_pressure": "{entity_name} pression", + "is_signal_strength": "{entity_name} force du signal", + "is_temperature": "{entity_name} temp\u00e9rature", + "is_timestamp": "{entity_name} horodatage", + "is_value": "{entity_name} valeur" + }, + "trigger_type": { + "battery_level": "{entity_name} niveau batterie", + "humidity": "{entity_name} humidit\u00e9", + "illuminance": "{entity_name} \u00e9clairement", + "power": "{entity_name} puissance", + "pressure": "{entity_name} pression", + "signal_strength": "{entity_name} force du signal", + "temperature": "{entity_name} temp\u00e9rature", + "timestamp": "{entity_name} horodatage", + "value": "{entity_name} valeur" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/pl.json b/homeassistant/components/sensor/.translations/pl.json new file mode 100644 index 00000000000..da1dcc1d6fd --- /dev/null +++ b/homeassistant/components/sensor/.translations/pl.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} poziom na\u0142adowania baterii", + "is_humidity": "{entity_name} wilgotno\u015b\u0107", + "is_temperature": "{entity_name} temperatura" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/sl.json b/homeassistant/components/sensor/.translations/sl.json new file mode 100644 index 00000000000..472af1dfe3b --- /dev/null +++ b/homeassistant/components/sensor/.translations/sl.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} raven baterije", + "is_humidity": "{entity_name} vla\u017enost", + "is_illuminance": "{entity_name} osvetlitev", + "is_power": "{entity_name} mo\u010d", + "is_pressure": "{entity_name} pritisk", + "is_signal_strength": "{entity_name} jakost signala", + "is_temperature": "{entity_name} temperatura" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json new file mode 100644 index 00000000000..e990fb98dc2 --- /dev/null +++ b/homeassistant/components/soma/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Vous ne pouvez configurer qu'un seul compte Soma.", + "authorize_url_timeout": "D\u00e9lai d'attente g\u00e9n\u00e9rant l'autorisation de l'URL.", + "missing_configuration": "Le composant Soma n'est pas configur\u00e9. Veuillez suivre la documentation." + }, + "create_entry": { + "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json new file mode 100644 index 00000000000..0ed881853b8 --- /dev/null +++ b/homeassistant/components/soma/.translations/pl.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "already_setup": "Mo\u017cesz skonfigurowa\u0107 tylko jedno konto Soma.", + "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", + "missing_configuration": "Komponent Soma nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono z Soma" + }, + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/fr.json b/homeassistant/components/transmission/.translations/fr.json new file mode 100644 index 00000000000..e2360c016ca --- /dev/null +++ b/homeassistant/components/transmission/.translations/fr.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + }, + "error": { + "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te", + "wrong_credentials": "Mauvais nom d'utilisateur ou mot de passe" + }, + "step": { + "options": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "title": "Configurer les options" + }, + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur" + }, + "title": "Configuration du client Transmission" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + }, + "description": "Configurer les options pour Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json index 5da2d4f9ef9..e7a438cae11 100644 --- a/homeassistant/components/transmission/.translations/ru.json +++ b/homeassistant/components/transmission/.translations/ru.json @@ -33,7 +33,7 @@ "data": { "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" }, - "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Transmission" } } } diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 3741b035d7a..899b532290e 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -38,6 +38,11 @@ "one": "un", "other": "altre" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Crea sensors d'\u00fas d'ample de banda per a clients de la xarxa" + } } } } diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index 8c2526f8a15..c40b7822073 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -32,6 +32,11 @@ "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)", "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau" + } } } } diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json index 5285ed21873..80b546ebcf8 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -38,6 +38,11 @@ "one": "uno", "other": "altro" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Creare sensori di utilizzo della larghezza di banda per i client di rete" + } } } } diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json index 05b0ffc0c44..4fa1f62c602 100644 --- a/homeassistant/components/unifi/.translations/lb.json +++ b/homeassistant/components/unifi/.translations/lb.json @@ -38,6 +38,11 @@ "one": "Een", "other": "M\u00e9i" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente erstellen" + } } } } diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index c21a47c7ea2..9041f018423 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -38,6 +38,11 @@ "one": "en", "other": "andre" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Opprett b\u00e5ndbreddesensorer for nettverksklienter" + } } } } diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index 6366f82b3da..5887460a8a5 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -40,6 +40,11 @@ "one": "Jeden", "other": "Inne" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Stw\u00f3rz sensory wykorzystania przepustowo\u015bci przez klient\u00f3w sieciowych" + } } } } diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index 2a3a6207cf5..d7451bd81a0 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -32,6 +32,11 @@ "track_devices": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 (\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 Ubiquiti)", "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043f\u0440\u043e\u0432\u043e\u0434\u043d\u043e\u0439 \u0441\u0435\u0442\u0438" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u0421\u043e\u0437\u0434\u0430\u0432\u0430\u0442\u044c \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \u0434\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432" + } } } } diff --git a/homeassistant/components/unifi/.translations/sl.json b/homeassistant/components/unifi/.translations/sl.json index 35000bf4e1f..7084c4609c5 100644 --- a/homeassistant/components/unifi/.translations/sl.json +++ b/homeassistant/components/unifi/.translations/sl.json @@ -40,6 +40,11 @@ "other": "OSTALO", "two": "DVA" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Ustvarite senzorje porabe pasovne \u0161irine za omre\u017ene odjemalce" + } } } } diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index 498afcbb10a..5e0b881af15 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -32,6 +32,11 @@ "track_devices": "\u8ffd\u8e64\u7db2\u8def\u8a2d\u5099\uff08Ubiquiti \u8a2d\u5099\uff09", "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u65b0\u589e\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668" + } } } } diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index 48328aed878..f8b78af5721 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -16,5 +16,32 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "Hurlement", + "warn": "Pr\u00e9venir" + }, + "trigger_subtype": { + "both_buttons": "Les deux boutons", + "button_1": "Premier bouton", + "button_2": "Deuxi\u00e8me bouton", + "button_3": "Troisi\u00e8me bouton", + "button_4": "Quatri\u00e8me bouton", + "button_5": "Cinqui\u00e8me bouton", + "button_6": "Sixi\u00e8me bouton", + "close": "Fermer", + "left": "Gauche", + "open": "Ouvert", + "right": "Droite", + "turn_off": "\u00c9teindre", + "turn_on": "Allumer" + }, + "trigger_type": { + "device_shaken": "Appareil secou\u00e9", + "device_tilted": "Dispositif inclin\u00e9", + "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", + "remote_button_triple_press": "Bouton\"{sous-type}\" \u00e0 trois clics" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 390069b7698..18c4c3c9ff2 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -53,12 +53,12 @@ "device_rotated": "Enheten roterte \"{subtype}\"", "device_shaken": "Enhet er ristet", "device_slid": "Enheten skled \"{subtype}\"", - "device_tilted": "Enhetn skr\u00e5stilt", - "remote_button_double_press": "\" {subtype} \"-knappen ble dobbeltklikket", - "remote_button_long_press": "\" {subtype} \" - knappen ble kontinuerlig trykket", - "remote_button_long_release": "\" {subtype} \" -knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\" {subtype} \" -knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\" {subtype} \" - knappen ble femdobbelt klikket", + "device_tilted": "Enheten skr\u00e5stilt", + "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", + "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", + "remote_button_quintuple_press": "\"{subtype}\"-knappen ble femdobbelt klikket", "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen ble trippel klikket" From c9e26b6fd0cb062678f69fdda43d8e63f31b8092 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 7 Oct 2019 20:08:07 -0700 Subject: [PATCH 084/639] Improve speed websocket sends messages (#27295) * Improve speed websocket sends messages * return -> continue --- homeassistant/components/websocket_api/http.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 17a6709496a..08a0430ee2a 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -61,12 +61,15 @@ class WebSocketHandler: message = await self._to_write.get() if message is None: break + self._logger.debug("Sending %s", message) + + if isinstance(message, str): + await self.wsock.send_str(message) + continue + try: - if isinstance(message, str): - await self.wsock.send_str(message) - else: - await self.wsock.send_json(message, dumps=JSON_DUMP) + dumped = JSON_DUMP(message) except (ValueError, TypeError) as err: self._logger.error( "Unable to serialize to JSON: %s\n%s", err, message @@ -76,6 +79,9 @@ class WebSocketHandler: message["id"], ERR_UNKNOWN_ERROR, "Invalid JSON in response" ) ) + continue + + await self.wsock.send_str(dumped) @callback def _send_message(self, message): From c72ac87c73dc0ea8ce2c1f279c640872c12dbaac Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Oct 2019 05:10:21 +0200 Subject: [PATCH 085/639] Fix device condition scaffold (#27300) --- .../integration/device_condition.py | 35 ++++++++++++++----- .../tests/test_device_condition.py | 7 ++-- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index d19fa8817a0..e9c7e55e23a 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -4,24 +4,28 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_CONDITION, CONF_DOMAIN, CONF_TYPE, - CONF_PLATFORM, CONF_DEVICE_ID, CONF_ENTITY_ID, + STATE_OFF, STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import condition, entity_registry +from homeassistant.helpers import condition, config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA from . import DOMAIN # TODO specify your supported condition types. -CONDITION_TYPES = {"is_on"} +CONDITION_TYPES = {"is_on", "is_off"} CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( - {vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES)} + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } ) @@ -39,13 +43,22 @@ async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[str] # TODO add your own conditions. conditions.append( { - CONF_PLATFORM: "device", + CONF_CONDITION: "device", CONF_DEVICE_ID: device_id, CONF_DOMAIN: DOMAIN, CONF_ENTITY_ID: entry.entity_id, CONF_TYPE: "is_on", } ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_off", + } + ) return conditions @@ -56,9 +69,13 @@ def async_condition_from_config( """Create a function to test a device condition.""" if config_validation: config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_on": + state = STATE_ON + else: + state = STATE_OFF - def test_is_on(hass: HomeAssistant, variables: TemplateVarsType) -> bool: - """Test if an entity is on.""" - return condition.state(hass, config[ATTR_ENTITY_ID], STATE_ON) + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) - return test_is_on + return test_is_state diff --git a/script/scaffold/templates/device_condition/tests/test_device_condition.py b/script/scaffold/templates/device_condition/tests/test_device_condition.py index d9cef083510..1ae4df5f1b7 100644 --- a/script/scaffold/templates/device_condition/tests/test_device_condition.py +++ b/script/scaffold/templates/device_condition/tests/test_device_condition.py @@ -1,7 +1,7 @@ """The tests for NEW_NAME device conditions.""" import pytest -from homeassistant.components.switch import DOMAIN +from homeassistant.components.NEW_DOMAIN import DOMAIN from homeassistant.const import STATE_ON, STATE_OFF from homeassistant.setup import async_setup_component import homeassistant.components.automation as automation @@ -9,6 +9,7 @@ from homeassistant.helpers import device_registry from tests.common import ( MockConfigEntry, + assert_lists_same, async_mock_service, mock_device_registry, mock_registry, @@ -35,7 +36,7 @@ def calls(hass): async def test_get_conditions(hass, device_reg, entity_reg): - """Test we get the expected conditions from a switch.""" + """Test we get the expected conditions from a NEW_DOMAIN.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -60,7 +61,7 @@ async def test_get_conditions(hass, device_reg, entity_reg): }, ] conditions = await async_get_device_automations(hass, "condition", device_entry.id) - assert conditions == expected_conditions + assert_lists_same(conditions, expected_conditions) async def test_if_state(hass, calls): From 50b5dba43eb2ec8270b949554aea8c78d64180d8 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Mon, 7 Oct 2019 22:22:13 -0700 Subject: [PATCH 086/639] Making withings logs less noisy. (#27311) --- homeassistant/components/withings/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index 67cf966c1bc..0293784fd3e 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -397,7 +397,7 @@ class WithingsHealthSensor(Entity): ] if not measure_groups: - _LOGGER.warning("No measure groups found, setting state to %s", None) + _LOGGER.debug("No measure groups found, setting state to %s", None) self._state = None return @@ -417,7 +417,7 @@ class WithingsHealthSensor(Entity): return if not data.series: - _LOGGER.warning("No sleep data, setting state to %s", None) + _LOGGER.debug("No sleep data, setting state to %s", None) self._state = None return @@ -444,7 +444,7 @@ class WithingsHealthSensor(Entity): return if not data.series: - _LOGGER.warning("Sleep data has no series, setting state to %s", None) + _LOGGER.debug("Sleep data has no series, setting state to %s", None) self._state = None return From 15870e0185fef1c0a97d1d342e30dcc015bef2ac Mon Sep 17 00:00:00 2001 From: Brendon Baumgartner Date: Tue, 8 Oct 2019 01:14:17 -0700 Subject: [PATCH 087/639] Do not fail smtp notify service on connection error (#27240) * smtp notify dont disable service * auth fails smtp notify service --- homeassistant/components/smtp/notify.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/smtp/notify.py b/homeassistant/components/smtp/notify.py index 8a96865ab8d..d592f25a61d 100644 --- a/homeassistant/components/smtp/notify.py +++ b/homeassistant/components/smtp/notify.py @@ -136,16 +136,15 @@ class MailNotificationService(BaseNotificationService): server = None try: server = self.connect() - except smtplib.socket.gaierror: + except (smtplib.socket.gaierror, ConnectionRefusedError): _LOGGER.exception( - "SMTP server not found (%s:%s). " - "Please check the IP address or hostname of your SMTP server", + "SMTP server not found or refused connection (%s:%s). " + "Please check the IP address, hostname, and availability of your SMTP server.", self._server, self._port, ) - return False - except (smtplib.SMTPAuthenticationError, ConnectionRefusedError): + except smtplib.SMTPAuthenticationError: _LOGGER.exception( "Login not possible. " "Please check your setting and/or your credentials" From 5645d43bd1981dce5536eb66cef1dbd555509e04 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 8 Oct 2019 21:50:23 +1100 Subject: [PATCH 088/639] move import to top-level (#27314) --- homeassistant/components/transport_nsw/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/transport_nsw/sensor.py b/homeassistant/components/transport_nsw/sensor.py index 9d0610c139e..79df41ac489 100644 --- a/homeassistant/components/transport_nsw/sensor.py +++ b/homeassistant/components/transport_nsw/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging import voluptuous as vol +from TransportNSW import TransportNSW import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -120,8 +121,6 @@ class PublicTransportData: def __init__(self, stop_id, route, destination, api_key): """Initialize the data object.""" - import TransportNSW - self._stop_id = stop_id self._route = route self._destination = destination @@ -134,7 +133,7 @@ class PublicTransportData: ATTR_DESTINATION: "n/a", ATTR_MODE: None, } - self.tnsw = TransportNSW.TransportNSW() + self.tnsw = TransportNSW() def update(self): """Get the next leave time.""" From 15c56f1f64bead00c479d47365d778ec014db787 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 8 Oct 2019 21:50:46 +1100 Subject: [PATCH 089/639] Move imports in geo_rss_events component (#27313) * move imports to top-level * fixed patch path * added myself as codeowner * regenerated codeowners --- CODEOWNERS | 1 + homeassistant/components/geo_rss_events/manifest.json | 6 ++++-- homeassistant/components/geo_rss_events/sensor.py | 8 ++++---- tests/components/geo_rss_events/test_sensor.py | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6e343e91533..070151d01e0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -105,6 +105,7 @@ homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/gearbest/* @HerrHofrat homeassistant/components/geniushub/* @zxdavb +homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff diff --git a/homeassistant/components/geo_rss_events/manifest.json b/homeassistant/components/geo_rss_events/manifest.json index 8fd19f6b034..c681807ad01 100644 --- a/homeassistant/components/geo_rss_events/manifest.json +++ b/homeassistant/components/geo_rss_events/manifest.json @@ -1,10 +1,12 @@ { "domain": "geo_rss_events", - "name": "Geo rss events", + "name": "Geo RSS events", "documentation": "https://www.home-assistant.io/integrations/geo_rss_events", "requirements": [ "georss_generic_client==0.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@exxamalte" + ] } diff --git a/homeassistant/components/geo_rss_events/sensor.py b/homeassistant/components/geo_rss_events/sensor.py index 9f336668142..39e6c5c7e82 100644 --- a/homeassistant/components/geo_rss_events/sensor.py +++ b/homeassistant/components/geo_rss_events/sensor.py @@ -12,6 +12,8 @@ import logging from datetime import timedelta import voluptuous as vol +from georss_client import UPDATE_OK, UPDATE_OK_NO_DATA +from georss_client.generic_feed import GenericFeed import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -108,7 +110,6 @@ class GeoRssServiceSensor(Entity): self._state = None self._state_attributes = None self._unit_of_measurement = unit_of_measurement - from georss_client.generic_feed import GenericFeed self._feed = GenericFeed( coordinates, @@ -146,10 +147,9 @@ class GeoRssServiceSensor(Entity): def update(self): """Update this sensor from the GeoRSS service.""" - import georss_client status, feed_entries = self._feed.update() - if status == georss_client.UPDATE_OK: + if status == UPDATE_OK: _LOGGER.debug( "Adding events to sensor %s: %s", self.entity_id, feed_entries ) @@ -159,7 +159,7 @@ class GeoRssServiceSensor(Entity): for entry in feed_entries: matrix[entry.title] = f"{entry.distance_to_home:.0f}km" self._state_attributes = matrix - elif status == georss_client.UPDATE_OK_NO_DATA: + elif status == UPDATE_OK_NO_DATA: _LOGGER.debug("Update successful, but no data received from %s", self._feed) # Don't change the state or state attributes. else: diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index 3a4c5333ba8..492290b9519 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -59,7 +59,7 @@ class TestGeoRssServiceUpdater(unittest.TestCase): feed_entry.category = category return feed_entry - @mock.patch("georss_client.generic_feed.GenericFeed") + @mock.patch("homeassistant.components.geo_rss_events.sensor.GenericFeed") def test_setup(self, mock_feed): """Test the general setup of the platform.""" # Set up some mock feed entries for this test. @@ -122,7 +122,7 @@ class TestGeoRssServiceUpdater(unittest.TestCase): ATTR_ICON: "mdi:alert", } - @mock.patch("georss_client.generic_feed.GenericFeed") + @mock.patch("homeassistant.components.geo_rss_events.sensor.GenericFeed") def test_setup_with_categories(self, mock_feed): """Test the general setup of the platform.""" # Set up some mock feed entries for this test. From e176f16141f4f08bf42e5fa49cb973b025aa0ae4 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 9 Oct 2019 01:14:50 +1100 Subject: [PATCH 090/639] move import to top-level (#27320) --- homeassistant/components/feedreader/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/feedreader/__init__.py b/homeassistant/components/feedreader/__init__.py index 44ec95f8213..e4ec154620e 100644 --- a/homeassistant/components/feedreader/__init__.py +++ b/homeassistant/components/feedreader/__init__.py @@ -6,6 +6,7 @@ from threading import Lock import pickle import voluptuous as vol +import feedparser from homeassistant.const import EVENT_HOMEASSISTANT_START, CONF_SCAN_INTERVAL from homeassistant.helpers.event import track_time_interval @@ -87,8 +88,6 @@ class FeedManager: def _update(self): """Update the feed and publish new entries to the event bus.""" - import feedparser - _LOGGER.info("Fetching new data from feed %s", self._url) self._feed = feedparser.parse( self._url, From 7a57c3a66af0e47cb6a1f9971dd2b14e6acae1bf Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 8 Oct 2019 16:23:21 +0200 Subject: [PATCH 091/639] Set pytz to >=2019.03 --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a64e0dc38e7..d583dbffe05 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 python-slugify==3.0.4 -pytz>=2019.02 +pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 diff --git a/requirements_all.txt b/requirements_all.txt index 17f39ac1d8d..2782c3709d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -12,7 +12,7 @@ PyJWT==1.7.1 cryptography==2.7 pip>=8.0.3 python-slugify==3.0.4 -pytz>=2019.02 +pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 diff --git a/setup.py b/setup.py index 23a8a808f43..1824ba2d41b 100755 --- a/setup.py +++ b/setup.py @@ -45,7 +45,7 @@ REQUIRES = [ "cryptography==2.7", "pip>=8.0.3", "python-slugify==3.0.4", - "pytz>=2019.02", + "pytz>=2019.03", "pyyaml==5.1.2", "requests==2.22.0", "ruamel.yaml==0.15.100", From df8bf5159888169c437c4f297e59c68e23734f17 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 8 Oct 2019 10:54:01 -0500 Subject: [PATCH 092/639] Fix single Plex server case (#27326) --- homeassistant/components/plex/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index df9e9f9f6c3..6ab11430766 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -57,7 +57,7 @@ class PlexServer: raise ServerNotSpecified(available_servers) server_choice = ( - self._server_name if self._server_name else available_servers[0] + self._server_name if self._server_name else available_servers[0][0] ) connections = account.resource(server_choice).connections local_url = [x.httpuri for x in connections if x.local] From 937d3488674375c5bddcf1585ff9ad64444b98a0 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 8 Oct 2019 18:00:11 +0200 Subject: [PATCH 093/639] Upgrade certifi to >=2019.9.11 (#27323) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d583dbffe05..6bd21ac3849 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,7 +6,7 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.2.0 bcrypt==3.1.7 -certifi>=2019.6.16 +certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2782c3709d3..1369d0ff0e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,7 +4,7 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.2.0 bcrypt==3.1.7 -certifi>=2019.6.16 +certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" importlib-metadata==0.23 jinja2>=2.10.1 diff --git a/setup.py b/setup.py index 1824ba2d41b..f4e94faf553 100755 --- a/setup.py +++ b/setup.py @@ -36,7 +36,7 @@ REQUIRES = [ "async_timeout==3.0.1", "attrs==19.2.0", "bcrypt==3.1.7", - "certifi>=2019.6.16", + "certifi>=2019.9.11", 'contextvars==2.4;python_version<"3.7"', "importlib-metadata==0.23", "jinja2>=2.10.1", From 0a66a03de607313311e7593ddfefbc5e830ecadf Mon Sep 17 00:00:00 2001 From: ottersen <42288505+ottersen@users.noreply.github.com> Date: Tue, 8 Oct 2019 18:27:49 +0200 Subject: [PATCH 094/639] Align user name vs username (#27328) Align to be Username as all other integrations --- homeassistant/components/transmission/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 7160cd109c4..203ed07adb5 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -7,7 +7,7 @@ "data": { "name": "Name", "host": "Host", - "username": "User name", + "username": "Username", "password": "Password", "port": "Port" } @@ -37,4 +37,4 @@ } } } -} \ No newline at end of file +} From 396e68a4b90a092f2ea4987695c4b37e13820da9 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 8 Oct 2019 18:28:37 +0200 Subject: [PATCH 095/639] Upgrade beautifulsoup4 to 4.8.1 (#27325) --- homeassistant/components/scrape/manifest.json | 2 +- homeassistant/components/scrape/sensor.py | 3 +-- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/scrape/manifest.json b/homeassistant/components/scrape/manifest.json index 989070900ca..5fdcca372b9 100644 --- a/homeassistant/components/scrape/manifest.json +++ b/homeassistant/components/scrape/manifest.json @@ -3,7 +3,7 @@ "name": "Scrape", "documentation": "https://www.home-assistant.io/integrations/scrape", "requirements": [ - "beautifulsoup4==4.8.0" + "beautifulsoup4==4.8.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index b6a6fdf4896..0bfb7351c88 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -1,6 +1,7 @@ """Support for getting data from websites with scraping.""" import logging +from bs4 import BeautifulSoup import voluptuous as vol from requests.auth import HTTPBasicAuth, HTTPDigestAuth @@ -124,8 +125,6 @@ class ScrapeSensor(Entity): _LOGGER.error("Unable to retrieve data") return - from bs4 import BeautifulSoup - raw_data = BeautifulSoup(self.rest.data, "html.parser") _LOGGER.debug(raw_data) diff --git a/requirements_all.txt b/requirements_all.txt index 1369d0ff0e6..f6993cc72c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -267,7 +267,7 @@ batinfo==0.4.2 # beacontools[scan]==1.2.3 # homeassistant.components.scrape -beautifulsoup4==4.8.0 +beautifulsoup4==4.8.1 # homeassistant.components.beewi_smartclim beewi_smartclim==0.0.7 From 13956d35164491d59c1c7fa0dbdb369649ba4fcc Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 8 Oct 2019 18:30:18 +0200 Subject: [PATCH 096/639] Upgrade sqlalchemy to 1.3.9 (#27322) --- homeassistant/components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index cdb09d66067..d349560e385 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -3,7 +3,7 @@ "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", "requirements": [ - "sqlalchemy==1.3.8" + "sqlalchemy==1.3.9" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 41d80ebccf9..5a87c813bc5 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -3,7 +3,7 @@ "name": "Sql", "documentation": "https://www.home-assistant.io/integrations/sql", "requirements": [ - "sqlalchemy==1.3.8" + "sqlalchemy==1.3.9" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6bd21ac3849..ae4fd687b94 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.8 +sqlalchemy==1.3.9 voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index f6993cc72c5..100e6f55fe7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1823,7 +1823,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.8 +sqlalchemy==1.3.9 # homeassistant.components.starlingbank starlingbank==3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 425170d168d..c2ac5bb2631 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -429,7 +429,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.8 +sqlalchemy==1.3.9 # homeassistant.components.statsd statsd==3.2.1 From 15c54f34dfebcb45430781d173253398728312e2 Mon Sep 17 00:00:00 2001 From: Evan Bruhn Date: Wed, 9 Oct 2019 03:31:52 +1100 Subject: [PATCH 097/639] Fix Logi Circle cameras not responding to turn on/off commands (#27317) --- homeassistant/components/logi_circle/camera.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index 27b81d8331e..ec8f1595168 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -148,11 +148,11 @@ class LogiCam(Camera): async def async_turn_off(self): """Disable streaming mode for this camera.""" - await self._camera.set_streaming_mode(False) + await self._camera.set_config("streaming", False) async def async_turn_on(self): """Enable streaming mode for this camera.""" - await self._camera.set_streaming_mode(True) + await self._camera.set_config("streaming", True) @property def should_poll(self): From cf555428d099e3d79c34e6b499f310f9ff58962b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 8 Oct 2019 18:33:14 +0200 Subject: [PATCH 098/639] Updated frontend to 20191002.1 (#27329) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 60a4f0faa9c..58e5558781a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191002.0" + "home-assistant-frontend==20191002.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ae4fd687b94..99bb622e989 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 100e6f55fe7..2327d4ce2d5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -643,7 +643,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2ac5bb2631..3bbb834eb55 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -185,7 +185,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.0 +home-assistant-frontend==20191002.1 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 0cfd0388d6f8b9625aa434f1d5cea5de4dba66ca Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Oct 2019 18:57:24 +0200 Subject: [PATCH 099/639] Fix translations for binary_sensor triggers (#27330) --- .../components/binary_sensor/device_trigger.py | 16 ++++++++-------- .../components/binary_sensor/strings.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index 5d58131fde9..c51b9749288 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -81,8 +81,8 @@ CONF_SOUND = "sound" CONF_NO_SOUND = "no_sound" CONF_VIBRATION = "vibration" CONF_NO_VIBRATION = "no_vibration" -CONF_OPEN = "open" -CONF_NOT_OPEN = "not_open" +CONF_OPENED = "opened" +CONF_NOT_OPENED = "not_opened" TURNED_ON = [ @@ -97,7 +97,7 @@ TURNED_ON = [ CONF_MOTION, CONF_MOVING, CONF_OCCUPIED, - CONF_OPEN, + CONF_OPENED, CONF_PLUGGED_IN, CONF_POWERED, CONF_PRESENT, @@ -118,7 +118,7 @@ TURNED_OFF = [ CONF_NOT_MOIST, CONF_NOT_MOVING, CONF_NOT_OCCUPIED, - CONF_NOT_OPEN, + CONF_NOT_OPENED, CONF_NOT_PLUGGED_IN, CONF_NOT_POWERED, CONF_NOT_PRESENT, @@ -141,8 +141,8 @@ ENTITY_TRIGGERS = { {CONF_TYPE: CONF_CONNECTED}, {CONF_TYPE: CONF_NOT_CONNECTED}, ], - DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], - DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], + DEVICE_CLASS_GARAGE_DOOR: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_GAS: [{CONF_TYPE: CONF_GAS}, {CONF_TYPE: CONF_NO_GAS}], DEVICE_CLASS_HEAT: [{CONF_TYPE: CONF_HOT}, {CONF_TYPE: CONF_NOT_HOT}], DEVICE_CLASS_LIGHT: [{CONF_TYPE: CONF_LIGHT}, {CONF_TYPE: CONF_NO_LIGHT}], @@ -154,7 +154,7 @@ ENTITY_TRIGGERS = { {CONF_TYPE: CONF_OCCUPIED}, {CONF_TYPE: CONF_NOT_OCCUPIED}, ], - DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_OPENING: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_PLUG: [{CONF_TYPE: CONF_PLUGGED_IN}, {CONF_TYPE: CONF_NOT_PLUGGED_IN}], DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWERED}, {CONF_TYPE: CONF_NOT_POWERED}], DEVICE_CLASS_PRESENCE: [{CONF_TYPE: CONF_PRESENT}, {CONF_TYPE: CONF_NOT_PRESENT}], @@ -166,7 +166,7 @@ ENTITY_TRIGGERS = { {CONF_TYPE: CONF_VIBRATION}, {CONF_TYPE: CONF_NO_VIBRATION}, ], - DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPEN}, {CONF_TYPE: CONF_NOT_OPEN}], + DEVICE_CLASS_WINDOW: [{CONF_TYPE: CONF_OPENED}, {CONF_TYPE: CONF_NOT_OPENED}], DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_TURNED_ON}, {CONF_TYPE: CONF_TURNED_OFF}], } diff --git a/homeassistant/components/binary_sensor/strings.json b/homeassistant/components/binary_sensor/strings.json index 109a2b1fd45..e01af8d183e 100644 --- a/homeassistant/components/binary_sensor/strings.json +++ b/homeassistant/components/binary_sensor/strings.json @@ -59,7 +59,7 @@ "no_light": "{entity_name} stopped detecting light", "locked": "{entity_name} locked", "not_locked": "{entity_name} unlocked", - "moist§": "{entity_name} became moist", + "moist": "{entity_name} became moist", "not_moist": "{entity_name} became dry", "motion": "{entity_name} started detecting motion", "no_motion": "{entity_name} stopped detecting motion", @@ -84,7 +84,7 @@ "vibration": "{entity_name} started detecting vibration", "no_vibration": "{entity_name} stopped detecting vibration", "opened": "{entity_name} opened", - "closed": "{entity_name} closed", + "not_opened": "{entity_name} closed", "turned_on": "{entity_name} turned on", "turned_off": "{entity_name} turned off" From a51e0d7a5f4e470d3f8e64ff0db1da2d440a5d72 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Oct 2019 09:58:36 -0700 Subject: [PATCH 100/639] Google: Report all states on activating report state (#27312) --- .../components/google_assistant/helpers.py | 5 +++ .../google_assistant/report_state.py | 24 +++++++++++++- .../google_assistant/test_report_state.py | 31 ++++++++++++++++--- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 207194d79ed..933f0c07999 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -182,6 +182,11 @@ class GoogleEntity: ] return self._traits + @callback + def should_expose(self): + """If entity should be exposed.""" + return self.config.should_expose(self.state) + @callback def is_supported(self) -> bool: """Return if the entity is supported by Google.""" diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 33bb16d7830..869bc61d7a3 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,8 +1,13 @@ """Google Report State implementation.""" from homeassistant.core import HomeAssistant, callback from homeassistant.const import MATCH_ALL +from homeassistant.helpers.event import async_call_later -from .helpers import AbstractConfig, GoogleEntity +from .helpers import AbstractConfig, GoogleEntity, async_get_entities + +# Time to wait until the homegraph updates +# https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 +INITIAL_REPORT_DELAY = 60 @callback @@ -34,6 +39,23 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig {"devices": {"states": {changed_entity: entity_data}}} ) + async_call_later( + hass, INITIAL_REPORT_DELAY, _async_report_all_states(hass, google_config) + ) + return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener ) + + +async def _async_report_all_states(hass: HomeAssistant, google_config: AbstractConfig): + """Report all states.""" + entities = {} + + for entity in async_get_entities(hass, google_config): + if not entity.should_expose(): + continue + + entities[entity.entity_id] = entity.query_serialize() + + await google_config.async_report_state({"devices": {"states": entities}}) diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index bd59502a3a1..734d9ec7fc8 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,17 +1,38 @@ """Test Google report state.""" from unittest.mock import patch -from homeassistant.components.google_assistant.report_state import ( - async_enable_report_state, -) +from homeassistant.components.google_assistant import report_state +from homeassistant.util.dt import utcnow + from . import BASIC_CONFIG -from tests.common import mock_coro + +from tests.common import mock_coro, async_fire_time_changed async def test_report_state(hass): """Test report state works.""" - unsub = async_enable_report_state(hass, BASIC_CONFIG) + hass.states.async_set("light.ceiling", "off") + hass.states.async_set("switch.ac", "on") + + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report, patch.object(report_state, "INITIAL_REPORT_DELAY", 0): + unsub = report_state.async_enable_report_state(hass, BASIC_CONFIG) + + async_fire_time_changed(hass, utcnow()) + await hass.async_block_till_done() + + # Test that enabling report state does a report on all entities + assert len(mock_report.mock_calls) == 1 + assert mock_report.mock_calls[0][1][0] == { + "devices": { + "states": { + "light.ceiling": {"on": False, "online": True}, + "switch.ac": {"on": True, "online": True}, + } + } + } with patch.object( BASIC_CONFIG, "async_report_state", side_effect=mock_coro From f5bd0f29b41bacdccddb49f05db9736580a09ce2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 8 Oct 2019 09:59:32 -0700 Subject: [PATCH 101/639] Add scene.apply service (#27298) * Add scene.apply service * Use return value entity ID validator" * Require entities field in service call * Simplify scene service --- .../components/homeassistant/scene.py | 91 +++++++++++-------- homeassistant/components/scene/__init__.py | 17 +--- homeassistant/components/scene/services.yaml | 16 +++- tests/components/homeassistant/test_scene.py | 23 +++++ 4 files changed, 93 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/homeassistant/scene.py b/homeassistant/components/homeassistant/scene.py index 66b04109640..39b04f6d3ea 100644 --- a/homeassistant/components/homeassistant/scene.py +++ b/homeassistant/components/homeassistant/scene.py @@ -26,6 +26,36 @@ from homeassistant.helpers import ( from homeassistant.helpers.state import HASS_DOMAIN, async_reproduce_state from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN, STATES, Scene + +def _convert_states(states): + """Convert state definitions to State objects.""" + result = {} + + for entity_id in states: + entity_id = cv.entity_id(entity_id) + + if isinstance(states[entity_id], dict): + entity_attrs = states[entity_id].copy() + state = entity_attrs.pop(ATTR_STATE, None) + attributes = entity_attrs + else: + state = states[entity_id] + attributes = {} + + # YAML translates 'on' to a boolean + # http://yaml.org/type/bool.html + if isinstance(state, bool): + state = STATE_ON if state else STATE_OFF + elif not isinstance(state, str): + raise vol.Invalid(f"State for {entity_id} should be a string") + + result[entity_id] = State(entity_id, state, attributes) + + return result + + +STATES_SCHEMA = vol.All(dict, _convert_states) + PLATFORM_SCHEMA = vol.Schema( { vol.Required(CONF_PLATFORM): HASS_DOMAIN, @@ -34,9 +64,7 @@ PLATFORM_SCHEMA = vol.Schema( [ { vol.Required(CONF_NAME): cv.string, - vol.Required(CONF_ENTITIES): { - cv.entity_id: vol.Any(str, bool, dict) - }, + vol.Required(CONF_ENTITIES): STATES_SCHEMA, } ], ), @@ -44,6 +72,7 @@ PLATFORM_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +SERVICE_APPLY = "apply" SCENECONFIG = namedtuple("SceneConfig", [CONF_NAME, STATES]) _LOGGER = logging.getLogger(__name__) @@ -87,6 +116,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= SCENE_DOMAIN, SERVICE_RELOAD, reload_config ) + async def apply_service(call): + """Apply a scene.""" + await async_reproduce_state( + hass, call.data[CONF_ENTITIES].values(), blocking=True, context=call.context + ) + + hass.services.async_register( + SCENE_DOMAIN, + SERVICE_APPLY, + apply_service, + vol.Schema({vol.Required(CONF_ENTITIES): STATES_SCHEMA}), + ) + def _process_scenes_config(hass, async_add_entities, config): """Process multiple scenes and add them.""" @@ -97,41 +139,11 @@ def _process_scenes_config(hass, async_add_entities, config): return async_add_entities( - HomeAssistantScene(hass, _process_scene_config(scene)) for scene in scene_config + HomeAssistantScene(hass, SCENECONFIG(scene[CONF_NAME], scene[CONF_ENTITIES])) + for scene in scene_config ) -def _process_scene_config(scene_config): - """Process passed in config into a format to work with. - - Async friendly. - """ - name = scene_config.get(CONF_NAME) - - states = {} - c_entities = dict(scene_config.get(CONF_ENTITIES, {})) - - for entity_id in c_entities: - if isinstance(c_entities[entity_id], dict): - entity_attrs = c_entities[entity_id].copy() - state = entity_attrs.pop(ATTR_STATE, None) - attributes = entity_attrs - else: - state = c_entities[entity_id] - attributes = {} - - # YAML translates 'on' to a boolean - # http://yaml.org/type/bool.html - if isinstance(state, bool): - state = STATE_ON if state else STATE_OFF - else: - state = str(state) - - states[entity_id.lower()] = State(entity_id, state, attributes) - - return SCENECONFIG(name, states) - - class HomeAssistantScene(Scene): """A scene is a group of entities and the states we want them to be.""" @@ -148,8 +160,13 @@ class HomeAssistantScene(Scene): @property def device_state_attributes(self): """Return the scene state attributes.""" - return {ATTR_ENTITY_ID: list(self.scene_config.states.keys())} + return {ATTR_ENTITY_ID: list(self.scene_config.states)} async def async_activate(self): """Activate scene. Try to get entities into requested state.""" - await async_reproduce_state(self.hass, self.scene_config.states.values(), True) + await async_reproduce_state( + self.hass, + self.scene_config.states.values(), + blocking=True, + context=self._context, + ) diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 1f71a24c304..ec2dc3118a9 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -1,5 +1,4 @@ """Allow users to set and activate scenes.""" -import asyncio import importlib import logging @@ -7,7 +6,6 @@ import voluptuous as vol from homeassistant.core import DOMAIN as HA_DOMAIN from homeassistant.const import CONF_PLATFORM, SERVICE_TURN_ON -from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.state import HASS_DOMAIN @@ -69,20 +67,7 @@ async def async_setup(hass, config): HA_DOMAIN, {"platform": "homeasistant", STATES: []} ) - async def async_handle_scene_service(service): - """Handle calls to the switch services.""" - target_scenes = await component.async_extract_from_service(service) - - tasks = [scene.async_activate() for scene in target_scenes] - if tasks: - await asyncio.wait(tasks) - - hass.services.async_register( - DOMAIN, - SERVICE_TURN_ON, - async_handle_scene_service, - schema=ENTITY_SERVICE_SCHEMA, - ) + component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_activate") return True diff --git a/homeassistant/components/scene/services.yaml b/homeassistant/components/scene/services.yaml index ee255affe44..0f1e7103aaf 100644 --- a/homeassistant/components/scene/services.yaml +++ b/homeassistant/components/scene/services.yaml @@ -5,4 +5,18 @@ turn_on: fields: entity_id: description: Name(s) of scenes to turn on - example: 'scene.romantic' + example: "scene.romantic" + +reload: + description: Reload the scene configuration + +apply: + description: Activate a scene. Takes same data as the entities field from a single scene in the config. + fields: + entities: + description: The entities and the state that they need to be. + example: + light.kitchen: "on" + light.ceiling: + state: "on" + brightness: 80 diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 02c018a0b49..c7c3f2bc5d5 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -28,3 +28,26 @@ async def test_reload_config_service(hass): assert hass.states.get("scene.hallo") is None assert hass.states.get("scene.bye") is not None + + +async def test_apply_service(hass): + """Test the apply service.""" + assert await async_setup_component(hass, "scene", {}) + assert await async_setup_component(hass, "light", {"light": {"platform": "demo"}}) + + assert await hass.services.async_call( + "scene", "apply", {"entities": {"light.bed_light": "off"}}, blocking=True + ) + + assert hass.states.get("light.bed_light").state == "off" + + assert await hass.services.async_call( + "scene", + "apply", + {"entities": {"light.bed_light": {"state": "on", "brightness": 50}}}, + blocking=True, + ) + + state = hass.states.get("light.bed_light") + assert state.state == "on" + assert state.attributes["brightness"] == 50 From 1a9d07dbdc620674be96ce7ed77087602c538478 Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 8 Oct 2019 19:05:35 +0200 Subject: [PATCH 102/639] Improve Neato login process (#27327) * initial commit * Readded log message * Clean up try-except --- homeassistant/components/neato/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index c1fb128a1d1..14090c99a55 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -145,28 +145,26 @@ class NeatoHub: def login(self): """Login to My Neato.""" + _LOGGER.debug("Trying to connect to Neato API") try: - _LOGGER.debug("Trying to connect to Neato API") self.my_neato = self._neato( self.config[CONF_USERNAME], self.config[CONF_PASSWORD], self._vendor ) - self.logged_in = True - - _LOGGER.debug("Successfully connected to Neato API") - self._hass.data[NEATO_ROBOTS] = self.my_neato.robots - self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps - self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps except NeatoException as ex: if isinstance(ex, NeatoLoginException): _LOGGER.error("Invalid credentials") else: _LOGGER.error("Unable to connect to Neato API") self.logged_in = False + return + + self.logged_in = True + _LOGGER.debug("Successfully connected to Neato API") @Throttle(timedelta(minutes=SCAN_INTERVAL_MINUTES)) def update_robots(self): """Update the robot states.""" - _LOGGER.debug("Running HUB.update_robots %s", self._hass.data[NEATO_ROBOTS]) + _LOGGER.debug("Running HUB.update_robots %s", self._hass.data.get(NEATO_ROBOTS)) self._hass.data[NEATO_ROBOTS] = self.my_neato.robots self._hass.data[NEATO_PERSISTENT_MAPS] = self.my_neato.persistent_maps self._hass.data[NEATO_MAP_DATA] = self.my_neato.maps From 0ba4ee139838b48c76f23b032146f65218e4fd3a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Oct 2019 19:06:17 +0200 Subject: [PATCH 103/639] Validate generated device actions (#27262) * Validate generated actions * Use hass.services.async_call instead of service.async_call_from_config --- .../components/device_automation/toggle_entity.py | 12 +++++------- homeassistant/components/zha/device_action.py | 11 ++++------- .../device_action/tests/test_device_action.py | 5 +++-- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 29110144c14..98a1af9c4ca 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -17,6 +17,7 @@ from homeassistant.components.device_automation.const import ( CONF_TURNED_ON, ) from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_CONDITION, CONF_ENTITY_ID, CONF_FOR, @@ -24,7 +25,7 @@ from homeassistant.const import ( CONF_TYPE, ) from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers import condition, config_validation as cv, service +from homeassistant.helpers import condition, config_validation as cv from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import TRIGGER_BASE_SCHEMA @@ -112,13 +113,10 @@ async def async_call_action_from_config( else: action = "toggle" - service_action = { - service.CONF_SERVICE: "{}.{}".format(domain, action), - CONF_ENTITY_ID: config[CONF_ENTITY_ID], - } + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} - await service.async_call_from_config( - hass, service_action, blocking=True, variables=variables, context=context + await hass.services.async_call( + domain, action, service_data, blocking=True, context=context ) diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index 460676a75a0..60cfa0eec00 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -5,7 +5,7 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import config_validation as cv, service +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType, TemplateVarsType from . import DOMAIN @@ -78,13 +78,10 @@ async def _execute_service_based_action( service_name = SERVICE_NAMES[action_type] zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) - service_action = { - service.CONF_SERVICE: "{}.{}".format(DOMAIN, service_name), - ATTR_DATA: {ATTR_IEEE: str(zha_device.ieee)}, - } + service_data = {ATTR_IEEE: str(zha_device.ieee)} - await service.async_call_from_config( - hass, service_action, blocking=True, variables=variables, context=context + await hass.services.async_call( + DOMAIN, service_name, service_data, blocking=True, context=context ) diff --git a/script/scaffold/templates/device_action/tests/test_device_action.py b/script/scaffold/templates/device_action/tests/test_device_action.py index f8a00bf1ec8..b65c8257531 100644 --- a/script/scaffold/templates/device_action/tests/test_device_action.py +++ b/script/scaffold/templates/device_action/tests/test_device_action.py @@ -8,6 +8,7 @@ from homeassistant.helpers import device_registry from tests.common import ( MockConfigEntry, + assert_lists_same, async_mock_service, mock_device_registry, mock_registry, @@ -28,7 +29,7 @@ def entity_reg(hass): async def test_get_actions(hass, device_reg, entity_reg): - """Test we get the expected actions from a switch.""" + """Test we get the expected actions from a NEW_DOMAIN.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( @@ -51,7 +52,7 @@ async def test_get_actions(hass, device_reg, entity_reg): }, ] actions = await async_get_device_automations(hass, "action", device_entry.id) - assert actions == expected_actions + assert_lists_same(actions, expected_actions) async def test_action(hass): From 55e10d552e28f9c8414a2c432b95f482dec46899 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Tue, 8 Oct 2019 19:52:43 +0200 Subject: [PATCH 104/639] Cleanup handling of attributes for HomematicIP Cloud (#27331) * Cleanup handling of attributes for HomematicIP Cloud * Remove special climate handling --- .../components/homematicip_cloud/binary_sensor.py | 5 ++--- .../components/homematicip_cloud/device.py | 15 ++++++++++++++- .../components/homematicip_cloud/sensor.py | 10 +++++++--- .../components/homematicip_cloud/switch.py | 6 ++++-- .../components/homematicip_cloud/weather.py | 2 +- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index b3a21ba0b5c..1114f10b622 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -40,7 +40,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP, ATTR_MODEL_TYPE +from .device import ATTR_GROUP_MEMBER_UNREACHABLE _LOGGER = logging.getLogger(__name__) @@ -60,7 +60,6 @@ ATTR_WINDOW_STATE = "window_state" GROUP_ATTRIBUTES = { "lowBat": ATTR_LOW_BATTERY, - "modelType": ATTR_MODEL_TYPE, "moistureDetected": ATTR_MOISTURE_DETECTED, "motionDetected": ATTR_MOTION_DETECTED, "powerMainsFailure": ATTR_POWER_MAINS_FAILURE, @@ -353,7 +352,7 @@ class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorD @property def device_state_attributes(self): """Return the state attributes of the security zone group.""" - state_attr = {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: True} + state_attr = super().device_state_attributes for attr, attr_key in GROUP_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 1273278189d..0389e0b9935 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -3,6 +3,7 @@ import logging from typing import Optional from homematicip.aio.device import AsyncDevice +from homematicip.aio.group import AsyncGroup from homematicip.aio.home import AsyncHome from homeassistant.components import homematicip_cloud @@ -13,6 +14,7 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" +ATTR_GROUP_ID = "group_id" ATTR_ID = "id" ATTR_IS_GROUP = "is_group" # RSSI HAP -> Device @@ -35,15 +37,17 @@ DEVICE_ATTRIBUTE_ICONS = { DEVICE_ATTRIBUTES = { "modelType": ATTR_MODEL_TYPE, - "id": ATTR_ID, "sabotage": ATTR_SABOTAGE, "rssiDeviceValue": ATTR_RSSI_DEVICE, "rssiPeerValue": ATTR_RSSI_PEER, "deviceOverheated": ATTR_DEVICE_OVERHEATED, "deviceOverloaded": ATTR_DEVICE_OVERLOADED, "deviceUndervoltage": ATTR_DEVICE_UNTERVOLTAGE, + "id": ATTR_ID, } +GROUP_ATTRIBUTES = {"modelType": ATTR_MODEL_TYPE, "id": ATTR_GROUP_ID} + class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" @@ -173,6 +177,7 @@ class HomematicipGenericDevice(Entity): def device_state_attributes(self): """Return the state attributes of the generic device.""" state_attr = {} + if isinstance(self._device, AsyncDevice): for attr, attr_key in DEVICE_ATTRIBUTES.items(): attr_value = getattr(self._device, attr, None) @@ -181,4 +186,12 @@ class HomematicipGenericDevice(Entity): state_attr[ATTR_IS_GROUP] = False + if isinstance(self._device, AsyncGroup): + for attr, attr_key in GROUP_ATTRIBUTES.items(): + attr_value = getattr(self._device, attr, None) + if attr_value: + state_attr[attr_key] = attr_value + + state_attr[ATTR_IS_GROUP] = True + return state_attr diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 30e910cc33a..ceb7fc39fd7 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -115,7 +115,6 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): def __init__(self, home: AsyncHome) -> None: """Initialize access point device.""" - home.modelType = "HmIP-HAP" super().__init__(home, home) @property @@ -152,7 +151,12 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): @property def device_state_attributes(self): """Return the state attributes of the access point.""" - return {ATTR_MODEL_TYPE: self._device.modelType, ATTR_IS_GROUP: False} + state_attr = super().device_state_attributes + + state_attr[ATTR_MODEL_TYPE] = "HmIP-HAP" + state_attr[ATTR_IS_GROUP] = False + + return state_attr class HomematicipHeatingThermostat(HomematicipGenericDevice): @@ -316,7 +320,7 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): state_attr = super().device_state_attributes wind_direction = getattr(self._device, "windDirection", None) - if wind_direction: + if wind_direction is not None: state_attr[ATTR_WIND_DIRECTION] = _get_wind_direction(wind_direction) wind_direction_variation = getattr(self._device, "windDirectionVariation", None) diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index ababf793f0c..7994aa446b8 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -19,7 +19,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice -from .device import ATTR_GROUP_MEMBER_UNREACHABLE, ATTR_IS_GROUP +from .device import ATTR_GROUP_MEMBER_UNREACHABLE _LOGGER = logging.getLogger(__name__) @@ -113,9 +113,11 @@ class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): @property def device_state_attributes(self): """Return the state attributes of the switch-group.""" - state_attr = {ATTR_IS_GROUP: True} + state_attr = super().device_state_attributes + if self._device.unreach: state_attr[ATTR_GROUP_MEMBER_UNREACHABLE] = True + return state_attr async def async_turn_on(self, **kwargs): diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index ed9098559a3..0d020312fe9 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -123,7 +123,7 @@ class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity): def __init__(self, home: AsyncHome) -> None: """Initialize the home weather.""" - home.weather.modelType = "HmIP-Home-Weather" + home.modelType = "HmIP-Home-Weather" super().__init__(home, home) @property From 071476343c10cdf8fe4a448082021eca3df7c8d7 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Tue, 8 Oct 2019 11:14:52 -0700 Subject: [PATCH 105/639] Fix connection issues with withings API by switching to a maintained codebase (#27310) * Fixing connection issues with withings API by switching to a maintained client codebase. * Updating requirements files. * Adding withings api to requirements script. --- homeassistant/components/withings/common.py | 14 +- .../components/withings/config_flow.py | 4 +- .../components/withings/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- script/gen_requirements_all.py | 2 +- tests/components/withings/common.py | 12 +- tests/components/withings/conftest.py | 139 +++++++++--------- tests/components/withings/test_common.py | 20 +-- tests/components/withings/test_sensor.py | 44 +++--- 10 files changed, 131 insertions(+), 118 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index f2be849cbc7..9acca6f0cd6 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import logging import re import time -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import TokenUpdated @@ -68,7 +68,9 @@ class WithingsDataManager: service_available = None - def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): + def __init__( + self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi + ): """Constructor.""" self._hass = hass self._api = api @@ -253,7 +255,7 @@ def create_withings_data_manager( """Set up the sensor config entry.""" entry_creds = entry.data.get(const.CREDENTIALS) or {} profile = entry.data[const.PROFILE] - credentials = nokia.NokiaCredentials( + credentials = withings.WithingsCredentials( entry_creds.get("access_token"), entry_creds.get("token_expiry"), entry_creds.get("token_type"), @@ -266,7 +268,7 @@ def create_withings_data_manager( def credentials_saver(credentials_param): _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - # Sanitizing the data as sometimes a NokiaCredentials object + # Sanitizing the data as sometimes a WithingsCredentials object # is passed through from the API. cred_data = credentials_param if not isinstance(credentials_param, dict): @@ -275,8 +277,8 @@ def create_withings_data_manager( entry.data[const.CREDENTIALS] = cred_data hass.config_entries.async_update_entry(entry, data={**entry.data}) - _LOGGER.debug("Creating nokia api instance") - api = nokia.NokiaApi( + _LOGGER.debug("Creating withings api instance") + api = withings.WithingsApi( credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index f28a4f59d80..c781e785f5e 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ import logging from typing import Optional import aiohttp -import nokia +import withings_api as withings import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -75,7 +75,7 @@ class WithingsFlowHandler(config_entries.ConfigFlow): profile, ) - return nokia.NokiaAuth( + return withings.WithingsAuth( client_id, client_secret, callback_uri, diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index d38b69f2248..ae5cd4bcdd9 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "nokia==1.2.0" + "withings-api==2.0.0b7" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index 2327d4ce2d5..7d4ebb0074e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,9 +868,6 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 @@ -1976,6 +1973,9 @@ websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 +# homeassistant.components.withings +withings-api==2.0.0b7 + # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bbb834eb55..048601b3b99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,9 +231,6 @@ minio==4.0.9 # homeassistant.components.ssdp netdisco==2.6.0 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow @@ -457,6 +454,9 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==1.1.6 +# homeassistant.components.withings +withings-api==2.0.0b7 + # homeassistant.components.zeroconf zeroconf==0.23.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 61174e86a43..e8837b8d295 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -106,7 +106,6 @@ TEST_REQUIREMENTS = ( "mficlient", "minio", "netdisco", - "nokia", "numpy", "oauth2client", "paho-mqtt", @@ -185,6 +184,7 @@ TEST_REQUIREMENTS = ( "vultr", "wakeonlan", "warrant", + "withings-api", "YesssSMS", "zeroconf", "zigpy-homeassistant", diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b8406c39711..f3839a1be55 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,7 +1,7 @@ """Common data for for the withings component tests.""" import time -import nokia +import withings_api as withings import homeassistant.components.withings.const as const @@ -92,7 +92,7 @@ def new_measure(type_str, value, unit): } -def nokia_sleep_response(states): +def withings_sleep_response(states): """Create a sleep response based on states.""" data = [] for state in states: @@ -104,10 +104,10 @@ def nokia_sleep_response(states): ) ) - return nokia.NokiaSleep(new_sleep_data("aa", data)) + return withings.WithingsSleep(new_sleep_data("aa", data)) -NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( +WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( { "updatetime": "", "timezone": "", @@ -174,7 +174,7 @@ NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( ) -NOKIA_SLEEP_RESPONSE = nokia_sleep_response( +WITHINGS_SLEEP_RESPONSE = withings_sleep_response( [ const.MEASURE_TYPE_SLEEP_STATE_AWAKE, const.MEASURE_TYPE_SLEEP_STATE_LIGHT, @@ -183,7 +183,7 @@ NOKIA_SLEEP_RESPONSE = nokia_sleep_response( ] ) -NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( +WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( { "series": [ new_sleep_summary( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 7cbe3dc1cd4..0aa6af0d7c0 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ import time from typing import Awaitable, Callable, List import asynctest -import nokia +import withings_api as withings import pytest import homeassistant.components.api as api @@ -15,9 +15,9 @@ from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC from homeassistant.setup import async_setup_component from .common import ( - NOKIA_MEASURES_RESPONSE, - NOKIA_SLEEP_RESPONSE, - NOKIA_SLEEP_SUMMARY_RESPONSE, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_SLEEP_RESPONSE, + WITHINGS_SLEEP_SUMMARY_RESPONSE, ) @@ -34,17 +34,17 @@ class WithingsFactoryConfig: measures: List[str] = None, unit_system: str = None, throttle_interval: int = const.THROTTLE_INTERVAL, - nokia_request_response="DATA", - nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, - nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, - nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, + withings_request_response="DATA", + withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, + withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, + withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, ) -> None: """Constructor.""" self._throttle_interval = throttle_interval - self._nokia_request_response = nokia_request_response - self._nokia_measures_response = nokia_measures_response - self._nokia_sleep_response = nokia_sleep_response - self._nokia_sleep_summary_response = nokia_sleep_summary_response + self._withings_request_response = withings_request_response + self._withings_measures_response = withings_measures_response + self._withings_sleep_response = withings_sleep_response + self._withings_sleep_summary_response = withings_sleep_summary_response self._withings_config = { const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret", @@ -103,24 +103,24 @@ class WithingsFactoryConfig: return self._throttle_interval @property - def nokia_request_response(self): + def withings_request_response(self): """Request response.""" - return self._nokia_request_response + return self._withings_request_response @property - def nokia_measures_response(self) -> nokia.NokiaMeasures: + def withings_measures_response(self) -> withings.WithingsMeasures: """Measures response.""" - return self._nokia_measures_response + return self._withings_measures_response @property - def nokia_sleep_response(self) -> nokia.NokiaSleep: + def withings_sleep_response(self) -> withings.WithingsSleep: """Sleep response.""" - return self._nokia_sleep_response + return self._withings_sleep_response @property - def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: + def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: """Sleep summary response.""" - return self._nokia_sleep_summary_response + return self._withings_sleep_summary_response class WithingsFactoryData: @@ -130,21 +130,21 @@ class WithingsFactoryData: self, hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ): """Constructor.""" self._hass = hass self._flow_id = flow_id - self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock - self._nokia_api_request_mock = nokia_api_request_mock - self._nokia_api_get_measures_mock = nokia_api_get_measures_mock - self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock - self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock + self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock + self._withings_api_request_mock = withings_api_request_mock + self._withings_api_get_measures_mock = withings_api_get_measures_mock + self._withings_api_get_sleep_mock = withings_api_get_sleep_mock + self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock self._data_manager_get_throttle_interval_mock = ( data_manager_get_throttle_interval_mock ) @@ -160,29 +160,29 @@ class WithingsFactoryData: return self._flow_id @property - def nokia_auth_get_credentials_mock(self): + def withings_auth_get_credentials_mock(self): """Get auth credentials mock.""" - return self._nokia_auth_get_credentials_mock + return self._withings_auth_get_credentials_mock @property - def nokia_api_request_mock(self): + def withings_api_request_mock(self): """Get request mock.""" - return self._nokia_api_request_mock + return self._withings_api_request_mock @property - def nokia_api_get_measures_mock(self): + def withings_api_get_measures_mock(self): """Get measures mock.""" - return self._nokia_api_get_measures_mock + return self._withings_api_get_measures_mock @property - def nokia_api_get_sleep_mock(self): + def withings_api_get_sleep_mock(self): """Get sleep mock.""" - return self._nokia_api_get_sleep_mock + return self._withings_api_get_sleep_mock @property - def nokia_api_get_sleep_summary_mock(self): + def withings_api_get_sleep_summary_mock(self): """Get sleep summary mock.""" - return self._nokia_api_get_sleep_summary_mock + return self._withings_api_get_sleep_summary_mock @property def data_manager_get_throttle_interval_mock(self): @@ -243,9 +243,9 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: assert await async_setup_component(hass, http.DOMAIN, config.hass_config) assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - nokia_auth_get_credentials_patch = asynctest.patch( - "nokia.NokiaAuth.get_credentials", - return_value=nokia.NokiaCredentials( + withings_auth_get_credentials_patch = asynctest.patch( + "withings_api.WithingsAuth.get_credentials", + return_value=withings.WithingsCredentials( access_token="my_access_token", token_expiry=time.time() + 600, token_type="my_token_type", @@ -255,28 +255,33 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: consumer_secret=config.withings_config.get(const.CLIENT_SECRET), ), ) - nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() + withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() - nokia_api_request_patch = asynctest.patch( - "nokia.NokiaApi.request", return_value=config.nokia_request_response + withings_api_request_patch = asynctest.patch( + "withings_api.WithingsApi.request", + return_value=config.withings_request_response, ) - nokia_api_request_mock = nokia_api_request_patch.start() + withings_api_request_mock = withings_api_request_patch.start() - nokia_api_get_measures_patch = asynctest.patch( - "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response + withings_api_get_measures_patch = asynctest.patch( + "withings_api.WithingsApi.get_measures", + return_value=config.withings_measures_response, ) - nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() + withings_api_get_measures_mock = withings_api_get_measures_patch.start() - nokia_api_get_sleep_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response + withings_api_get_sleep_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep", + return_value=config.withings_sleep_response, ) - nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() + withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() - nokia_api_get_sleep_summary_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep_summary", - return_value=config.nokia_sleep_summary_response, + withings_api_get_sleep_summary_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep_summary", + return_value=config.withings_sleep_summary_response, + ) + withings_api_get_sleep_summary_mock = ( + withings_api_get_sleep_summary_patch.start() ) - nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() data_manager_get_throttle_interval_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" @@ -295,11 +300,11 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: patches.extend( [ - nokia_auth_get_credentials_patch, - nokia_api_request_patch, - nokia_api_get_measures_patch, - nokia_api_get_sleep_patch, - nokia_api_get_sleep_summary_patch, + withings_auth_get_credentials_patch, + withings_api_request_patch, + withings_api_get_measures_patch, + withings_api_get_sleep_patch, + withings_api_get_sleep_summary_patch, data_manager_get_throttle_interval_patch, get_measures_patch, ] @@ -328,11 +333,11 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: return WithingsFactoryData( hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index a22689f92bb..9f2480f9094 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,6 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError import pytest from requests_oauthlib import TokenUpdated @@ -13,19 +13,19 @@ from homeassistant.components.withings.common import ( from homeassistant.exceptions import PlatformNotReady -@pytest.fixture(name="nokia_api") -def nokia_api_fixture(): - """Provide nokia api.""" - nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) - nokia_api.get_measures = MagicMock() - nokia_api.get_sleep = MagicMock() - return nokia_api +@pytest.fixture(name="withings_api") +def withings_api_fixture(): + """Provide withings api.""" + withings_api = withings.WithingsApi.__new__(withings.WithingsApi) + withings_api.get_measures = MagicMock() + withings_api.get_sleep = MagicMock() + return withings_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): +def data_manager_fixture(hass, withings_api: withings.WithingsApi): """Provide data manager.""" - return WithingsDataManager(hass, "My Profile", nokia_api) + return WithingsDataManager(hass, "My Profile", withings_api) def test_print_service(): diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index da77910097b..697d0a8b864 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,7 +2,12 @@ from unittest.mock import MagicMock, patch import asynctest -from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary +from withings_api import ( + WithingsApi, + WithingsMeasures, + WithingsSleep, + WithingsSleepSummary, +) import pytest from homeassistant.components.withings import DOMAIN @@ -15,7 +20,7 @@ from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from .common import nokia_sleep_response +from .common import withings_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig @@ -120,9 +125,9 @@ async def test_health_sensor_state_none( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=None, - nokia_sleep_response=None, - nokia_sleep_summary_response=None, + withings_measures_response=None, + withings_sleep_response=None, + withings_sleep_summary_response=None, ) ) @@ -153,9 +158,9 @@ async def test_health_sensor_state_empty( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=NokiaMeasures({"measuregrps": []}), - nokia_sleep_response=NokiaSleep({"series": []}), - nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), + withings_measures_response=WithingsMeasures({"measuregrps": []}), + withings_sleep_response=WithingsSleep({"series": []}), + withings_sleep_summary_response=WithingsSleepSummary({"series": []}), ) ) @@ -201,7 +206,8 @@ async def test_sleep_state_throttled( data = await withings_factory( WithingsFactoryConfig( - measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) + measures=[measure], + withings_sleep_response=withings_sleep_response(sleep_states), ) ) @@ -257,16 +263,16 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): "expires_in": "2", } - original_nokia_api = NokiaApi - nokia_api_instance = None + original_withings_api = WithingsApi + withings_api_instance = None - def new_nokia_api(*args, **kwargs): - nonlocal nokia_api_instance - nokia_api_instance = original_nokia_api(*args, **kwargs) - nokia_api_instance.request = MagicMock() - return nokia_api_instance + def new_withings_api(*args, **kwargs): + nonlocal withings_api_instance + withings_api_instance = original_withings_api(*args, **kwargs) + withings_api_instance.request = MagicMock() + return withings_api_instance - nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) + withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( @@ -275,7 +281,7 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): wraps=hass.config_entries.async_update_entry, ) - with session_patch, client_patch, nokia_api_patch, update_entry_patch: + with session_patch, client_patch, withings_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( @@ -298,7 +304,7 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): await async_setup_entry(hass, config_entry, async_add_entities) - nokia_api_instance.set_token(expected_creds) + withings_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2" From 7f20210e934ab6208dc6b1be46728c35117bb7c3 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 8 Oct 2019 21:52:25 +0200 Subject: [PATCH 106/639] Include unit_of_measurement in sensor device trigger capabilities (#27265) * Expose unit_of_measurement in sensor device trigger * Update test --- .../components/sensor/device_trigger.py | 16 ++++++++-- .../components/sensor/test_device_trigger.py | 31 ++++++++++++++++--- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index bd53dca0c9d..7eabc457161 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_FOR, CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -149,11 +150,22 @@ async def async_get_triggers(hass, device_id): async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else "" + ) + return { "extra_fields": vol.Schema( { - vol.Optional(CONF_ABOVE): vol.Coerce(float), - vol.Optional(CONF_BELOW): vol.Coerce(float), + vol.Optional( + CONF_ABOVE, + description={CONF_UNIT_OF_MEASUREMENT: unit_of_measurement}, + ): vol.Coerce(float), + vol.Optional( + CONF_BELOW, + description={CONF_UNIT_OF_MEASUREMENT: unit_of_measurement}, + ): vol.Coerce(float), vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 45452dc84a0..1bc7e5e1ee5 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -74,26 +74,49 @@ async def test_get_triggers(hass, device_reg, entity_reg): if device_class != "none" ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 8 assert triggers == expected_triggers async def test_get_trigger_capabilities(hass, device_reg, entity_reg): - """Test we get the expected capabilities from a binary_sensor trigger.""" + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["battery"].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + expected_capabilities = { "extra_fields": [ - {"name": "above", "optional": True, "type": "float"}, - {"name": "below", "optional": True, "type": "float"}, + { + "description": {"unit_of_measurement": "%"}, + "name": "above", + "optional": True, + "type": "float", + }, + { + "description": {"unit_of_measurement": "%"}, + "name": "below", + "optional": True, + "type": "float", + }, {"name": "for", "optional": True, "type": "positive_time_period_dict"}, ] } triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 1 for trigger in triggers: capabilities = await async_get_device_automation_capabilities( hass, "trigger", trigger From d345b58ce6b9f0486bc901b850dfd72b5238c6fe Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 8 Oct 2019 23:44:33 +0200 Subject: [PATCH 107/639] Improve UniFi config flow tests and add options flow test (#27340) --- homeassistant/components/unifi/config_flow.py | 6 - tests/components/deconz/test_config_flow.py | 2 +- tests/components/unifi/test_config_flow.py | 265 ++++++++++++++++++ tests/components/unifi/test_init.py | 172 +----------- 4 files changed, 268 insertions(+), 177 deletions(-) create mode 100644 tests/components/unifi/test_config_flow.py diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 92281837f48..01b97a78366 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -150,12 +150,6 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.desc = next(iter(self.sites.values()))["desc"] return await self.async_step_site(user_input={}) - if self.desc is not None: - for site in self.sites.values(): - if self.desc == site["name"]: - self.desc = site["desc"] - return await self.async_step_site(user_input={}) - sites = [] for site in self.sites.values(): sites.append(site["desc"]) diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d0423c394a6..4045201bd18 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -387,7 +387,7 @@ async def test_hassio_confirm(hass): async def test_option_flow(hass): - """Test config flow selection of one of two bridges.""" + """Test config flow options.""" entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None) hass.config_entries._entries.append(entry) diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py new file mode 100644 index 00000000000..aea4d565f3d --- /dev/null +++ b/tests/components/unifi/test_config_flow.py @@ -0,0 +1,265 @@ +"""Test UniFi config flow.""" +from asynctest import patch + +from homeassistant.components import unifi +from homeassistant.components.unifi import config_flow +from homeassistant.components.unifi.const import CONF_CONTROLLER, CONF_SITE_ID + +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, + CONF_VERIFY_SSL, +) + +from tests.common import MockConfigEntry + +import aiounifi + + +async def test_flow_works(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + aioclient_mock.post( + "https://1.2.3.4:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": "application/json"}, + ) + + aioclient_mock.get( + "https://1.2.3.4:1234/api/self/sites", + json={ + "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "meta": {"rc": "ok"}, + }, + headers={"content-type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "Site name" + assert result["data"] == { + CONF_CONTROLLER: { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_SITE_ID: "site_id", + CONF_VERIFY_SSL: True, + } + } + + +async def test_flow_works_multiple_sites(hass, aioclient_mock): + """Test config flow works when finding multiple sites.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + aioclient_mock.post( + "https://1.2.3.4:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": "application/json"}, + ) + + aioclient_mock.get( + "https://1.2.3.4:1234/api/self/sites", + json={ + "data": [ + {"name": "default", "role": "admin", "desc": "site name"}, + {"name": "site2", "role": "admin", "desc": "site2 name"}, + ], + "meta": {"rc": "ok"}, + }, + headers={"content-type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "site" + assert result["data_schema"]({"site": "site name"}) + assert result["data_schema"]({"site": "site2 name"}) + + +async def test_flow_fails_site_already_configured(hass, aioclient_mock): + """Test config flow.""" + entry = MockConfigEntry( + domain=unifi.DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "site_id"}} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + aioclient_mock.post( + "https://1.2.3.4:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": "application/json"}, + ) + + aioclient_mock.get( + "https://1.2.3.4:1234/api/self/sites", + json={ + "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "meta": {"rc": "ok"}, + }, + headers={"content-type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "abort" + + +async def test_flow_fails_user_credentials_faulty(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.Unauthorized): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "faulty_credentials"} + + +async def test_flow_fails_controller_unavailable(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("aiounifi.Controller.login", side_effect=aiounifi.errors.RequestError): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "service_unavailable"} + + +async def test_flow_fails_unknown_problem(hass, aioclient_mock): + """Test config flow.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + with patch("aiounifi.Controller.login", side_effect=Exception): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == "abort" + + +async def test_option_flow(hass): + """Test config flow options.""" + entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None) + hass.config_entries._entries.append(entry) + + flow = await hass.config_entries.options._async_create_flow( + entry.entry_id, context={"source": "test"}, data=None + ) + + result = await flow.async_step_init() + assert result["type"] == "form" + assert result["step_id"] == "device_tracker" + + result = await flow.async_step_device_tracker( + user_input={ + config_flow.CONF_TRACK_CLIENTS: False, + config_flow.CONF_TRACK_WIRED_CLIENTS: False, + config_flow.CONF_TRACK_DEVICES: False, + config_flow.CONF_DETECTION_TIME: 100, + } + ) + assert result["type"] == "form" + assert result["step_id"] == "statistics_sensors" + + result = await flow.async_step_statistics_sensors( + user_input={config_flow.CONF_ALLOW_BANDWIDTH_SENSORS: True} + ) + assert result["type"] == "create_entry" + assert result["data"] == { + config_flow.CONF_TRACK_CLIENTS: False, + config_flow.CONF_TRACK_WIRED_CLIENTS: False, + config_flow.CONF_TRACK_DEVICES: False, + config_flow.CONF_DETECTION_TIME: 100, + config_flow.CONF_ALLOW_BANDWIDTH_SENSORS: True, + } diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 845954d8134..6b17b803390 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -2,16 +2,9 @@ from unittest.mock import Mock, patch from homeassistant.components import unifi -from homeassistant.components.unifi import config_flow + from homeassistant.setup import async_setup_component -from homeassistant.components.unifi.const import CONF_CONTROLLER, CONF_SITE_ID -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_PORT, - CONF_USERNAME, - CONF_VERIFY_SSL, -) + from tests.common import mock_coro, MockConfigEntry @@ -179,164 +172,3 @@ async def test_unload_entry(hass): assert await unifi.async_unload_entry(hass, entry) assert len(mock_controller.return_value.async_reset.mock_calls) == 1 assert hass.data[unifi.DOMAIN] == {} - - -async def test_flow_works(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch("aiounifi.Controller") as mock_controller: - - def mock_constructor( - host, username, password, port, site, websession, sslcontext - ): - """Fake the controller constructor.""" - mock_controller.host = host - mock_controller.username = username - mock_controller.password = password - mock_controller.port = port - mock_controller.site = site - return mock_controller - - mock_controller.side_effect = mock_constructor - mock_controller.login.return_value = mock_coro() - mock_controller.sites.return_value = mock_coro( - {"site1": {"name": "default", "role": "admin", "desc": "site name"}} - ) - - await flow.async_step_user( - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - CONF_VERIFY_SSL: True, - } - ) - - result = await flow.async_step_site(user_input={}) - - assert mock_controller.host == "1.2.3.4" - assert len(mock_controller.login.mock_calls) == 1 - assert len(mock_controller.sites.mock_calls) == 1 - - assert result["type"] == "create_entry" - assert result["title"] == "site name" - assert result["data"] == { - CONF_CONTROLLER: { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_PORT: 1234, - CONF_SITE_ID: "default", - CONF_VERIFY_SSL: True, - } - } - - -async def test_controller_multiple_sites(hass): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - flow.config = { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - } - flow.sites = { - "site1": {"name": "default", "role": "admin", "desc": "site name"}, - "site2": {"name": "site2", "role": "admin", "desc": "site2 name"}, - } - - result = await flow.async_step_site() - - assert result["type"] == "form" - assert result["step_id"] == "site" - - assert result["data_schema"]({"site": "site name"}) - assert result["data_schema"]({"site": "site2 name"}) - - -async def test_controller_site_already_configured(hass): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - entry = MockConfigEntry( - domain=unifi.DOMAIN, data={"controller": {"host": "1.2.3.4", "site": "default"}} - ) - entry.add_to_hass(hass) - - flow.config = { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - } - flow.desc = "site name" - flow.sites = {"site1": {"name": "default", "role": "admin", "desc": "site name"}} - - result = await flow.async_step_site() - - assert result["type"] == "abort" - - -async def test_user_credentials_faulty(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch.object( - config_flow, "get_controller", side_effect=unifi.errors.AuthenticationRequired - ): - result = await flow.async_step_user( - { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_SITE_ID: "default", - } - ) - - assert result["type"] == "form" - assert result["errors"] == {"base": "faulty_credentials"} - - -async def test_controller_is_unavailable(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch.object( - config_flow, "get_controller", side_effect=unifi.errors.CannotConnect - ): - result = await flow.async_step_user( - { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_SITE_ID: "default", - } - ) - - assert result["type"] == "form" - assert result["errors"] == {"base": "service_unavailable"} - - -async def test_controller_unkown_problem(hass, aioclient_mock): - """Test config flow.""" - flow = config_flow.UnifiFlowHandler() - flow.hass = hass - - with patch.object(config_flow, "get_controller", side_effect=Exception): - result = await flow.async_step_user( - { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - CONF_SITE_ID: "default", - } - ) - - assert result["type"] == "abort" From 3e6b9a17cc98a20efa27ffb8a8d4dded734786b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 9 Oct 2019 00:45:24 +0300 Subject: [PATCH 108/639] Run mypy in pre-commit (#27339) * Move mypy files config to setup.cfg * Add mypy in pre-commit --- .pre-commit-config.yaml | 4 ++++ azure-pipelines-ci.yml | 5 +---- mypyrc | 38 -------------------------------------- setup.cfg | 14 +++++++++----- tox.ini | 3 +-- 5 files changed, 15 insertions(+), 49 deletions(-) delete mode 100644 mypyrc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 78b7ec29859..8e8792e88c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,3 +13,7 @@ repos: additional_dependencies: - flake8-docstrings==1.3.1 - pydocstyle==4.0.0 +- repo: https://github.com/pre-commit/mirrors-mypy.git + rev: v0.730 + hooks: + - id: mypy diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 13f0915bc56..74e9ea107c5 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -166,9 +166,6 @@ stages: pip install -r requirements_test.txt -c homeassistant/package_constraints.txt displayName: 'Setup Env' - script: | - TYPING_FILES=$(cat mypyrc) - echo -e "Run mypy on: \n$TYPING_FILES" - . venv/bin/activate - mypy $TYPING_FILES + mypy homeassistant displayName: 'Run mypy' diff --git a/mypyrc b/mypyrc deleted file mode 100644 index 08413ecd23c..00000000000 --- a/mypyrc +++ /dev/null @@ -1,38 +0,0 @@ -homeassistant/*.py -homeassistant/auth/ -homeassistant/components/*.py -homeassistant/components/automation/ -homeassistant/components/binary_sensor/ -homeassistant/components/calendar/ -homeassistant/components/camera/ -homeassistant/components/cover/ -homeassistant/components/device_automation/ -homeassistant/components/frontend/ -homeassistant/components/geo_location/ -homeassistant/components/group/ -homeassistant/components/history/ -homeassistant/components/http/ -homeassistant/components/image_processing/ -homeassistant/components/integration/ -homeassistant/components/light/ -homeassistant/components/lock/ -homeassistant/components/mailbox/ -homeassistant/components/media_player/ -homeassistant/components/notify/ -homeassistant/components/persistent_notification/ -homeassistant/components/proximity/ -homeassistant/components/remote/ -homeassistant/components/scene/ -homeassistant/components/sensor/ -homeassistant/components/sun/ -homeassistant/components/switch/ -homeassistant/components/systemmonitor/ -homeassistant/components/tts/ -homeassistant/components/vacuum/ -homeassistant/components/water_heater/ -homeassistant/components/weather/ -homeassistant/components/websocket_api/ -homeassistant/components/zone/ -homeassistant/helpers/ -homeassistant/scripts/ -homeassistant/util/ diff --git a/setup.cfg b/setup.cfg index 4c9c892b93f..6d0e5378b44 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,17 +57,21 @@ combine_as_imports = true [mypy] python_version = 3.6 +ignore_errors = true +follow_imports = silent +ignore_missing_imports = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_unused_configs = true + +[mypy-homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.loader,homeassistant.__main__,homeassistant.monkey_patch,homeassistant.requirements,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*] +ignore_errors = false check_untyped_defs = true disallow_incomplete_defs = true disallow_untyped_calls = true disallow_untyped_defs = true -follow_imports = silent -ignore_missing_imports = true no_implicit_optional = true strict_equality = true -warn_incomplete_stub = true -warn_redundant_casts = true warn_return_any = true warn_unreachable = true -warn_unused_configs = true warn_unused_ignores = true diff --git a/tox.ini b/tox.ini index 2d4cf7c54ba..8c3563dac83 100644 --- a/tox.ini +++ b/tox.ini @@ -37,9 +37,8 @@ commands = flake8 {posargs: homeassistant tests script} [testenv:typing] -whitelist_externals=/bin/bash deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - /bin/bash -c 'TYPING_FILES=$(cat mypyrc); mypy $TYPING_FILES' + mypy homeassistant From 768bb0017789f0d3a1737d032f9662b66cffe8b9 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 9 Oct 2019 00:32:17 +0000 Subject: [PATCH 109/639] [ci skip] Translation update --- .../components/adguard/.translations/nn.json | 11 ++++ .../components/airly/.translations/ko.json | 22 ++++++++ .../components/airly/.translations/nn.json | 10 ++++ .../arcam_fmj/.translations/nn.json | 5 ++ .../components/axis/.translations/nn.json | 3 +- .../binary_sensor/.translations/en.json | 2 + .../binary_sensor/.translations/ko.json | 54 ++++++++++++++++--- .../components/daikin/.translations/nn.json | 5 ++ .../components/deconz/.translations/ko.json | 1 + .../dialogflow/.translations/nn.json | 5 ++ .../components/ecobee/.translations/de.json | 11 ++++ .../components/ecobee/.translations/ko.json | 25 +++++++++ .../components/ecobee/.translations/nn.json | 5 ++ .../emulated_roku/.translations/nn.json | 5 ++ .../components/esphome/.translations/nn.json | 7 ++- .../geonetnz_quakes/.translations/nn.json | 12 +++++ .../components/heos/.translations/de.json | 2 +- .../iaqualink/.translations/nn.json | 5 ++ .../components/ipma/.translations/nn.json | 11 ++++ .../components/iqvia/.translations/nn.json | 10 ++++ .../components/izone/.translations/nn.json | 10 ++++ .../components/life360/.translations/nn.json | 12 +++++ .../components/lifx/.translations/nn.json | 10 ++++ .../components/linky/.translations/nn.json | 10 ++++ .../components/mailgun/.translations/nn.json | 5 ++ .../components/met/.translations/nn.json | 11 ++++ .../components/neato/.translations/de.json | 3 +- .../components/neato/.translations/es.json | 3 +- .../components/neato/.translations/ko.json | 27 ++++++++++ .../components/neato/.translations/nn.json | 12 +++++ .../opentherm_gw/.translations/ko.json | 23 ++++++++ .../opentherm_gw/.translations/nn.json | 12 +++++ .../owntracks/.translations/nn.json | 5 ++ .../components/plaato/.translations/nn.json | 5 ++ .../components/plex/.translations/de.json | 17 ++++++ .../components/plex/.translations/es.json | 5 ++ .../components/plex/.translations/ko.json | 31 ++++++++++- .../components/plex/.translations/lb.json | 5 ++ .../components/plex/.translations/nn.json | 5 ++ .../components/plex/.translations/no.json | 5 ++ .../components/plex/.translations/ru.json | 5 ++ .../components/plex/.translations/sl.json | 5 ++ .../plex/.translations/zh-Hant.json | 5 ++ .../components/point/.translations/nn.json | 5 ++ .../components/ps4/.translations/nn.json | 13 ++++- .../components/sensor/.translations/de.json | 21 ++++++++ .../components/sensor/.translations/ko.json | 26 +++++++++ .../components/sensor/.translations/sl.json | 15 +++++- .../simplisafe/.translations/nn.json | 5 ++ .../components/soma/.translations/de.json | 9 ++++ .../components/soma/.translations/nn.json | 5 ++ .../components/somfy/.translations/nn.json | 5 ++ .../tellduslive/.translations/nn.json | 5 ++ .../components/toon/.translations/nn.json | 7 +++ .../components/tplink/.translations/nn.json | 10 ++++ .../components/traccar/.translations/nn.json | 5 ++ .../transmission/.translations/de.json | 37 +++++++++++++ .../transmission/.translations/en.json | 2 +- .../transmission/.translations/ko.json | 40 ++++++++++++++ .../transmission/.translations/nn.json | 12 +++++ .../twentemilieu/.translations/nn.json | 10 ++++ .../components/twilio/.translations/nn.json | 5 ++ .../components/unifi/.translations/de.json | 5 ++ .../components/unifi/.translations/es.json | 5 ++ .../components/unifi/.translations/ko.json | 5 ++ .../components/unifi/.translations/nn.json | 11 ++++ .../components/upnp/.translations/nn.json | 3 +- .../components/vesync/.translations/nn.json | 5 ++ .../components/wemo/.translations/nn.json | 10 ++++ .../components/withings/.translations/nn.json | 5 ++ .../components/zha/.translations/de.json | 8 +++ .../components/zha/.translations/ko.json | 47 ++++++++++++++++ .../components/zha/.translations/nn.json | 10 ++++ .../components/zwave/.translations/nn.json | 3 +- 74 files changed, 779 insertions(+), 17 deletions(-) create mode 100644 homeassistant/components/adguard/.translations/nn.json create mode 100644 homeassistant/components/airly/.translations/ko.json create mode 100644 homeassistant/components/airly/.translations/nn.json create mode 100644 homeassistant/components/arcam_fmj/.translations/nn.json create mode 100644 homeassistant/components/daikin/.translations/nn.json create mode 100644 homeassistant/components/dialogflow/.translations/nn.json create mode 100644 homeassistant/components/ecobee/.translations/de.json create mode 100644 homeassistant/components/ecobee/.translations/ko.json create mode 100644 homeassistant/components/ecobee/.translations/nn.json create mode 100644 homeassistant/components/emulated_roku/.translations/nn.json create mode 100644 homeassistant/components/geonetnz_quakes/.translations/nn.json create mode 100644 homeassistant/components/iaqualink/.translations/nn.json create mode 100644 homeassistant/components/ipma/.translations/nn.json create mode 100644 homeassistant/components/iqvia/.translations/nn.json create mode 100644 homeassistant/components/izone/.translations/nn.json create mode 100644 homeassistant/components/life360/.translations/nn.json create mode 100644 homeassistant/components/lifx/.translations/nn.json create mode 100644 homeassistant/components/linky/.translations/nn.json create mode 100644 homeassistant/components/mailgun/.translations/nn.json create mode 100644 homeassistant/components/met/.translations/nn.json create mode 100644 homeassistant/components/neato/.translations/ko.json create mode 100644 homeassistant/components/neato/.translations/nn.json create mode 100644 homeassistant/components/opentherm_gw/.translations/ko.json create mode 100644 homeassistant/components/opentherm_gw/.translations/nn.json create mode 100644 homeassistant/components/owntracks/.translations/nn.json create mode 100644 homeassistant/components/plaato/.translations/nn.json create mode 100644 homeassistant/components/plex/.translations/nn.json create mode 100644 homeassistant/components/point/.translations/nn.json create mode 100644 homeassistant/components/sensor/.translations/de.json create mode 100644 homeassistant/components/sensor/.translations/ko.json create mode 100644 homeassistant/components/simplisafe/.translations/nn.json create mode 100644 homeassistant/components/soma/.translations/de.json create mode 100644 homeassistant/components/soma/.translations/nn.json create mode 100644 homeassistant/components/somfy/.translations/nn.json create mode 100644 homeassistant/components/tellduslive/.translations/nn.json create mode 100644 homeassistant/components/tplink/.translations/nn.json create mode 100644 homeassistant/components/traccar/.translations/nn.json create mode 100644 homeassistant/components/transmission/.translations/de.json create mode 100644 homeassistant/components/transmission/.translations/ko.json create mode 100644 homeassistant/components/transmission/.translations/nn.json create mode 100644 homeassistant/components/twentemilieu/.translations/nn.json create mode 100644 homeassistant/components/twilio/.translations/nn.json create mode 100644 homeassistant/components/unifi/.translations/nn.json create mode 100644 homeassistant/components/vesync/.translations/nn.json create mode 100644 homeassistant/components/wemo/.translations/nn.json create mode 100644 homeassistant/components/withings/.translations/nn.json create mode 100644 homeassistant/components/zha/.translations/nn.json diff --git a/homeassistant/components/adguard/.translations/nn.json b/homeassistant/components/adguard/.translations/nn.json new file mode 100644 index 00000000000..7c129cba3af --- /dev/null +++ b/homeassistant/components/adguard/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/ko.json b/homeassistant/components/airly/.translations/ko.json new file mode 100644 index 00000000000..eb20c9174b4 --- /dev/null +++ b/homeassistant/components/airly/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API \ud0a4\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", + "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4.", + "wrong_location": "\uc774 \uc9c0\uc5ed\uc5d0\ub294 Airly \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "name": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc758 \uc774\ub984" + }, + "description": "Airly \uacf5\uae30 \ud488\uc9c8 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub97c \uc124\uc815\ud569\ub2c8\ub2e4. API \ud0a4\ub97c \uc0dd\uc131\ud558\ub824\uba74 https://developer.airly.eu/register \ub85c \uc774\ub3d9\ud574\uc8fc\uc138\uc694", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/nn.json b/homeassistant/components/airly/.translations/nn.json new file mode 100644 index 00000000000..7e2f4f1ff6b --- /dev/null +++ b/homeassistant/components/airly/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/.translations/nn.json b/homeassistant/components/arcam_fmj/.translations/nn.json new file mode 100644 index 00000000000..b0ad4660d0f --- /dev/null +++ b/homeassistant/components/arcam_fmj/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Arcam FMJ" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/nn.json b/homeassistant/components/axis/.translations/nn.json index 33644469359..b6296d1acab 100644 --- a/homeassistant/components/axis/.translations/nn.json +++ b/homeassistant/components/axis/.translations/nn.json @@ -5,7 +5,8 @@ "data": { "host": "Vert", "password": "Passord", - "port": "Port" + "port": "Port", + "username": "Brukarnamn" } } } diff --git a/homeassistant/components/binary_sensor/.translations/en.json b/homeassistant/components/binary_sensor/.translations/en.json index 6379df936b8..93b61893980 100644 --- a/homeassistant/components/binary_sensor/.translations/en.json +++ b/homeassistant/components/binary_sensor/.translations/en.json @@ -53,6 +53,7 @@ "hot": "{entity_name} became hot", "light": "{entity_name} started detecting light", "locked": "{entity_name} locked", + "moist": "{entity_name} became moist", "moist\u00a7": "{entity_name} became moist", "motion": "{entity_name} started detecting motion", "moving": "{entity_name} started moving", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} became dry", "not_moving": "{entity_name} stopped moving", "not_occupied": "{entity_name} became not occupied", + "not_opened": "{entity_name} closed", "not_plugged_in": "{entity_name} unplugged", "not_powered": "{entity_name} not powered", "not_present": "{entity_name} not present", diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json index 3c12eabe8ff..167708c2cf1 100644 --- a/homeassistant/components/binary_sensor/.translations/ko.json +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", + "is_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud569\ub2c8\ub2e4", "is_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc2b5\ub2c8\ub2e4", "is_connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "is_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4", @@ -18,20 +18,20 @@ "is_no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "is_no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", "is_no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", - "is_not_bat_low": "{entity_name} \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", + "is_not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac\uac00 \uc815\uc0c1\uc785\ub2c8\ub2e4", "is_not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", "is_not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc84c\uc2b5\ub2c8\ub2e4", "is_not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", "is_not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud569\ub2c8\ub2e4", "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "is_not_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "is_not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", "is_not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud614\uc2b5\ub2c8\ub2e4", "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "is_not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc2b5\ub2c8\ub2e4", "is_not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud569\ub2c8\ub2e4", - "is_occupied": "{entity_name} \uc774 (\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4", + "is_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc785\ub2c8\ub2e4", "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc84c\uc2b5\ub2c8\ub2e4", "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc84c\uc2b5\ub2c8\ub2e4", "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", @@ -45,8 +45,50 @@ "is_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud588\uc2b5\ub2c8\ub2e4" }, "trigger_type": { - "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871", - "closed": "{entity_name} \ub2eb\ud798" + "bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc794\ub7c9 \ubd80\uc871", + "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud798", + "cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6cc\uc9d0", + "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub428", + "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud568", + "hot": "{entity_name} \uc774(\uac00) \ub728\uac70\uc6cc\uc9d0", + "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud568", + "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae40", + "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0", + "moist\u00a7": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9d0", + "motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud568", + "moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784", + "no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0 \ubabb\ud568", + "no_light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0 \ubabb\ud568", + "no_motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0 \ubabb\ud568", + "no_problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0 \ubabb\ud568", + "no_smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0 \ubabb\ud568", + "no_sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0 \ubabb\ud568", + "no_vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0 \ubabb\ud568", + "not_bat_low": "{entity_name} \uc758 \ubc30\ud130\ub9ac \uc815\uc0c1", + "not_cold": "{entity_name} \uc774(\uac00) \ucc28\uac11\uc9c0 \uc54a\uc74c", + "not_connected": "{entity_name} \uc758 \uc5f0\uacb0\uc774 \ub04a\uc5b4\uc9d0", + "not_hot": "{entity_name} \uc774(\uac00) \ub728\uac81\uc9c0 \uc54a\uc74c", + "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub428", + "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9d0", + "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc74c", + "not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911\uc774\uc9c0 \uc54a\uc74c", + "not_opened": "{entity_name} \uc774(\uac00) \ub2eb\ud798", + "not_plugged_in": "{entity_name} \uc774(\uac00) \ubf51\ud798", + "not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc74c", + "not_present": "{entity_name} \uc774(\uac00) \uc5c6\uc74c", + "not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud574\uc9d0", + "occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9\uc911", + "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9bc", + "plugged_in": "{entity_name} \uc774(\uac00) \uaf3d\ud798", + "powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub428", + "present": "{entity_name} \uc774(\uac00) \uc788\uc74c", + "problem": "{entity_name} \uc774(\uac00) \ubb38\uc81c\ub97c \uac10\uc9c0\ud568", + "smoke": "{entity_name} \uc774(\uac00) \uc5f0\uae30\ub97c \uac10\uc9c0\ud568", + "sound": "{entity_name} \uc774(\uac00) \uc18c\ub9ac\ub97c \uac10\uc9c0\ud568", + "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9d0", + "turned_on": "{entity_name} \uc774(\uac00) \ucf1c\uc9d0", + "unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud558\uc9c0 \uc54a\uc74c", + "vibration": "{entity_name} \uc774(\uac00) \uc9c4\ub3d9\uc744 \uac10\uc9c0\ud568" } } } \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/nn.json b/homeassistant/components/daikin/.translations/nn.json new file mode 100644 index 00000000000..67d4f852625 --- /dev/null +++ b/homeassistant/components/daikin/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Daikin AC" + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 923a2beb2ff..ef8d3910ecf 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", "remote_button_rotated": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804", + "remote_button_rotation_stopped": "\"{subtype}\" \ubc84\ud2bc\uc744 \ud68c\uc804 \uc815\uc9c0", "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984", diff --git a/homeassistant/components/dialogflow/.translations/nn.json b/homeassistant/components/dialogflow/.translations/nn.json new file mode 100644 index 00000000000..5a96b853eb0 --- /dev/null +++ b/homeassistant/components/dialogflow/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Dialogflow" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/de.json b/homeassistant/components/ecobee/.translations/de.json new file mode 100644 index 00000000000..1959f769d3a --- /dev/null +++ b/homeassistant/components/ecobee/.translations/de.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/ko.json b/homeassistant/components/ecobee/.translations/ko.json new file mode 100644 index 00000000000..2fea66a9d38 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "one_instance_only": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ud604\uc7ac \ud558\ub098\uc758 ecobee \uc778\uc2a4\ud134\uc2a4\ub9cc \uc9c0\uc6d0\ud569\ub2c8\ub2e4." + }, + "error": { + "pin_request_failed": "ecobee \ub85c\ubd80\ud130 PIN \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; API \ud0a4\uac00 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", + "token_request_failed": "ecobee \ub85c\ubd80\ud130 \ud1a0\ud070 \uc694\uccad\uc5d0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4; \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "step": { + "authorize": { + "description": "https://www.ecobee.com/consumerportal/index.html \uc5d0\uc11c PIN \ucf54\ub4dc\ub97c \uc0ac\uc6a9\ud558\uc5ec \uc774 \uc571\uc744 \uc2b9\uc778\ud574\uc8fc\uc138\uc694:\n\n {pin} \n \n \uadf8\ub7f0 \ub2e4\uc74c Submit \uc744 \ub20c\ub7ec\uc8fc\uc138\uc694.", + "title": "ecobee.com \uc5d0\uc11c \uc571 \uc2b9\uc778\ud558\uae30" + }, + "user": { + "data": { + "api_key": "API \ud0a4" + }, + "description": "ecobee.com \uc5d0\uc11c \uc5bb\uc740 API \ud0a4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "ecobee API \ud0a4" + } + }, + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/nn.json b/homeassistant/components/ecobee/.translations/nn.json new file mode 100644 index 00000000000..301239cf31a --- /dev/null +++ b/homeassistant/components/ecobee/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "ecobee" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/nn.json b/homeassistant/components/emulated_roku/.translations/nn.json new file mode 100644 index 00000000000..fc349a0d9de --- /dev/null +++ b/homeassistant/components/emulated_roku/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "EmulatedRoku" + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/nn.json b/homeassistant/components/esphome/.translations/nn.json index 830391f58f6..5e40c8ec5e5 100644 --- a/homeassistant/components/esphome/.translations/nn.json +++ b/homeassistant/components/esphome/.translations/nn.json @@ -1,9 +1,14 @@ { "config": { + "flow_title": "ESPHome: {name}", "step": { "discovery_confirm": { "title": "Fann ESPhome node" + }, + "user": { + "title": "ESPHome" } - } + }, + "title": "ESPHome" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/nn.json b/homeassistant/components/geonetnz_quakes/.translations/nn.json new file mode 100644 index 00000000000..d8afb1e7aae --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radius" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/de.json b/homeassistant/components/heos/.translations/de.json index e8f4df930db..e98df7466ff 100644 --- a/homeassistant/components/heos/.translations/de.json +++ b/homeassistant/components/heos/.translations/de.json @@ -16,6 +16,6 @@ "title": "Mit Heos verbinden" } }, - "title": "Heos" + "title": "HEOS" } } \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/nn.json b/homeassistant/components/iaqualink/.translations/nn.json new file mode 100644 index 00000000000..ea78f0d0d5d --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/.translations/nn.json b/homeassistant/components/ipma/.translations/nn.json new file mode 100644 index 00000000000..0e024a0e1eb --- /dev/null +++ b/homeassistant/components/ipma/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/nn.json b/homeassistant/components/iqvia/.translations/nn.json new file mode 100644 index 00000000000..89922b66f03 --- /dev/null +++ b/homeassistant/components/iqvia/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "IQVIA" + } + }, + "title": "IQVIA" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/nn.json b/homeassistant/components/izone/.translations/nn.json new file mode 100644 index 00000000000..eaf7601be9c --- /dev/null +++ b/homeassistant/components/izone/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/.translations/nn.json b/homeassistant/components/life360/.translations/nn.json new file mode 100644 index 00000000000..98345b022f2 --- /dev/null +++ b/homeassistant/components/life360/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + }, + "title": "Life360" + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/.translations/nn.json b/homeassistant/components/lifx/.translations/nn.json new file mode 100644 index 00000000000..c78905b09c8 --- /dev/null +++ b/homeassistant/components/lifx/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "LIFX" + } + }, + "title": "LIFX" + } +} \ No newline at end of file diff --git a/homeassistant/components/linky/.translations/nn.json b/homeassistant/components/linky/.translations/nn.json new file mode 100644 index 00000000000..6e084d1a9d2 --- /dev/null +++ b/homeassistant/components/linky/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Linky" + } + }, + "title": "Linky" + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/nn.json b/homeassistant/components/mailgun/.translations/nn.json new file mode 100644 index 00000000000..2bab2e43001 --- /dev/null +++ b/homeassistant/components/mailgun/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Mailgun" + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/nn.json b/homeassistant/components/met/.translations/nn.json new file mode 100644 index 00000000000..0e024a0e1eb --- /dev/null +++ b/homeassistant/components/met/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/de.json b/homeassistant/components/neato/.translations/de.json index a3f54f9f69a..2a75d11a9ec 100644 --- a/homeassistant/components/neato/.translations/de.json +++ b/homeassistant/components/neato/.translations/de.json @@ -8,7 +8,8 @@ "default": "Siehe [Neato-Dokumentation]({docs_url})." }, "error": { - "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen", + "unexpected_error": "Unerwarteter Fehler" }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/es.json b/homeassistant/components/neato/.translations/es.json index d033b8af6a4..99e7574e4b2 100644 --- a/homeassistant/components/neato/.translations/es.json +++ b/homeassistant/components/neato/.translations/es.json @@ -8,7 +8,8 @@ "default": "Ver [documentaci\u00f3n Neato]({docs_url})." }, "error": { - "invalid_credentials": "Credenciales no v\u00e1lidas" + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unexpected_error": "Error inesperado" }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/ko.json b/homeassistant/components/neato/.translations/ko.json new file mode 100644 index 00000000000..aeb591f7b20 --- /dev/null +++ b/homeassistant/components/neato/.translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "create_entry": { + "default": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + }, + "error": { + "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unexpected_error": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "vendor": "\uacf5\uae09 \uc5c5\uccb4" + }, + "description": "[Neato \uc124\uba85\uc11c] ({docs_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.", + "title": "Neato \uacc4\uc815 \uc815\ubcf4" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/nn.json b/homeassistant/components/neato/.translations/nn.json new file mode 100644 index 00000000000..e04e73aef24 --- /dev/null +++ b/homeassistant/components/neato/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/ko.json b/homeassistant/components/opentherm_gw/.translations/ko.json new file mode 100644 index 00000000000..85790702435 --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/ko.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "already_configured": "OpenTherm Gateway \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "id_exists": "OpenTherm Gateway id \uac00 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4", + "serial_error": "\uae30\uae30 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", + "timeout": "\uc5f0\uacb0 \uc2dc\ub3c4 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "init": { + "data": { + "device": "\uacbd\ub85c \ub610\ub294 URL", + "floor_temperature": "\uc9c0\uba74 \uae30\ud6c4 \uc628\ub3c4", + "id": "ID", + "name": "\uc774\ub984", + "precision": "\uae30\ud6c4 \uc628\ub3c4 \uc815\ubc00\ub3c4" + }, + "title": "OpenTherm Gateway" + } + }, + "title": "OpenTherm Gateway" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nn.json b/homeassistant/components/opentherm_gw/.translations/nn.json new file mode 100644 index 00000000000..3d018a2292d --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "id": "ID", + "name": "Namn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/.translations/nn.json b/homeassistant/components/owntracks/.translations/nn.json new file mode 100644 index 00000000000..cdfd651beec --- /dev/null +++ b/homeassistant/components/owntracks/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "OwnTracks" + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/.translations/nn.json b/homeassistant/components/plaato/.translations/nn.json new file mode 100644 index 00000000000..750e14b1dae --- /dev/null +++ b/homeassistant/components/plaato/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Plaato Airlock" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json index 210fe732360..95083102273 100644 --- a/homeassistant/components/plex/.translations/de.json +++ b/homeassistant/components/plex/.translations/de.json @@ -1,9 +1,26 @@ { "config": { + "abort": { + "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden" + }, "step": { + "manual_setup": { + "title": "Plex Server" + }, + "start_website_auth": { + "description": "Weiter zur Autorisierung unter plex.tv.", + "title": "Plex Server verbinden" + }, "user": { "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." } } + }, + "options": { + "step": { + "plex_mp_settings": { + "description": "Optionen f\u00fcr Plex-Media-Player" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json index 45417d09d02..261ca951490 100644 --- a/homeassistant/components/plex/.translations/es.json +++ b/homeassistant/components/plex/.translations/es.json @@ -4,6 +4,7 @@ "all_configured": "Todos los servidores vinculados ya configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "Plex se est\u00e1 configurando", + "discovery_no_file": "No se ha encontrado ning\u00fan archivo de configuraci\u00f3n antiguo", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", "unknown": "Fall\u00f3 por razones desconocidas" @@ -32,6 +33,10 @@ "description": "Varios servidores disponibles, seleccione uno:", "title": "Seleccione el servidor Plex" }, + "start_website_auth": { + "description": "Contin\u00fae en plex.tv para autorizar", + "title": "Conectar servidor Plex" + }, "user": { "data": { "manual_setup": "Configuraci\u00f3n manual", diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json index 171c656566d..f8e78945802 100644 --- a/homeassistant/components/plex/.translations/ko.json +++ b/homeassistant/components/plex/.translations/ko.json @@ -4,15 +4,28 @@ "all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84", "already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", + "discovery_no_file": "\ub808\uac70\uc2dc \uad6c\uc131 \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "token_request_timeout": "\ud1a0\ud070 \ud68d\ub4dd \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc774\uc720\ub85c \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4" }, "error": { "faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4", "no_servers": "\uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc11c\ubc84\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "no_token": "\ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc218\ub3d9 \uc124\uc815\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", "not_found": "Plex \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "step": { + "manual_setup": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8", + "ssl": "SSL \uc0ac\uc6a9", + "token": "\ud1a0\ud070 (\ud544\uc694\ud55c \uacbd\uc6b0)", + "verify_ssl": "SSL \uc778\uc99d\uc11c \uac80\uc99d" + }, + "title": "Plex \uc11c\ubc84" + }, "select_server": { "data": { "server": "\uc11c\ubc84" @@ -20,14 +33,30 @@ "description": "\uc5ec\ub7ec \uc11c\ubc84\uac00 \uc0ac\uc6a9 \uac00\ub2a5\ud569\ub2c8\ub2e4. \ud558\ub098\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694:", "title": "Plex \uc11c\ubc84 \uc120\ud0dd" }, + "start_website_auth": { + "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud574\uc8fc\uc138\uc694.", + "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" + }, "user": { "data": { + "manual_setup": "\uc218\ub3d9 \uc124\uc815", "token": "Plex \ud1a0\ud070" }, - "description": "\uc790\ub3d9 \uc124\uc815\uc744 \uc704\ud574 Plex \ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uad6c\uc131\ud574\uc8fc\uc138\uc694.", + "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" } }, "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "\ubaa8\ub4e0 \ucee8\ud2b8\ub864 \ud45c\uc2dc\ud558\uae30", + "use_episode_art": "\uc5d0\ud53c\uc18c\ub4dc \uc544\ud2b8 \uc0ac\uc6a9" + }, + "description": "Plex \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4 \uc635\uc158" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 1795ef6b6d3..7b0f7232976 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -4,6 +4,7 @@ "all_configured": "All verbonne Server sinn scho konfigur\u00e9iert", "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert", "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", + "discovery_no_file": "Kee Konfiguratioun Fichier am ale Format fonnt.", "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", "unknown": "Onbekannte Feeler opgetrueden" @@ -32,6 +33,10 @@ "description": "M\u00e9i Server disponibel, wielt een aus:", "title": "Plex Server auswielen" }, + "start_website_auth": { + "description": "Weiderfueren op plex.tv fir d'Autorisatioun.", + "title": "Plex Server verbannen" + }, "user": { "data": { "manual_setup": "Manuell Konfiguratioun", diff --git a/homeassistant/components/plex/.translations/nn.json b/homeassistant/components/plex/.translations/nn.json new file mode 100644 index 00000000000..a16deb2fca2 --- /dev/null +++ b/homeassistant/components/plex/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Plex" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index a0a9d087d1e..18c4e865a84 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -4,6 +4,7 @@ "all_configured": "Alle knyttet servere som allerede er konfigurert", "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Plex blir konfigurert", + "discovery_no_file": "Ingen eldre konfigurasjonsfil ble funnet", "invalid_import": "Den importerte konfigurasjonen er ugyldig", "token_request_timeout": "Tidsavbrudd ved innhenting av token", "unknown": "Mislyktes av ukjent \u00e5rsak" @@ -32,6 +33,10 @@ "description": "Flere servere tilgjengelig, velg en:", "title": "Velg Plex-server" }, + "start_website_auth": { + "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv.", + "title": "Koble til Plex server" + }, "user": { "data": { "manual_setup": "Manuelt oppsett", diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 53b4bfd9bb5..fe773f72be9 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -4,6 +4,7 @@ "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", + "discovery_no_file": "\u0421\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430", "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" @@ -32,6 +33,10 @@ "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u0438\u043d \u0438\u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432:", "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" }, + "start_website_auth": { + "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", + "title": "Plex" + }, "user": { "data": { "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index d6bc85725eb..9be270a017c 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -4,6 +4,7 @@ "all_configured": "Vsi povezani stre\u017eniki so \u017ee konfigurirani", "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", "already_in_progress": "Plex se konfigurira", + "discovery_no_file": "Podatkovne konfiguracijske datoteke ni bilo mogo\u010de najti", "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", "unknown": "Ni uspelo iz neznanega razloga" @@ -32,6 +33,10 @@ "description": "Na voljo je ve\u010d stre\u017enikov, izberite enega:", "title": "Izberite stre\u017enik Plex" }, + "start_website_auth": { + "description": "Nadaljujte z avtorizacijo na plex.tv.", + "title": "Pove\u017eite stre\u017enik Plex" + }, "user": { "data": { "manual_setup": "Ro\u010dna nastavitev", diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index a0a033651a5..2d4ce1ea6aa 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -4,6 +4,7 @@ "all_configured": "\u6240\u6709\u7d81\u5b9a\u4f3a\u670d\u5668\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210", "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", + "discovery_no_file": "\u627e\u4e0d\u5230\u820a\u7248\u8a2d\u5b9a\u6a94\u6848", "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", "unknown": "\u672a\u77e5\u539f\u56e0\u5931\u6557" @@ -32,6 +33,10 @@ "description": "\u627e\u5230\u591a\u500b\u4f3a\u670d\u5668\uff0c\u8acb\u9078\u64c7\u4e00\u7d44\uff1a", "title": "\u9078\u64c7 Plex \u4f3a\u670d\u5668" }, + "start_website_auth": { + "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u3002", + "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" + }, "user": { "data": { "manual_setup": "\u624b\u52d5\u8a2d\u5b9a", diff --git a/homeassistant/components/point/.translations/nn.json b/homeassistant/components/point/.translations/nn.json new file mode 100644 index 00000000000..865155c0494 --- /dev/null +++ b/homeassistant/components/point/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Minut Point" + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/nn.json b/homeassistant/components/ps4/.translations/nn.json index b3302389c88..86920906003 100644 --- a/homeassistant/components/ps4/.translations/nn.json +++ b/homeassistant/components/ps4/.translations/nn.json @@ -5,9 +5,20 @@ "port_997_bind_error": "Kunne ikkje binde til port 997. Sj\u00e5 [dokumentasjonen] (https://www.home-assistant.io/components/ps4/) for ytterlegare informasjon." }, "step": { + "creds": { + "title": "Playstation 4" + }, + "link": { + "data": { + "code": "PIN", + "name": "Namn" + }, + "title": "Playstation 4" + }, "mode": { "title": "Playstation 4" } - } + }, + "title": "Playstation 4" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json new file mode 100644 index 00000000000..1f248099df3 --- /dev/null +++ b/homeassistant/components/sensor/.translations/de.json @@ -0,0 +1,21 @@ +{ + "device_automation": { + "condition_type": { + "is_humidity": "{entity_name} Feuchtigkeit", + "is_pressure": "{entity_name} Druck", + "is_signal_strength": "{entity_name} Signalst\u00e4rke", + "is_temperature": "{entity_name} Temperatur", + "is_timestamp": "{entity_name} Zeitstempel", + "is_value": "{entity_name} Wert" + }, + "trigger_type": { + "battery_level": "{entity_name} Batteriestatus", + "humidity": "{entity_name} Feuchtigkeit", + "pressure": "{entity_name} Druck", + "signal_strength": "{entity_name} Signalst\u00e4rke", + "temperature": "{entity_name} Temperatur", + "timestamp": "{entity_name} Zeitstempel", + "value": "{entity_name} Wert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/ko.json b/homeassistant/components/sensor/.translations/ko.json new file mode 100644 index 00000000000..d24a4058343 --- /dev/null +++ b/homeassistant/components/sensor/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", + "is_humidity": "{entity_name} \uc2b5\ub3c4", + "is_illuminance": "{entity_name} \uc870\ub3c4", + "is_power": "{entity_name} \uc18c\ube44 \uc804\ub825", + "is_pressure": "{entity_name} \uc555\ub825", + "is_signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4", + "is_temperature": "{entity_name} \uc628\ub3c4", + "is_timestamp": "{entity_name} \uc2dc\uac01", + "is_value": "{entity_name} \uac12" + }, + "trigger_type": { + "battery_level": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9", + "humidity": "{entity_name} \uc2b5\ub3c4", + "illuminance": "{entity_name} \uc870\ub3c4", + "power": "{entity_name} \uc18c\ube44 \uc804\ub825", + "pressure": "{entity_name} \uc555\ub825", + "signal_strength": "{entity_name} \uc2e0\ud638 \uac15\ub3c4", + "temperature": "{entity_name} \uc628\ub3c4", + "timestamp": "{entity_name} \uc2dc\uac01", + "value": "{entity_name} \uac12" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/sl.json b/homeassistant/components/sensor/.translations/sl.json index 472af1dfe3b..e3bc994b6ea 100644 --- a/homeassistant/components/sensor/.translations/sl.json +++ b/homeassistant/components/sensor/.translations/sl.json @@ -7,7 +7,20 @@ "is_power": "{entity_name} mo\u010d", "is_pressure": "{entity_name} pritisk", "is_signal_strength": "{entity_name} jakost signala", - "is_temperature": "{entity_name} temperatura" + "is_temperature": "{entity_name} temperatura", + "is_timestamp": "{entity_name} \u010dasovni \u017eig", + "is_value": "{entity_name} vrednost" + }, + "trigger_type": { + "battery_level": "{entity_name} raven baterije", + "humidity": "{entity_name} vla\u017enost", + "illuminance": "{entity_name} osvetljenosti", + "power": "{entity_name} mo\u010d", + "pressure": "{entity_name} tlak", + "signal_strength": "{entity_name} jakost signala", + "temperature": "{entity_name} temperatura", + "timestamp": "{entity_name} \u010dasovni \u017eig", + "value": "{entity_name} vrednost" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/nn.json b/homeassistant/components/simplisafe/.translations/nn.json new file mode 100644 index 00000000000..0568cad3f6d --- /dev/null +++ b/homeassistant/components/simplisafe/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "SimpliSafe" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json new file mode 100644 index 00000000000..d93eec8aed7 --- /dev/null +++ b/homeassistant/components/soma/.translations/de.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/nn.json b/homeassistant/components/soma/.translations/nn.json new file mode 100644 index 00000000000..6eeb4f75a3c --- /dev/null +++ b/homeassistant/components/soma/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Soma" + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/.translations/nn.json b/homeassistant/components/somfy/.translations/nn.json new file mode 100644 index 00000000000..ff0383c7f01 --- /dev/null +++ b/homeassistant/components/somfy/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Somfy" + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/nn.json b/homeassistant/components/tellduslive/.translations/nn.json new file mode 100644 index 00000000000..934f56a420b --- /dev/null +++ b/homeassistant/components/tellduslive/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Telldus Live" + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/nn.json b/homeassistant/components/toon/.translations/nn.json index b8dbeff27ca..eed288a5e39 100644 --- a/homeassistant/components/toon/.translations/nn.json +++ b/homeassistant/components/toon/.translations/nn.json @@ -1,5 +1,12 @@ { "config": { + "step": { + "authenticate": { + "data": { + "username": "Brukarnamn" + } + } + }, "title": "Toon" } } \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/nn.json b/homeassistant/components/tplink/.translations/nn.json new file mode 100644 index 00000000000..1d9fb41fc8c --- /dev/null +++ b/homeassistant/components/tplink/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "TP-Link Smart Home" + } + }, + "title": "TP-Link Smart Home" + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/.translations/nn.json b/homeassistant/components/traccar/.translations/nn.json new file mode 100644 index 00000000000..9fc23b3e394 --- /dev/null +++ b/homeassistant/components/traccar/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Traccar" + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json new file mode 100644 index 00000000000..ed0342b9430 --- /dev/null +++ b/homeassistant/components/transmission/.translations/de.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + }, + "error": { + "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "wrong_credentials": "Falscher Benutzername oder Kennwort" + }, + "step": { + "options": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + }, + "title": "Konfigurationsoptionen" + }, + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index e1bc8dc3228..67461d1a3e8 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -20,7 +20,7 @@ "name": "Name", "password": "Password", "port": "Port", - "username": "User name" + "username": "Username" }, "title": "Setup Transmission Client" } diff --git a/homeassistant/components/transmission/.translations/ko.json b/homeassistant/components/transmission/.translations/ko.json new file mode 100644 index 00000000000..a9b1b369f90 --- /dev/null +++ b/homeassistant/components/transmission/.translations/ko.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "wrong_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "options": { + "data": { + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" + }, + "title": "\uc635\uc158 \uc124\uc815" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "Transmission \ud074\ub77c\uc774\uc5b8\ud2b8 \uc124\uc815" + } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" + }, + "description": "Transmission \uc635\uc158 \uc124\uc815" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/nn.json b/homeassistant/components/transmission/.translations/nn.json new file mode 100644 index 00000000000..7622ac1b459 --- /dev/null +++ b/homeassistant/components/transmission/.translations/nn.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Namn", + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/.translations/nn.json b/homeassistant/components/twentemilieu/.translations/nn.json new file mode 100644 index 00000000000..02ac8ecf27a --- /dev/null +++ b/homeassistant/components/twentemilieu/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Twente Milieu" + } + }, + "title": "Twente Milieu" + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/.translations/nn.json b/homeassistant/components/twilio/.translations/nn.json new file mode 100644 index 00000000000..86e5d9051b3 --- /dev/null +++ b/homeassistant/components/twilio/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Twilio" + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index e447e89644f..32a378b7c00 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -38,6 +38,11 @@ "one": "eins", "other": "andere" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Erstellen von Bandbreiten-Nutzungssensoren f\u00fcr Netzwerk-Clients" + } } } } diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 0539f5607b4..1db6712142d 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -38,6 +38,11 @@ "one": "uno", "other": "otro" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Crear sensores para monitorizar uso de ancho de banda de clientes de red" + } } } } diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json index 1fff9887906..295430b7284 100644 --- a/homeassistant/components/unifi/.translations/ko.json +++ b/homeassistant/components/unifi/.translations/ko.json @@ -32,6 +32,11 @@ "track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)", "track_wired_clients": "\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ud3ec\ud568" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8\ub97c \uc704\ud55c \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c \uc0dd\uc131\ud558\uae30" + } } } } diff --git a/homeassistant/components/unifi/.translations/nn.json b/homeassistant/components/unifi/.translations/nn.json new file mode 100644 index 00000000000..7c129cba3af --- /dev/null +++ b/homeassistant/components/unifi/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Brukarnamn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/.translations/nn.json b/homeassistant/components/upnp/.translations/nn.json index 286efcf0353..cfbedd994af 100644 --- a/homeassistant/components/upnp/.translations/nn.json +++ b/homeassistant/components/upnp/.translations/nn.json @@ -11,6 +11,7 @@ "init": { "title": "UPnP / IGD" } - } + }, + "title": "UPnP / IGD" } } \ No newline at end of file diff --git a/homeassistant/components/vesync/.translations/nn.json b/homeassistant/components/vesync/.translations/nn.json new file mode 100644 index 00000000000..372e37133b1 --- /dev/null +++ b/homeassistant/components/vesync/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "VeSync" + } +} \ No newline at end of file diff --git a/homeassistant/components/wemo/.translations/nn.json b/homeassistant/components/wemo/.translations/nn.json new file mode 100644 index 00000000000..c1c8830cb25 --- /dev/null +++ b/homeassistant/components/wemo/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "confirm": { + "title": "Wemo" + } + }, + "title": "Wemo" + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nn.json b/homeassistant/components/withings/.translations/nn.json new file mode 100644 index 00000000000..7d8b268367c --- /dev/null +++ b/homeassistant/components/withings/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Withings" + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 280c941b427..9ffd5211a1f 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -16,5 +16,13 @@ } }, "title": "ZHA" + }, + "device_automation": { + "trigger_subtype": { + "close": "Schlie\u00dfen", + "left": "Links", + "open": "Offen", + "right": "Rechts" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json index 44f45f43570..7ed1a8c69b4 100644 --- a/homeassistant/components/zha/.translations/ko.json +++ b/homeassistant/components/zha/.translations/ko.json @@ -16,5 +16,52 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "squawk": "\ube44\uc0c1", + "warn": "\uacbd\uace0" + }, + "trigger_subtype": { + "both_buttons": "\ub450 \uac1c", + "button_1": "\uccab \ubc88\uc9f8", + "button_2": "\ub450 \ubc88\uc9f8", + "button_3": "\uc138 \ubc88\uc9f8", + "button_4": "\ub124 \ubc88\uc9f8", + "button_5": "\ub2e4\uc12f \ubc88\uc9f8", + "button_6": "\uc5ec\uc12f \ubc88\uc9f8", + "close": "\ub2eb\uae30", + "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30", + "dim_up": "\ubc1d\uac8c \ud558\uae30", + "face_1": "\uba74 1\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_2": "\uba74 2\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_3": "\uba74 3\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_4": "\uba74 4\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_5": "\uba74 5\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_6": "\uba74 6\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_any": "\uc784\uc758\uc758 \uba74 \ub610\ub294 \ud2b9\uc815 \uba74 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "left": "\uc67c\ucabd", + "open": "\uc5f4\uae30", + "right": "\uc624\ub978\ucabd", + "turn_off": "\ub044\uae30", + "turn_on": "\ucf1c\uae30" + }, + "trigger_type": { + "device_dropped": "\uc7a5\uce58\ub97c \ub5a8\uc5b4\ub728\ub9bc", + "device_flipped": "\"{subtype}\" \uae30\uae30\ub97c \ub4a4\uc9d1\uc74c", + "device_knocked": "\"{subtype}\" \uae30\uae30\ub97c \ub450\ub4dc\ub9bc", + "device_rotated": "\"{subtype}\" \uae30\uae30\ub97c \ud68c\uc804", + "device_shaken": "\uae30\uae30 \ud754\ub4e6", + "device_slid": "\"{subtype}\" \uae30\uae30\ub97c \uc2ac\ub77c\uc774\ub4dc", + "device_tilted": "\uae30\uae30\ub97c \uae30\uc6b8\uc784", + "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", + "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uacc4\uc18d \ub204\ub984", + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \uae38\uac8c \ub20c\ub800\ub2e4\uac00 \ub5cc", + "remote_button_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub124 \ubc88 \ub204\ub984", + "remote_button_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub2e4\uc12f \ubc88 \ub204\ub984", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub204\ub984", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub5cc", + "remote_button_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \uc138 \ubc88 \ub204\ub984" + } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nn.json b/homeassistant/components/zha/.translations/nn.json new file mode 100644 index 00000000000..ad2c240baf1 --- /dev/null +++ b/homeassistant/components/zha/.translations/nn.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "ZHA" + } + }, + "title": "ZHA" + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/.translations/nn.json b/homeassistant/components/zwave/.translations/nn.json index ebd9d44796c..8d1c737170f 100644 --- a/homeassistant/components/zwave/.translations/nn.json +++ b/homeassistant/components/zwave/.translations/nn.json @@ -4,6 +4,7 @@ "user": { "description": "Sj\u00e5 [www.home-assistant.io/docs/z-wave/installation/](https://www.home-assistant.io/docs/z-wave/installation/) for informasjon om konfigurasjonsvariablene." } - } + }, + "title": "Z-Wave" } } \ No newline at end of file From 3d937bfd8ac4d45d8119deb6c2e8c85d5e1c3164 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 9 Oct 2019 19:57:51 +1100 Subject: [PATCH 110/639] move import to top-level (#27348) --- homeassistant/components/workday/binary_sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/workday/binary_sensor.py b/homeassistant/components/workday/binary_sensor.py index 19edf231624..3ca2afcc749 100644 --- a/homeassistant/components/workday/binary_sensor.py +++ b/homeassistant/components/workday/binary_sensor.py @@ -2,6 +2,7 @@ import logging from datetime import datetime, timedelta +import holidays import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -141,8 +142,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Workday sensor.""" - import holidays - sensor_name = config.get(CONF_NAME) country = config.get(CONF_COUNTRY) province = config.get(CONF_PROVINCE) From 9ea58b970e759d909c7133a6ef29894a11a38e7a Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Wed, 9 Oct 2019 21:02:09 +1100 Subject: [PATCH 111/639] Move imports in caldav component (#27349) --- homeassistant/components/caldav/calendar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 6251679b225..2bbff2a6bc7 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import logging import re +import caldav import voluptuous as vol from homeassistant.components.calendar import ( @@ -62,8 +63,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) def setup_platform(hass, config, add_entities, disc_info=None): """Set up the WebDav Calendar platform.""" - import caldav - url = config[CONF_URL] username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) From 1257706bd9d4e314f7161da2d1322b2a83096f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Wed, 9 Oct 2019 14:47:43 +0200 Subject: [PATCH 112/639] Update zigpy-zigate to 0.4.1 (#27345) * Update zigpy-zigate to 0.4.1 Fix #27297 * Update zigpy-zigate to 0.4.1 Fix #27297 --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 59d9508ac33..9790fbffd06 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -9,7 +9,7 @@ "zigpy-deconz==0.5.0", "zigpy-homeassistant==0.9.0", "zigpy-xbee-homeassistant==0.5.0", - "zigpy-zigate==0.4.0" + "zigpy-zigate==0.4.1" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index 7d4ebb0074e..fc2d498da7b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2044,7 +2044,7 @@ zigpy-homeassistant==0.9.0 zigpy-xbee-homeassistant==0.5.0 # homeassistant.components.zha -zigpy-zigate==0.4.0 +zigpy-zigate==0.4.1 # homeassistant.components.zoneminder zm-py==0.3.3 From 3194dd34562c98bf564cf8ff4a9d38a2666d70d9 Mon Sep 17 00:00:00 2001 From: Oncleben31 Date: Wed, 9 Oct 2019 19:58:36 +0200 Subject: [PATCH 113/639] Add documentation for logger.set_level service (#27211) * Add set_level doc * use only yaml * reformat * improvements --- homeassistant/components/logger/services.yaml | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/logger/services.yaml b/homeassistant/components/logger/services.yaml index 4d1ba649d36..514aac4c71c 100644 --- a/homeassistant/components/logger/services.yaml +++ b/homeassistant/components/logger/services.yaml @@ -1,6 +1,22 @@ set_default_level: description: Set the default log level for components. fields: - level: {description: 'Default severity level. Possible values are notset, debug, - info, warn, warning, error, fatal, critical', example: debug} -set_level: {description: Set log level for components.} + level: + description: "Default severity level. Possible values are debug, info, warn, warning, error, fatal, critical" + example: debug + +set_level: + description: Set log level for components. + fields: + homeassistant.core: + description: "Example on how to change the logging level for a Home Assistant core components. Possible values are debug, info, warn, warning, error, fatal, critical" + example: debug + homeassistant.components.mqtt: + description: "Example on how to change the logging level for an Integration. Possible values are debug, info, warn, warning, error, fatal, critical" + example: warning + custom_components.my_integration: + description: "Example on how to change the logging level for a Custom Integration. Possible values are debug, info, warn, warning, error, fatal, critical" + example: debug + aiohttp: + description: "Example on how to change the logging level for a Python module. Possible values are debug, info, warn, warning, error, fatal, critical" + example: error From fdf4f398a79e775e11dc9fdd391a8b53f7b773c5 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 9 Oct 2019 21:04:11 +0200 Subject: [PATCH 114/639] Support async validation of device trigger (#27333) --- .../components/automation/__init__.py | 8 +- homeassistant/components/automation/config.py | 10 +- homeassistant/components/automation/device.py | 3 + .../components/deconz/device_trigger.py | 16 ++- .../components/zha/device_trigger.py | 14 ++- tests/components/zha/test_device_trigger.py | 100 +++++++++--------- 6 files changed, 87 insertions(+), 64 deletions(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index f669d415854..3409ce832dd 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -7,9 +7,6 @@ from typing import Any, Awaitable, Callable import voluptuous as vol -from homeassistant.components.device_automation.exceptions import ( - InvalidDeviceAutomationConfig, -) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, @@ -476,10 +473,7 @@ async def _async_process_trigger(hass, config, trigger_configs, name, action): for conf in trigger_configs: platform = importlib.import_module(".{}".format(conf[CONF_PLATFORM]), __name__) - try: - remove = await platform.async_attach_trigger(hass, conf, action, info) - except InvalidDeviceAutomationConfig: - remove = False + remove = await platform.async_attach_trigger(hass, conf, action, info) if not remove: _LOGGER.error("Error setting up trigger %s", name) diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index ebbd1771e84..5733cd2e83e 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -4,6 +4,9 @@ import importlib import voluptuous as vol +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.const import CONF_PLATFORM from homeassistant.config import async_log_exception, config_without_domain from homeassistant.exceptions import HomeAssistantError @@ -52,7 +55,12 @@ async def _try_async_validate_config_item(hass, config, full_config=None): """Validate config item.""" try: config = await async_validate_config_item(hass, config, full_config) - except (vol.Invalid, HomeAssistantError, IntegrationNotFound) as ex: + except ( + vol.Invalid, + HomeAssistantError, + IntegrationNotFound, + InvalidDeviceAutomationConfig, + ) as ex: async_log_exception(ex, DOMAIN, full_config or config, hass) return None diff --git a/homeassistant/components/automation/device.py b/homeassistant/components/automation/device.py index dc65008c3fb..ced8f65cbf5 100644 --- a/homeassistant/components/automation/device.py +++ b/homeassistant/components/automation/device.py @@ -18,6 +18,9 @@ async def async_validate_trigger_config(hass, config): platform = await async_get_device_automation_platform( hass, config[CONF_DOMAIN], "trigger" ) + if hasattr(platform, "async_validate_trigger_config"): + return await getattr(platform, "async_validate_trigger_config")(hass, config) + return platform.TRIGGER_SCHEMA(config) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 9f66cf156aa..27ff6fcd590 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -204,8 +204,10 @@ def _get_deconz_event_from_device_id(hass, device_id): return None -async def async_attach_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" +async def async_validate_trigger_config(hass, config): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + device_registry = await hass.helpers.device_registry.async_get_registry() device = device_registry.async_get(config[CONF_DEVICE_ID]) @@ -214,6 +216,16 @@ async def async_attach_trigger(hass, config, action, automation_info): if device.model not in REMOTES or trigger not in REMOTES[device.model]: raise InvalidDeviceAutomationConfig + return config + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(config[CONF_DEVICE_ID]) + + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + trigger = REMOTES[device.model][trigger] deconz_event = _get_deconz_event_from_device_id(hass, device.id) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index ddf7465e0c0..8d74ae108a2 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -21,8 +21,10 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( ) -async def async_attach_trigger(hass, config, action, automation_info): - """Listen for state changes based on configuration.""" +async def async_validate_trigger_config(hass, config): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) @@ -32,6 +34,14 @@ async def async_attach_trigger(hass, config, action, automation_info): ): raise InvalidDeviceAutomationConfig + return config + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + trigger = zha_device.device_automation_triggers[trigger] event_config = { diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 2f4ddb6b8b2..8df1a072801 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,6 +1,4 @@ """ZHA device automation trigger tests.""" -from unittest.mock import patch - import pytest import homeassistant.components.automation as automation @@ -197,7 +195,7 @@ async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): assert calls[0].data["message"] == "service called" -async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls): +async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, caplog): """Test for exception on event triggers firing.""" from zigpy.zcl.clusters.general import OnOff, Basic @@ -219,33 +217,32 @@ async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - with patch("logging.Logger.error") as mock: - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "device_id": reg_device.id, - "domain": "zha", - "platform": "device", - "type": "junk", - "subtype": "junk", - }, - "action": { - "service": "test.automation", - "data": {"message": "service called"}, - }, - } - ] - }, - ) - await hass.async_block_till_done() - mock.assert_called_with("Error setting up trigger %s", "automation 0") + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert "Invalid config for [automation]" in caplog.text -async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls): +async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls, caplog): """Test for exception on event triggers firing.""" from zigpy.zcl.clusters.general import OnOff, Basic @@ -275,27 +272,26 @@ async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls): ha_device_registry = await async_get_registry(hass) reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) - with patch("logging.Logger.error") as mock: - await async_setup_component( - hass, - automation.DOMAIN, - { - automation.DOMAIN: [ - { - "trigger": { - "device_id": reg_device.id, - "domain": "zha", - "platform": "device", - "type": "junk", - "subtype": "junk", - }, - "action": { - "service": "test.automation", - "data": {"message": "service called"}, - }, - } - ] - }, - ) - await hass.async_block_till_done() - mock.assert_called_with("Error setting up trigger %s", "automation 0") + await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "device_id": reg_device.id, + "domain": "zha", + "platform": "device", + "type": "junk", + "subtype": "junk", + }, + "action": { + "service": "test.automation", + "data": {"message": "service called"}, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert "Invalid config for [automation]" in caplog.text From a8db8d8c0b02cd7f259fe1fcf6fff3c55050b9bc Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 9 Oct 2019 21:44:02 +0200 Subject: [PATCH 115/639] deCONZ - Update discovery address (#27365) --- homeassistant/components/deconz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 1e5cd414425..63ab17d001a 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/deconz", "requirements": [ - "pydeconz==63" + "pydeconz==64" ], "ssdp": { "manufacturer": [ diff --git a/requirements_all.txt b/requirements_all.txt index fc2d498da7b..f17a70057b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1135,7 +1135,7 @@ pydaikin==1.6.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==63 +pydeconz==64 # homeassistant.components.delijn pydelijn==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 048601b3b99..328675e89bc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -300,7 +300,7 @@ pybotvac==0.0.16 pychromecast==4.0.1 # homeassistant.components.deconz -pydeconz==63 +pydeconz==64 # homeassistant.components.zwave pydispatcher==2.0.5 From 78e9bba279878a581a494da534617aebaae61f90 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Wed, 9 Oct 2019 21:56:16 +0200 Subject: [PATCH 116/639] Refactor Tradfri constants (#27334) * Refactor constants * Rename constant * Rename constant * Rename constant * Review update * Remove duplicate constant * Reorder constants * Dont refresh features * Order package imports * Fix bug * Put back features in refresh * Fix import order * Refactor supported features * Refactor supported features, take 2 --- homeassistant/components/tradfri/__init__.py | 46 ++++++++---------- .../components/tradfri/base_class.py | 6 +-- .../components/tradfri/config_flow.py | 5 +- homeassistant/components/tradfri/const.py | 22 ++++++++- homeassistant/components/tradfri/cover.py | 7 ++- homeassistant/components/tradfri/light.py | 48 +++++++++---------- homeassistant/components/tradfri/sensor.py | 5 +- homeassistant/components/tradfri/switch.py | 3 +- 8 files changed, 73 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index bca91134bed..c719fa41614 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -3,12 +3,22 @@ import logging import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP -import homeassistant.helpers.config_validation as cv from homeassistant.util.json import load_json - +from . import config_flow # noqa pylint_disable=unused-import from .const import ( + DOMAIN, + CONFIG_FILE, + KEY_GATEWAY, + KEY_API, + CONF_ALLOW_TRADFRI_GROUPS, + DEFAULT_ALLOW_TRADFRI_GROUPS, + TRADFRI_DEVICE_TYPES, + ATTR_TRADFRI_MANUFACTURER, + ATTR_TRADFRI_GATEWAY, + ATTR_TRADFRI_GATEWAY_MODEL, CONF_IMPORT_GROUPS, CONF_IDENTITY, CONF_HOST, @@ -16,18 +26,8 @@ from .const import ( CONF_GATEWAY_ID, ) -from . import config_flow # noqa pylint_disable=unused-import - _LOGGER = logging.getLogger(__name__) - -DOMAIN = "tradfri" -CONFIG_FILE = ".tradfri_psk.conf" -KEY_GATEWAY = "tradfri_gateway" -KEY_API = "tradfri_api" -CONF_ALLOW_TRADFRI_GROUPS = "allow_tradfri_groups" -DEFAULT_ALLOW_TRADFRI_GROUPS = False - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -124,24 +124,16 @@ async def async_setup_entry(hass, entry): config_entry_id=entry.entry_id, connections=set(), identifiers={(DOMAIN, entry.data[CONF_GATEWAY_ID])}, - manufacturer="IKEA", - name="Gateway", + manufacturer=ATTR_TRADFRI_MANUFACTURER, + name=ATTR_TRADFRI_GATEWAY, # They just have 1 gateway model. Type is not exposed yet. - model="E1526", + model=ATTR_TRADFRI_GATEWAY_MODEL, sw_version=gateway_info.firmware_version, ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "cover") - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "light") - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "sensor") - ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, "switch") - ) + for device in TRADFRI_DEVICE_TYPES: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, device) + ) return True diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index aa8487b087e..8430a342c09 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -5,7 +5,7 @@ from pytradfri.error import PytradfriError from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from . import DOMAIN as TRADFRI_DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -60,12 +60,12 @@ class TradfriBaseDevice(Entity): info = self._device.device_info return { - "identifiers": {(TRADFRI_DOMAIN, self._device.id)}, + "identifiers": {(DOMAIN, self._device.id)}, "manufacturer": info.manufacturer, "model": info.model_number, "name": self._name, "sw_version": info.firmware_version, - "via_device": (TRADFRI_DOMAIN, self._gateway_id), + "via_device": (DOMAIN, self._gateway_id), } @property diff --git a/homeassistant/components/tradfri/config_flow.py b/homeassistant/components/tradfri/config_flow.py index 9da381deb75..bdb195cf53f 100644 --- a/homeassistant/components/tradfri/config_flow.py +++ b/homeassistant/components/tradfri/config_flow.py @@ -7,18 +7,15 @@ import async_timeout import voluptuous as vol from homeassistant import config_entries - from .const import ( CONF_IMPORT_GROUPS, CONF_IDENTITY, CONF_HOST, CONF_KEY, CONF_GATEWAY_ID, + KEY_SECURITY_CODE, ) -KEY_SECURITY_CODE = "security_code" -KEY_IMPORT_GROUPS = "import_groups" - class AuthError(Exception): """Exception if authentication occurs.""" diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index d37b5d99f9f..a7acfcbf876 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -1,7 +1,25 @@ """Consts used by Tradfri.""" +from homeassistant.components.light import SUPPORT_TRANSITION, SUPPORT_BRIGHTNESS from homeassistant.const import CONF_HOST # noqa pylint: disable=unused-import -CONF_IMPORT_GROUPS = "import_groups" +ATTR_DIMMER = "dimmer" +ATTR_HUE = "hue" +ATTR_SAT = "saturation" +ATTR_TRADFRI_GATEWAY = "Gateway" +ATTR_TRADFRI_GATEWAY_MODEL = "E1526" +ATTR_TRADFRI_MANUFACTURER = "IKEA" +ATTR_TRANSITION_TIME = "transition_time" +CONF_ALLOW_TRADFRI_GROUPS = "allow_tradfri_groups" CONF_IDENTITY = "identity" -CONF_KEY = "key" +CONF_IMPORT_GROUPS = "import_groups" CONF_GATEWAY_ID = "gateway_id" +CONF_KEY = "key" +CONFIG_FILE = ".tradfri_psk.conf" +DEFAULT_ALLOW_TRADFRI_GROUPS = False +DOMAIN = "tradfri" +KEY_API = "tradfri_api" +KEY_GATEWAY = "tradfri_gateway" +KEY_SECURITY_CODE = "security_code" +SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION +SUPPORTED_LIGHT_FEATURES = SUPPORT_TRANSITION +TRADFRI_DEVICE_TYPES = ["cover", "light", "sensor", "switch"] diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 3dea978044f..1a3bf841665 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -11,8 +11,7 @@ from homeassistant.components.cover import ( SUPPORT_SET_POSITION, ) from homeassistant.core import callback -from . import DOMAIN as TRADFRI_DOMAIN, KEY_API, KEY_GATEWAY -from .const import CONF_GATEWAY_ID +from .const import DOMAIN, KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID _LOGGER = logging.getLogger(__name__) @@ -62,12 +61,12 @@ class TradfriCover(CoverDevice): info = self._cover.device_info return { - "identifiers": {(TRADFRI_DOMAIN, self._cover.id)}, + "identifiers": {(DOMAIN, self._cover.id)}, "name": self._name, "manufacturer": info.manufacturer, "model": info.model_number, "sw_version": info.firmware_version, - "via_device": (TRADFRI_DOMAIN, self._gateway_id), + "via_device": (DOMAIN, self._gateway_id), } async def async_added_to_hass(self): diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index f5d61f0aaed..089f80223e8 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -9,29 +9,28 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, - PLATFORM_SCHEMA as LIGHT_PLATFORM_SCHEMA, + Light, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, - SUPPORT_TRANSITION, - Light, ) -from homeassistant.components.tradfri.base_class import TradfriBaseDevice from homeassistant.core import callback -from . import KEY_API, KEY_GATEWAY -from .const import CONF_GATEWAY_ID, CONF_IMPORT_GROUPS +from .base_class import TradfriBaseDevice +from .const import ( + ATTR_DIMMER, + ATTR_HUE, + ATTR_SAT, + ATTR_TRANSITION_TIME, + SUPPORTED_LIGHT_FEATURES, + SUPPORTED_GROUP_FEATURES, + CONF_GATEWAY_ID, + CONF_IMPORT_GROUPS, + KEY_GATEWAY, + KEY_API, +) _LOGGER = logging.getLogger(__name__) -ATTR_DIMMER = "dimmer" -ATTR_HUE = "hue" -ATTR_SAT = "saturation" -ATTR_TRANSITION_TIME = "transition_time" -PLATFORM_SCHEMA = LIGHT_PLATFORM_SCHEMA -TRADFRI_LIGHT_MANAGER = "Tradfri Light Manager" -SUPPORTED_FEATURES = SUPPORT_TRANSITION -SUPPORTED_GROUP_FEATURES = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION - async def async_setup_entry(hass, config_entry, async_add_entities): """Load Tradfri lights based on a config entry.""" @@ -152,7 +151,16 @@ class TradfriLight(TradfriBaseDevice, Light): super().__init__(device, api, gateway_id) self._unique_id = f"light-{gateway_id}-{device.id}" self._hs_color = None - self._features = SUPPORTED_FEATURES + + # Calculate supported features + _features = SUPPORTED_LIGHT_FEATURES + if device.light_control.can_set_dimmer: + _features |= SUPPORT_BRIGHTNESS + if device.light_control.can_set_color: + _features |= SUPPORT_COLOR + if device.light_control.can_set_temp: + _features |= SUPPORT_COLOR_TEMP + self._features = _features self._refresh(device) @@ -297,11 +305,3 @@ class TradfriLight(TradfriBaseDevice, Light): # Caching of LightControl and light object self._device_control = device.light_control self._device_data = device.light_control.lights[0] - self._features = SUPPORTED_FEATURES - - if device.light_control.can_set_dimmer: - self._features |= SUPPORT_BRIGHTNESS - if device.light_control.can_set_color: - self._features |= SUPPORT_COLOR - if device.light_control.can_set_temp: - self._features |= SUPPORT_COLOR_TEMP diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 7814daf8f7a..56c1a464580 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,10 +1,9 @@ """Support for IKEA Tradfri sensors.""" import logging -from homeassistant.components.tradfri.base_class import TradfriBaseDevice from homeassistant.const import DEVICE_CLASS_BATTERY -from . import KEY_API, KEY_GATEWAY -from .const import CONF_GATEWAY_ID +from .base_class import TradfriBaseDevice +from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tradfri/switch.py b/homeassistant/components/tradfri/switch.py index 1e322ff47f5..e1c549a1805 100644 --- a/homeassistant/components/tradfri/switch.py +++ b/homeassistant/components/tradfri/switch.py @@ -1,8 +1,7 @@ """Support for IKEA Tradfri switches.""" from homeassistant.components.switch import SwitchDevice -from . import KEY_API, KEY_GATEWAY from .base_class import TradfriBaseDevice -from .const import CONF_GATEWAY_ID +from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID async def async_setup_entry(hass, config_entry, async_add_entities): From 74ef1358daa00330f10a93653fb9cc15161bb97b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 9 Oct 2019 23:06:27 +0200 Subject: [PATCH 117/639] Updated frontend to 20191002.2 (#27370) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 58e5558781a..67a66bc9612 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191002.1" + "home-assistant-frontend==20191002.2" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 99bb622e989..3f0588f2a99 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index f17a70057b8..cc107176261 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -643,7 +643,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 328675e89bc..acc8de8a1b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -185,7 +185,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.1 +home-assistant-frontend==20191002.2 # homeassistant.components.homekit_controller homekit[IP]==0.15.0 From 54c24de158daf3f03b684b0478b69e14b4780d9d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Oct 2019 16:16:29 -0700 Subject: [PATCH 118/639] Install requirements for all deps with tests (#27362) * Install requirements for all deps with tests * Remove unused REQUIREMENTS var * Print diff if not the same * Simplify * Update command line * Fix detecting empty dirs * Install non-integration * Fix upnp tests * Lint * Fix ZHA test --- .../components/epsonworkforce/sensor.py | 2 - .../components/ign_sismologia/geo_location.py | 2 - homeassistant/components/supla/__init__.py | 2 - homeassistant/components/upnp/device.py | 7 +- homeassistant/package_constraints.txt | 3 - requirements_test_all.txt | 192 +++++++++++- script/gen_requirements_all.py | 296 +++++------------- tests/components/upnp/test_init.py | 30 +- tests/components/zha/test_device_action.py | 2 +- 9 files changed, 284 insertions(+), 252 deletions(-) diff --git a/homeassistant/components/epsonworkforce/sensor.py b/homeassistant/components/epsonworkforce/sensor.py index 99e2723bf4a..b310376e5cc 100644 --- a/homeassistant/components/epsonworkforce/sensor.py +++ b/homeassistant/components/epsonworkforce/sensor.py @@ -10,8 +10,6 @@ from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -REQUIREMENTS = ["epsonprinter==0.0.9"] - _LOGGER = logging.getLogger(__name__) MONITORED_CONDITIONS = { "black": ["Ink level Black", "%", "mdi:water"], diff --git a/homeassistant/components/ign_sismologia/geo_location.py b/homeassistant/components/ign_sismologia/geo_location.py index 057d832b4fa..8ad045c9f7a 100644 --- a/homeassistant/components/ign_sismologia/geo_location.py +++ b/homeassistant/components/ign_sismologia/geo_location.py @@ -19,8 +19,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.event import track_time_interval -REQUIREMENTS = ["georss_ign_sismologia_client==0.2"] - _LOGGER = logging.getLogger(__name__) ATTR_EXTERNAL_ID = "external_id" diff --git a/homeassistant/components/supla/__init__.py b/homeassistant/components/supla/__init__.py index 86e763142e6..4293f187f5b 100644 --- a/homeassistant/components/supla/__init__.py +++ b/homeassistant/components/supla/__init__.py @@ -9,8 +9,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform from homeassistant.helpers.entity import Entity -REQUIREMENTS = ["pysupla==0.0.3"] - _LOGGER = logging.getLogger(__name__) DOMAIN = "supla" diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 5a5c7b38e7e..7f7f0f5b93a 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -3,6 +3,7 @@ import asyncio from ipaddress import IPv4Address import aiohttp +from async_upnp_client.profiles.igd import IgdDevice from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import HomeAssistantType @@ -29,9 +30,6 @@ class Device: if local_ip: local_ip = IPv4Address(local_ip) - # discover devices - from async_upnp_client.profiles.igd import IgdDevice - discovery_infos = await IgdDevice.async_search(source_ip=local_ip, timeout=10) # add extra info and store devices @@ -61,9 +59,6 @@ class Device: factory = UpnpFactory(requester, disable_state_variable_validation=True) upnp_device = await factory.async_create_device(ssdp_description) - # wrap with async_upnp_client.IgdDevice - from async_upnp_client.profiles.igd import IgdDevice - igd_device = IgdDevice(upnp_device, None) return cls(igd_device) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 3f0588f2a99..fb6239c8070 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -33,6 +33,3 @@ enum34==1000000000.0.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 - -# Contains code to modify Home Assistant to work around our rules -python-systemair-savecair==1000000000.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index acc8de8a1b3..2ce7eeb54a4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -30,15 +30,24 @@ HAP-python==2.6.0 # homeassistant.components.owntracks PyNaCl==1.3.0 +# homeassistant.auth.mfa_modules.totp +PyQRCode==1.2.1 + # homeassistant.components.rmvtransport PyRMVtransport==0.1.3 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 +# homeassistant.components.remember_the_milk +RtmAPI==0.7.0 + # homeassistant.components.yessssms YesssSMS==0.4.1 +# homeassistant.components.androidtv +adb-shell==0.0.4 + # homeassistant.components.adguard adguardhome==0.2.1 @@ -48,6 +57,9 @@ aio_geojson_geonetnz_quakes==0.10 # homeassistant.components.ambient_station aioambient==0.3.2 +# homeassistant.components.asuswrt +aioasuswrt==1.1.21 + # homeassistant.components.automatic aioautomatic==0.6.5 @@ -91,6 +103,13 @@ apns2==0.3.0 # homeassistant.components.aprs aprslib==0.6.46 +# homeassistant.components.arcam_fmj +arcam-fmj==0.4.3 + +# homeassistant.components.dlna_dmr +# homeassistant.components.upnp +async-upnp-client==0.14.11 + # homeassistant.components.stream av==6.1.2 @@ -100,17 +119,46 @@ axis==25 # homeassistant.components.zha bellows-homeassistant==0.10.0 +# homeassistant.components.bom +bomradarloop==0.1.3 + +# homeassistant.components.broadlink +broadlink==0.12.0 + +# homeassistant.components.buienradar +buienradar==1.0.1 + # homeassistant.components.caldav caldav==0.6.1 # homeassistant.components.coinmarketcap coinmarketcap==5.0.3 +# homeassistant.scripts.check_config +colorlog==4.0.2 + +# homeassistant.components.eddystone_temperature +# homeassistant.components.eq3btsmart +# homeassistant.components.xiaomi_miio +construct==2.9.45 + +# homeassistant.scripts.credstash +# credstash==1.15.0 + +# homeassistant.components.datadog +datadog==0.15.0 + # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect defusedxml==0.6.0 +# homeassistant.components.directv +directpy==0.5 + +# homeassistant.components.updater +distro==1.4.0 + # homeassistant.components.dsmr dsmr_parser==0.12 @@ -120,9 +168,6 @@ eebrightbox==0.0.4 # homeassistant.components.emulated_roku emulated_roku==0.1.8 -# homeassistant.components.enocean -enocean==0.50 - # homeassistant.components.season ephem==3.7.6.0 @@ -160,6 +205,9 @@ getmac==0.8.1 # homeassistant.components.google google-api-python-client==1.6.4 +# homeassistant.components.google_pubsub +google-cloud-pubsub==0.39.1 + # homeassistant.components.ffmpeg ha-ffmpeg==2.0 @@ -187,6 +235,9 @@ holidays==0.9.11 # homeassistant.components.frontend home-assistant-frontend==20191002.2 +# homeassistant.components.zwave +homeassistant-pyozw==0.1.4 + # homeassistant.components.homekit_controller homekit[IP]==0.15.0 @@ -209,12 +260,21 @@ influxdb==5.2.3 # homeassistant.components.verisure jsonpath==0.75 +# homeassistant.scripts.keyring +keyring==17.1.1 + +# homeassistant.scripts.keyring +keyrings.alt==3.1.1 + # homeassistant.components.dyson libpurecool==0.5.0 # homeassistant.components.soundtouch libsoundtouch==0.7.2 +# homeassistant.components.logi_circle +logi_circle==0.2.2 + # homeassistant.components.luftdaten luftdaten==0.6.3 @@ -227,10 +287,22 @@ mficlient==0.3.0 # homeassistant.components.minio minio==4.0.9 +# homeassistant.components.tts +mutagen==1.42.0 + +# homeassistant.components.ness_alarm +nessclient==0.9.15 + # homeassistant.components.discovery # homeassistant.components.ssdp netdisco==2.6.0 +# homeassistant.components.nsw_fuel_station +nsw-fuel-api-client==1.0.10 + +# homeassistant.components.nuheat +nuheat==0.3.0 + # homeassistant.components.iqvia # homeassistant.components.opencv # homeassistant.components.tensorflow @@ -268,6 +340,12 @@ plexauth==0.0.4 # homeassistant.components.serial_pm pmsensor==0.4 +# homeassistant.components.reddit +praw==6.3.1 + +# homeassistant.components.islamic_prayer_times +prayer_times_calculator==0.0.3 + # homeassistant.components.prometheus prometheus_client==0.7.1 @@ -280,6 +358,9 @@ pushbullet.py==0.11.0 # homeassistant.components.canary py-canary==0.5.0 +# homeassistant.components.melissa +py-melissa-climate==2.0.0 + # homeassistant.components.seventeentrack py17track==2.2.2 @@ -290,6 +371,15 @@ pyHS100==0.3.5 # homeassistant.components.norway_air pyMetno==0.4.6 +# homeassistant.components.rfxtrx +pyRFXtrx==0.23 + +# homeassistant.components.nextbus +py_nextbusnext==0.1.4 + +# homeassistant.components.arlo +pyarlo==0.2.3 + # homeassistant.components.blackbird pyblackbird==0.5 @@ -299,30 +389,69 @@ pybotvac==0.0.16 # homeassistant.components.cast pychromecast==4.0.1 +# homeassistant.components.daikin +pydaikin==1.6.1 + # homeassistant.components.deconz pydeconz==64 # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.everlights +pyeverlights==0.1.0 + +# homeassistant.components.fido +pyfido==2.1.1 + +# homeassistant.components.fritzbox +pyfritzhome==0.4.0 + +# homeassistant.components.ifttt +pyfttt==0.3 + +# homeassistant.components.version +pyhaversion==3.1.0 + # homeassistant.components.heos pyheos==0.6.0 # homeassistant.components.homematic pyhomematic==0.1.60 +# homeassistant.components.hydroquebec +pyhydroquebec==2.2.2 + +# homeassistant.components.ipma +pyipma==1.2.1 + # homeassistant.components.iqvia pyiqvia==0.2.1 +# homeassistant.components.kira +pykira==0.1.1 + +# homeassistant.components.webostv +pylgtv==0.1.9 + # homeassistant.components.linky pylinky==0.4.0 # homeassistant.components.litejet pylitejet==0.1 +# homeassistant.components.mailgun +pymailgunner==1.4 + # homeassistant.components.somfy pymfy==0.5.2 +# homeassistant.components.mochad +pymochad==0.2.0 + +# homeassistant.components.modbus +pymodbus==1.5.2 + # homeassistant.components.monoprice pymonoprice==0.3 @@ -343,6 +472,9 @@ pyotgw==0.5b0 # homeassistant.components.otp pyotp==2.3.0 +# homeassistant.components.point +pypoint==1.1.1 + # homeassistant.components.ps4 pyps4-2ndscreen==1.0.1 @@ -376,6 +508,9 @@ python-forecastio==1.4.0 # homeassistant.components.izone python-izone==1.1.1 +# homeassistant.components.xiaomi_miio +python-miio==0.4.6 + # homeassistant.components.nest python-nest==4.1.0 @@ -385,6 +520,9 @@ python-velbus==2.0.27 # homeassistant.components.awair python_awair==0.0.4 +# homeassistant.components.traccar +pytraccar==0.9.0 + # homeassistant.components.tradfri pytradfri[async]==6.3.1 @@ -409,6 +547,9 @@ ring_doorbell==0.2.3 # homeassistant.components.yamaha rxv==0.6.0 +# homeassistant.components.samsungtv +samsungctl[websocket]==0.7.1 + # homeassistant.components.simplisafe simplisafe-python==5.0.1 @@ -431,15 +572,29 @@ sqlalchemy==1.3.9 # homeassistant.components.statsd statsd==3.2.1 +# homeassistant.components.solaredge +# homeassistant.components.thermoworks_smoke +# homeassistant.components.traccar +stringcase==1.2.0 + +# homeassistant.components.tellduslive +tellduslive==0.10.10 + # homeassistant.components.toon toonapilib==3.2.4 +# homeassistant.components.tplink +tplink==0.2.1 + # homeassistant.components.transmission transmissionrpc==0.11 # homeassistant.components.twentemilieu twentemilieu==0.1.0 +# homeassistant.components.twilio +twilio==6.19.1 + # homeassistant.components.uvc uvcclient==0.11.0 @@ -454,11 +609,42 @@ vultr==0.1.2 # homeassistant.components.wake_on_lan wakeonlan==1.1.6 +# homeassistant.components.folder_watcher +watchdog==0.8.3 + +# homeassistant.components.webostv +websockets==6.0 + # homeassistant.components.withings withings-api==2.0.0b7 +# homeassistant.components.bluesound +# homeassistant.components.startca +# homeassistant.components.ted5000 +# homeassistant.components.yr +# homeassistant.components.zestimate +xmltodict==0.12.0 + +# homeassistant.components.yandex_transport +ya_ma==0.3.7 + +# homeassistant.components.yweather +yahooweather==0.10 + # homeassistant.components.zeroconf zeroconf==0.23.0 +# homeassistant.components.zha +zha-quirks==0.0.26 + +# homeassistant.components.zha +zigpy-deconz==0.5.0 + # homeassistant.components.zha zigpy-homeassistant==0.9.0 + +# homeassistant.components.zha +zigpy-xbee-homeassistant==0.5.0 + +# homeassistant.components.zha +zigpy-zigate==0.4.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index e8837b8d295..930ffa11b5f 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 """Generate an updated requirements_all.txt.""" +import difflib import importlib import os -import pathlib +from pathlib import Path import pkgutil import re import sys @@ -41,159 +42,8 @@ COMMENT_REQUIREMENTS = ( "VL53L1X2", ) -TEST_REQUIREMENTS = ( - "adguardhome", - "aio_geojson_geonetnz_quakes", - "aioambient", - "aioautomatic", - "aiobotocore", - "aioesphomeapi", - "aiohttp_cors", - "aiohue", - "aionotion", - "aioswitcher", - "aiounifi", - "aiowwlln", - "airly", - "ambiclimate", - "androidtv", - "apns2", - "aprslib", - "av", - "axis", - "bellows-homeassistant", - "caldav", - "coinmarketcap", - "defusedxml", - "dsmr_parser", - "eebrightbox", - "emulated_roku", - "enocean", - "ephem", - "evohomeclient", - "feedparser-homeassistant", - "foobot_async", - "geojson_client", - "geopy", - "georss_generic_client", - "georss_ign_sismologia_client", - "georss_qld_bushfire_alert_client", - "getmac", - "google-api-python-client", - "gTTS-token", - "ha-ffmpeg", - "hangups", - "HAP-python", - "hass-nabucasa", - "haversine", - "hbmqtt", - "hdate", - "herepy", - "hole", - "holidays", - "home-assistant-frontend", - "homekit[IP]", - "homematicip", - "httplib2", - "huawei-lte-api", - "iaqualink", - "influxdb", - "jsonpath", - "libpurecool", - "libsoundtouch", - "luftdaten", - "mbddns", - "mficlient", - "minio", - "netdisco", - "numpy", - "oauth2client", - "paho-mqtt", - "pexpect", - "pilight", - "pillow", - "plexapi", - "plexauth", - "pmsensor", - "prometheus_client", - "ptvsd", - "pushbullet.py", - "py-canary", - "py17track", - "pyblackbird", - "pybotvac", - "pychromecast", - "pydeconz", - "pydispatcher", - "pyheos", - "pyhomematic", - "pyHS100", - "pyiqvia", - "pylinky", - "pylitejet", - "pyMetno", - "pymfy", - "pymonoprice", - "PyNaCl", - "pynws", - "pynx584", - "pyopenuv", - "pyotgw", - "pyotp", - "pyps4-2ndscreen", - "pyqwikswitch", - "PyRMVtransport", - "pysma", - "pysmartapp", - "pysmartthings", - "pysoma", - "pysonos", - "pyspcwebgw", - "python_awair", - "python-ecobee-api", - "python-forecastio", - "python-izone", - "python-nest", - "python-velbus", - "pythonwhois", - "pytradfri[async]", - "PyTransportNSW", - "pyunifi", - "pyupnp-async", - "pyvesync", - "pywebpush", - "regenmaschine", - "restrictedpython", - "rflink", - "ring_doorbell", - "ruamel.yaml", - "rxv", - "simplisafe-python", - "sleepyq", - "smhi-pkg", - "solaredge", - "somecomfort", - "sqlalchemy", - "srpenergy", - "statsd", - "toonapilib", - "transmissionrpc", - "twentemilieu", - "uvcclient", - "vsure", - "vultr", - "wakeonlan", - "warrant", - "withings-api", - "YesssSMS", - "zeroconf", - "zigpy-homeassistant", -) - IGNORE_PIN = ("colorlog>2.1,<3", "keyring>=9.3,<10.0", "urllib3") -IGNORE_REQ = ("colorama<=1",) # Windows only requirement in check_config - URL_PIN = ( "https://developers.home-assistant.io/docs/" "creating_platform_code_review.html#1-requirements" @@ -211,12 +61,31 @@ enum34==1000000000.0.0 # This is a old unmaintained library and is replaced with pycryptodome pycrypto==1000000000.0.0 - -# Contains code to modify Home Assistant to work around our rules -python-systemair-savecair==1000000000.0.0 """ +def has_tests(module: str): + """Test if a module has tests. + + Module format: homeassistant.components.hue + Test if exists: tests/components/hue + """ + path = Path(module.replace(".", "/").replace("homeassistant", "tests")) + if not path.exists(): + return False + + if not path.is_dir(): + return True + + # Dev environments might have stale directories around + # from removed tests. Check for that. + content = [f.name for f in path.glob("*")] + + # Directories need to contain more than `__pycache__` + # to exist in Git and so be seen by CI. + return content != ["__pycache__"] + + def explore_module(package, explore_children): """Explore the modules.""" module = importlib.import_module(package) @@ -237,8 +106,9 @@ def explore_module(package, explore_children): def core_requirements(): """Gather core requirements out of setup.py.""" - with open("setup.py") as inp: - reqs_raw = re.search(r"REQUIRES = \[(.*?)\]", inp.read(), re.S).group(1) + reqs_raw = re.search( + r"REQUIRES = \[(.*?)\]", Path("setup.py").read_text(), re.S + ).group(1) return [x[1] for x in re.findall(r"(['\"])(.*?)\1", reqs_raw)] @@ -248,7 +118,7 @@ def gather_recursive_requirements(domain, seen=None): seen = set() seen.add(domain) - integration = Integration(pathlib.Path(f"homeassistant/components/{domain}")) + integration = Integration(Path(f"homeassistant/components/{domain}")) integration.load_manifest() reqs = set(integration.manifest["requirements"]) for dep_domain in integration.manifest["dependencies"]: @@ -283,7 +153,7 @@ def gather_modules(): def gather_requirements_from_manifests(errors, reqs): """Gather all of the requirements from manifests.""" - integrations = Integration.load_dir(pathlib.Path("homeassistant/components")) + integrations = Integration.load_dir(Path("homeassistant/components")) for domain in sorted(integrations): integration = integrations[domain] @@ -319,8 +189,6 @@ def gather_requirements_from_modules(errors, reqs): def process_requirements(errors, module_requirements, package, reqs): """Process all of the requirements.""" for req in module_requirements: - if req in IGNORE_REQ: - continue if "://" in req: errors.append(f"{package}[Only pypi dependencies are allowed: {req}]") if req.partition("==")[1] == "" and req not in IGNORE_PIN: @@ -359,15 +227,18 @@ def requirements_test_output(reqs): output = [] output.append("# Home Assistant test") output.append("\n") - with open("requirements_test.txt") as test_file: - output.append(test_file.read()) + output.append(Path("requirements_test.txt").read_text()) output.append("\n") + filtered = { - key: value - for key, value in reqs.items() + requirement: modules + for requirement, modules in reqs.items() if any( - re.search(r"(^|#){}($|[=><])".format(re.escape(ign)), key) is not None - for ign in TEST_REQUIREMENTS + # Always install requirements that are not part of integrations + not mdl.startswith("homeassistant.components.") or + # Install tests for integrations that have tests + has_tests(mdl) + for mdl in modules ) } output.append(generate_requirements_list(filtered)) @@ -377,48 +248,28 @@ def requirements_test_output(reqs): def gather_constraints(): """Construct output for constraint file.""" - return "\n".join( - sorted( - core_requirements() + list(gather_recursive_requirements("default_config")) + return ( + "\n".join( + sorted( + core_requirements() + + list(gather_recursive_requirements("default_config")) + ) + + [""] ) - + [""] + + CONSTRAINT_BASE ) -def write_requirements_file(data): - """Write the modules to the requirements_all.txt.""" - with open("requirements_all.txt", "w+", newline="\n") as req_file: - req_file.write(data) - - -def write_test_requirements_file(data): - """Write the modules to the requirements_test_all.txt.""" - with open("requirements_test_all.txt", "w+", newline="\n") as req_file: - req_file.write(data) - - -def write_constraints_file(data): - """Write constraints to a file.""" - with open(CONSTRAINT_PATH, "w+", newline="\n") as req_file: - req_file.write(data + CONSTRAINT_BASE) - - -def validate_requirements_file(data): - """Validate if requirements_all.txt is up to date.""" - with open("requirements_all.txt", "r") as req_file: - return data == req_file.read() - - -def validate_requirements_test_file(data): - """Validate if requirements_test_all.txt is up to date.""" - with open("requirements_test_all.txt", "r") as req_file: - return data == req_file.read() - - -def validate_constraints_file(data): - """Validate if constraints is up to date.""" - with open(CONSTRAINT_PATH, "r") as req_file: - return data + CONSTRAINT_BASE == req_file.read() +def diff_file(filename, content): + """Diff a file.""" + return list( + difflib.context_diff( + [line + "\n" for line in Path(filename).read_text().split("\n")], + [line + "\n" for line in content.split("\n")], + filename, + "generated", + ) + ) def main(validate): @@ -432,33 +283,38 @@ def main(validate): if data is None: return 1 - constraints = gather_constraints() - reqs_file = requirements_all_output(data) reqs_test_file = requirements_test_output(data) + constraints = gather_constraints() + + files = ( + ("requirements_all.txt", reqs_file), + ("requirements_test_all.txt", reqs_test_file), + ("homeassistant/package_constraints.txt", constraints), + ) if validate: errors = [] - if not validate_requirements_file(reqs_file): - errors.append("requirements_all.txt is not up to date") - if not validate_requirements_test_file(reqs_test_file): - errors.append("requirements_test_all.txt is not up to date") - - if not validate_constraints_file(constraints): - errors.append("home-assistant/package_constraints.txt is not up to date") + for filename, content in files: + diff = diff_file(filename, content) + if diff: + errors.append("".join(diff)) if errors: - print("******* ERROR") - print("\n".join(errors)) - print("Please run script/gen_requirements_all.py") + print("ERROR - FOUND THE FOLLOWING DIFFERENCES") + print() + print() + print("\n\n".join(errors)) + print() + print("Please run python3 -m script.gen_requirements_all") return 1 return 0 - write_requirements_file(reqs_file) - write_test_requirements_file(reqs_test_file) - write_constraints_file(constraints) + for filename, content in files: + Path(filename).write_text(content) + return 0 diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 5f17606146b..5e2106ff208 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -59,17 +59,20 @@ async def test_async_setup_entry_default(hass): } with MockDependency("netdisco.discovery"), patch( "homeassistant.components.upnp.get_local_ip", return_value="192.168.1.10" - ): + ), patch.object(Device, "async_create_device") as create_device, patch.object( + Device, "async_create_device" + ) as create_device, patch.object( + Device, "async_discover", return_value=mock_coro([]) + ) as async_discover: await async_setup_component(hass, "http", config) await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() - # mock homeassistant.components.upnp.device.Device - mock_device = MockDevice(udn) - discovery_infos = [{"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"}] - with patch.object(Device, "async_create_device") as create_device, patch.object( - Device, "async_discover" - ) as async_discover: # noqa:E125 + # mock homeassistant.components.upnp.device.Device + mock_device = MockDevice(udn) + discovery_infos = [ + {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} + ] create_device.return_value = mock_coro(return_value=mock_device) async_discover.return_value = mock_coro(return_value=discovery_infos) @@ -100,16 +103,17 @@ async def test_async_setup_entry_port_mapping(hass): } with MockDependency("netdisco.discovery"), patch( "homeassistant.components.upnp.get_local_ip", return_value="192.168.1.10" - ): + ), patch.object(Device, "async_create_device") as create_device, patch.object( + Device, "async_discover", return_value=mock_coro([]) + ) as async_discover: await async_setup_component(hass, "http", config) await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() - mock_device = MockDevice(udn) - discovery_infos = [{"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"}] - with patch.object(Device, "async_create_device") as create_device, patch.object( - Device, "async_discover" - ) as async_discover: # noqa:E125 + mock_device = MockDevice(udn) + discovery_infos = [ + {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} + ] create_device.return_value = mock_coro(return_value=mock_device) async_discover.return_value = mock_coro(return_value=discovery_infos) diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 6e7bc6ab4b1..91049a9bfa8 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -41,7 +41,7 @@ async def test_get_actions(hass, config_entry, zha_gateway): zha_gateway, ) - await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) + await hass.config_entries.async_forward_entry_setup(config_entry, "binary_sensor") await hass.async_block_till_done() hass.config_entries._entries.append(config_entry) From 762a714d87f30eaa09264a93384f8bc77ce171b7 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 10 Oct 2019 00:31:40 +0000 Subject: [PATCH 119/639] [ci skip] Translation update --- .../binary_sensor/.translations/ca.json | 2 ++ .../binary_sensor/.translations/fr.json | 20 ++++++++++++++++++- .../binary_sensor/.translations/lb.json | 2 ++ .../binary_sensor/.translations/no.json | 2 ++ .../binary_sensor/.translations/pl.json | 2 ++ .../binary_sensor/.translations/sl.json | 2 ++ .../components/deconz/.translations/pl.json | 1 + .../components/ecobee/.translations/pl.json | 2 +- .../components/hue/.translations/pl.json | 2 +- .../opentherm_gw/.translations/ko.json | 4 ++-- .../opentherm_gw/.translations/pl.json | 17 +++++++++++++--- .../components/plex/.translations/da.json | 4 ++++ .../components/plex/.translations/fr.json | 4 ++++ .../components/plex/.translations/pl.json | 7 ++++++- .../components/sensor/.translations/pl.json | 19 +++++++++++++++++- .../smartthings/.translations/pl.json | 2 +- .../components/zha/.translations/fr.json | 1 + .../components/zha/.translations/ko.json | 2 +- 18 files changed, 83 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/binary_sensor/.translations/ca.json b/homeassistant/components/binary_sensor/.translations/ca.json index de7d837b12c..8bbd19a0d45 100644 --- a/homeassistant/components/binary_sensor/.translations/ca.json +++ b/homeassistant/components/binary_sensor/.translations/ca.json @@ -53,6 +53,7 @@ "hot": "{entity_name} es torna calent", "light": "{entity_name} ha comen\u00e7at a detectar llum", "locked": "{entity_name} est\u00e0 bloquejat", + "moist": "{entity_name} es torna humit", "moist\u00a7": "{entity_name} es torna humit", "motion": "{entity_name} ha comen\u00e7at a detectar moviment", "moving": "{entity_name} ha comen\u00e7at a moure's", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} es torna sec", "not_moving": "{entity_name} ha parat de moure's", "not_occupied": "{entity_name} es desocupa", + "not_opened": "{entity_name} es tanca", "not_plugged_in": "{entity_name} desendollat", "not_powered": "{entity_name} no est\u00e0 alimentat", "not_present": "{entity_name} no est\u00e0 present", diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json index 80792f16635..1a11bfa4bc2 100644 --- a/homeassistant/components/binary_sensor/.translations/fr.json +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -40,9 +40,27 @@ "is_present": "{entity_name} est pr\u00e9sent", "is_problem": "{entity_name} d\u00e9tecte un probl\u00e8me", "is_smoke": "{entity_name} d\u00e9tecte de la fum\u00e9e", - "is_sound": "{entity_name} d\u00e9tecte du son" + "is_sound": "{entity_name} d\u00e9tecte du son", + "is_unsafe": "{entity_name} est dangereux", + "is_vibration": "{entity_name} d\u00e9tecte des vibrations" }, "trigger_type": { + "bat_low": "{entity_name} batterie faible", + "closed": "{entity_name} ferm\u00e9", + "cold": "{entity_name} est devenu froid", + "connected": "{entity_name} connect\u00e9", + "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz", + "not_occupied": "{entity_name} est devenu non occup\u00e9", + "not_plugged_in": "{entity_name} d\u00e9branch\u00e9", + "not_powered": "{entity_name} non aliment\u00e9", + "not_present": "{entity_name} non pr\u00e9sent", + "not_unsafe": "{entity_name} est devenu s\u00fbr", + "occupied": "{entity_name} est devenu occup\u00e9", + "opened": "{entity_name} ouvert", + "plugged_in": "{entity_name} branch\u00e9", + "powered": "{entity_name} aliment\u00e9", + "present": "{entity_name} pr\u00e9sent", + "problem": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter un probl\u00e8me", "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", "turned_off": "{entity_name} d\u00e9sactiv\u00e9", diff --git a/homeassistant/components/binary_sensor/.translations/lb.json b/homeassistant/components/binary_sensor/.translations/lb.json index 0b10e1f51a5..c65ae94396b 100644 --- a/homeassistant/components/binary_sensor/.translations/lb.json +++ b/homeassistant/components/binary_sensor/.translations/lb.json @@ -53,6 +53,7 @@ "hot": "{entity_name} gouf waarm", "light": "{entity_name} huet ugefange Luucht z'entdecken", "locked": "{entity_name} gespaart", + "moist": "{entity_name} gouf fiicht", "moist\u00a7": "{entity_name} gouf fiicht", "motion": "{entity_name} huet ugefaange Beweegung z'entdecken", "moving": "{entity_name} huet ugefaangen sech ze beweegen", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} gouf dr\u00e9chen", "not_moving": "{entity_name} huet opgehale sech ze beweegen", "not_occupied": "{entity_name} gouf fr\u00e4i", + "not_opened": "{entity_name} gouf zougemaach", "not_plugged_in": "{entity_name} net ugeschloss", "not_powered": "{entity_name} net aliment\u00e9iert", "not_present": "{entity_name} net pr\u00e4sent", diff --git a/homeassistant/components/binary_sensor/.translations/no.json b/homeassistant/components/binary_sensor/.translations/no.json index 5a1916bce59..4194102948b 100644 --- a/homeassistant/components/binary_sensor/.translations/no.json +++ b/homeassistant/components/binary_sensor/.translations/no.json @@ -53,6 +53,7 @@ "hot": "{entity_name} ble varm", "light": "{entity_name} begynte \u00e5 registrere lys", "locked": "{entity_name} l\u00e5st", + "moist": "{entity_name} ble fuktig", "moist\u00a7": "{entity_name} ble fuktig", "motion": "{entity_name} begynte \u00e5 registrere bevegelse", "moving": "{entity_name} begynte \u00e5 bevege seg", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} ble t\u00f8rr", "not_moving": "{entity_name} sluttet \u00e5 bevege seg", "not_occupied": "{entity_name} ble ledig", + "not_opened": "{entity_name} stengt", "not_plugged_in": "{entity_name} koblet fra", "not_powered": "{entity_name} spenningsl\u00f8s", "not_present": "{entity_name} ikke til stede", diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index a7f0bd516a0..a1ab03770f9 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -53,6 +53,7 @@ "hot": "sensor {entity_name} wykryje gor\u0105co", "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", "locked": "zamkni\u0119cie {entity_name}", + "moist": "sensor {entity_name} wykry\u0142 wilgo\u0107", "moist\u00a7": "sensor {entity_name} wykryje wilgo\u0107", "motion": "sensor {entity_name} wykryje ruch", "moving": "sensor {entity_name} zacznie porusza\u0107 si\u0119", @@ -71,6 +72,7 @@ "not_moist": "sensor {entity_name} przestanie wykrywa\u0107 wilgo\u0107", "not_moving": "sensor {entity_name} przestanie porusza\u0107 si\u0119", "not_occupied": "sensor {entity_name} przesta\u0142 by\u0107 zaj\u0119ty", + "not_opened": "sensor {entity_name} zosta\u0142 zamkni\u0119ty", "not_plugged_in": "od\u0142\u0105czenie {entity_name}", "not_powered": "od\u0142\u0105czenie zasilania {entity_name}", "not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107", diff --git a/homeassistant/components/binary_sensor/.translations/sl.json b/homeassistant/components/binary_sensor/.translations/sl.json index 6b4e144d9a6..2004caeb342 100644 --- a/homeassistant/components/binary_sensor/.translations/sl.json +++ b/homeassistant/components/binary_sensor/.translations/sl.json @@ -53,6 +53,7 @@ "hot": "{entity_name} je postal vro\u010d", "light": "{entity_name} za\u010del zaznavati svetlobo", "locked": "{entity_name} zaklenjen", + "moist": "{entity_name} postal vla\u017een", "moist\u00a7": "{entity_name} postal vla\u017een", "motion": "{entity_name} za\u010del zaznavati gibanje", "moving": "{entity_name} se je za\u010del premikati", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} je postalo suh", "not_moving": "{entity_name} se je prenehal premikati", "not_occupied": "{entity_name} ni zaseden", + "not_opened": "{entity_name} zaprto", "not_plugged_in": "{entity_name} odklopljen", "not_powered": "{entity_name} ni napajan", "not_present": "{entity_name} ni prisoten", diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 11a1beb10d6..498c8dd18d6 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -64,6 +64,7 @@ "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", "remote_button_rotated": "przycisk obr\u00f3cony \"{subtype}\"", + "remote_button_rotation_stopped": "obr\u00f3t przycisku \"{subtype}\" zatrzymany", "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", diff --git a/homeassistant/components/ecobee/.translations/pl.json b/homeassistant/components/ecobee/.translations/pl.json index 5c51d86fee4..bd4e7aa1ddc 100644 --- a/homeassistant/components/ecobee/.translations/pl.json +++ b/homeassistant/components/ecobee/.translations/pl.json @@ -5,7 +5,7 @@ }, "error": { "pin_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania kodu PIN od ecobee; sprawd\u017a, czy klucz API jest poprawny.", - "token_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania token\u00f3w od ecobee; prosz\u0119 spr\u00f3buj ponownie." + "token_request_failed": "B\u0142\u0105d podczas \u017c\u0105dania token\u00f3w od ecobee. Spr\u00f3buj ponownie." }, "step": { "authorize": { diff --git a/homeassistant/components/hue/.translations/pl.json b/homeassistant/components/hue/.translations/pl.json index 9062e427a27..33b1ffbfe86 100644 --- a/homeassistant/components/hue/.translations/pl.json +++ b/homeassistant/components/hue/.translations/pl.json @@ -12,7 +12,7 @@ }, "error": { "linking": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w trakcie \u0142\u0105czenia.", - "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Prosz\u0119 spr\u00f3bowa\u0107 ponownie." + "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Spr\u00f3buj ponownie." }, "step": { "init": { diff --git a/homeassistant/components/opentherm_gw/.translations/ko.json b/homeassistant/components/opentherm_gw/.translations/ko.json index 85790702435..e5daf826ee5 100644 --- a/homeassistant/components/opentherm_gw/.translations/ko.json +++ b/homeassistant/components/opentherm_gw/.translations/ko.json @@ -10,10 +10,10 @@ "init": { "data": { "device": "\uacbd\ub85c \ub610\ub294 URL", - "floor_temperature": "\uc9c0\uba74 \uae30\ud6c4 \uc628\ub3c4", + "floor_temperature": "\uc2e4\ub0b4\uc628\ub3c4 \uc18c\uc218\uc810 \ub0b4\ub9bc", "id": "ID", "name": "\uc774\ub984", - "precision": "\uae30\ud6c4 \uc628\ub3c4 \uc815\ubc00\ub3c4" + "precision": "\uc2e4\ub0b4\uc628\ub3c4 \uc815\ubc00\ub3c4" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index 7e4a0eed013..32e5cde82cb 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -1,12 +1,23 @@ { "config": { + "error": { + "already_configured": "Bramka jest ju\u017c skonfigurowana", + "id_exists": "Identyfikator bramki ju\u017c istnieje", + "serial_error": "B\u0142\u0105d po\u0142\u0105czenia z urz\u0105dzeniem", + "timeout": "Up\u0142yn\u0105\u0142 limit czasu pr\u00f3by po\u0142\u0105czenia" + }, "step": { "init": { "data": { "device": "\u015acie\u017cka lub adres URL", - "name": "Nazwa" - } + "floor_temperature": "Temperatura pod\u0142ogi", + "id": "Identyfikator", + "name": "Nazwa", + "precision": "Precyzja temperatury" + }, + "title": "Bramka OpenTherm" } - } + }, + "title": "Bramka OpenTherm" } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 1da4b4b4b49..4ca695e74d8 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -4,6 +4,7 @@ "all_configured": "Alle linkede servere er allerede konfigureret", "already_configured": "Denne Plex-server er allerede konfigureret", "already_in_progress": "Plex konfigureres", + "discovery_no_file": "Der blev ikke fundet nogen legacy konfigurationsfil", "invalid_import": "Importeret konfiguration er ugyldig", "token_request_timeout": "Timeout ved hentning af token", "unknown": "Mislykkedes af ukendt \u00e5rsag" @@ -32,6 +33,9 @@ "description": "Flere servere til r\u00e5dighed, v\u00e6lg en:", "title": "V\u00e6lg Plex-server" }, + "start_website_auth": { + "title": "Tilslut Plex-server" + }, "user": { "data": { "manual_setup": "Manuel ops\u00e6tning", diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index c9e61dcf2e9..3854b19b5d2 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -4,6 +4,7 @@ "all_configured": "Tous les serveurs li\u00e9s sont d\u00e9j\u00e0 configur\u00e9s", "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "Plex en cours de configuration", + "discovery_no_file": "Aucun fichier de configuration h\u00e9rit\u00e9 trouv\u00e9", "invalid_import": "La configuration import\u00e9e est invalide", "token_request_timeout": "D\u00e9lai d'obtention du jeton", "unknown": "\u00c9chec pour une raison inconnue" @@ -32,6 +33,9 @@ "description": "Plusieurs serveurs disponibles, s\u00e9lectionnez-en un:", "title": "S\u00e9lectionnez le serveur Plex" }, + "start_website_auth": { + "title": "Connecter un serveur Plex" + }, "user": { "data": { "manual_setup": "Installation manuelle", diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index 9b75a0061e8..0b94e3eacb6 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -4,6 +4,7 @@ "all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.", "already_configured": "Serwer Plex jest ju\u017c skonfigurowany", "already_in_progress": "Plex jest konfigurowany", + "discovery_no_file": "Nie znaleziono pliku konfiguracyjnego", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena", "unknown": "Nieznany b\u0142\u0105d" @@ -32,6 +33,10 @@ "description": "Dost\u0119pnych jest wiele serwer\u00f3w, wybierz jeden:", "title": "Wybierz serwer Plex" }, + "start_website_auth": { + "description": "Kontynuuj, by dokona\u0107 autoryzacji w plex.tv.", + "title": "Po\u0142\u0105cz z serwerem Plex" + }, "user": { "data": { "manual_setup": "Konfiguracja r\u0119czna", @@ -48,7 +53,7 @@ "plex_mp_settings": { "data": { "show_all_controls": "Poka\u017c wszystkie elementy steruj\u0105ce", - "use_episode_art": "U\u017cyj grafiki episodu" + "use_episode_art": "U\u017cyj grafiki odcinka" }, "description": "Opcje dla odtwarzaczy multimedialnych Plex" } diff --git a/homeassistant/components/sensor/.translations/pl.json b/homeassistant/components/sensor/.translations/pl.json index da1dcc1d6fd..68a3a0fecfd 100644 --- a/homeassistant/components/sensor/.translations/pl.json +++ b/homeassistant/components/sensor/.translations/pl.json @@ -3,7 +3,24 @@ "condition_type": { "is_battery_level": "{entity_name} poziom na\u0142adowania baterii", "is_humidity": "{entity_name} wilgotno\u015b\u0107", - "is_temperature": "{entity_name} temperatura" + "is_illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "is_power": "moc {entity_name}", + "is_pressure": "ci\u015bnienie {entity_name}", + "is_signal_strength": "si\u0142a sygna\u0142u {entity_name}", + "is_temperature": "temperatura {entity_name}", + "is_timestamp": "znacznik czasu {entity_name}", + "is_value": "warto\u015b\u0107 {entity_name}" + }, + "trigger_type": { + "battery_level": "poziom baterii {entity_name}", + "humidity": "wilgotno\u015b\u0107 {entity_name}", + "illuminance": "nat\u0119\u017cenie o\u015bwietlenia {entity_name}", + "power": "moc {entity_name}", + "pressure": "ci\u015bnienie {entity_name}", + "signal_strength": "si\u0142a sygna\u0142u {entity_name}", + "temperature": "temperatura {entity_name}", + "timestamp": "znacznik czasu {entity_name}", + "value": "warto\u015b\u0107 {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/pl.json b/homeassistant/components/smartthings/.translations/pl.json index 33803994764..849ad174134 100644 --- a/homeassistant/components/smartthings/.translations/pl.json +++ b/homeassistant/components/smartthings/.translations/pl.json @@ -2,7 +2,7 @@ "config": { "error": { "app_not_installed": "Upewnij si\u0119, \u017ce zainstalowa\u0142e\u015b i autoryzowa\u0142e\u015b Home Assistant SmartApp i spr\u00f3buj ponownie.", - "app_setup_error": "Nie mo\u017cna skonfigurowa\u0107 SmartApp. Prosz\u0119 spr\u00f3buj ponownie.", + "app_setup_error": "Nie mo\u017cna skonfigurowa\u0107 SmartApp. Spr\u00f3buj ponownie.", "base_url_not_https": "Parametr `base_url` dla komponentu `http` musi by\u0107 skonfigurowany i rozpoczyna\u0107 si\u0119 od `https://`.", "token_already_setup": "Token zosta\u0142 ju\u017c skonfigurowany.", "token_forbidden": "Token nie ma wymaganych zakres\u00f3w OAuth.", diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index f8b78af5721..d7b0a783116 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -38,6 +38,7 @@ "turn_on": "Allumer" }, "trigger_type": { + "device_dropped": "Appareil tomb\u00e9", "device_shaken": "Appareil secou\u00e9", "device_tilted": "Dispositif inclin\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json index 7ed1a8c69b4..f2277414a3e 100644 --- a/homeassistant/components/zha/.translations/ko.json +++ b/homeassistant/components/zha/.translations/ko.json @@ -39,7 +39,7 @@ "face_4": "\uba74 4\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", "face_5": "\uba74 5\ub97c \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", "face_6": "\uba74 6\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", - "face_any": "\uc784\uc758\uc758 \uba74 \ub610\ub294 \ud2b9\uc815 \uba74 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", + "face_any": "\uc784\uc758\uc758 \uba74 \ub610\ub294 \ud2b9\uc815 \uba74\uc744 \ud65c\uc131\ud654 \ud55c \ucc44\ub85c", "left": "\uc67c\ucabd", "open": "\uc5f4\uae30", "right": "\uc624\ub978\ucabd", From 80f6781f21f335fcd1524e00214c4c3c6706f5fc Mon Sep 17 00:00:00 2001 From: Santobert Date: Thu, 10 Oct 2019 08:08:11 +0200 Subject: [PATCH 120/639] Migrate Neato to use top-level imports (#27363) * Neato move imports up * Move one last import * Fix tests --- homeassistant/components/neato/__init__.py | 9 ++++----- homeassistant/components/neato/camera.py | 2 +- homeassistant/components/neato/config_flow.py | 6 ++---- homeassistant/components/neato/const.py | 4 ++-- homeassistant/components/neato/sensor.py | 4 ++-- homeassistant/components/neato/vacuum.py | 3 +-- tests/components/neato/test_config_flow.py | 20 +++++++++++-------- tests/components/neato/test_init.py | 4 ++-- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 14090c99a55..839c24568d8 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -3,8 +3,9 @@ import asyncio import logging from datetime import timedelta -from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException import voluptuous as vol +from pybotvac import Account, Neato, Vorwerk +from pybotvac.exceptions import NeatoException, NeatoLoginException, NeatoRobotException from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -17,9 +18,9 @@ from .const import ( NEATO_CONFIG, NEATO_DOMAIN, NEATO_LOGIN, - NEATO_ROBOTS, - NEATO_PERSISTENT_MAPS, NEATO_MAP_DATA, + NEATO_PERSISTENT_MAPS, + NEATO_ROBOTS, SCAN_INTERVAL_MINUTES, VALID_VENDORS, ) @@ -91,8 +92,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up config entry.""" - from pybotvac import Account, Neato, Vorwerk - if entry.data[CONF_VENDOR] == "neato": hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Neato) elif entry.data[CONF_VENDOR] == "vorwerk": diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index 98b48dd7225..f60835b1146 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -8,9 +8,9 @@ from homeassistant.components.camera import Camera from .const import ( NEATO_DOMAIN, + NEATO_LOGIN, NEATO_MAP_DATA, NEATO_ROBOTS, - NEATO_LOGIN, SCAN_INTERVAL_MINUTES, ) diff --git a/homeassistant/components/neato/config_flow.py b/homeassistant/components/neato/config_flow.py index 7ece3b8d300..56fba9047e7 100644 --- a/homeassistant/components/neato/config_flow.py +++ b/homeassistant/components/neato/config_flow.py @@ -2,8 +2,9 @@ import logging -import voluptuous as vol +from pybotvac import Account, Neato, Vorwerk from pybotvac.exceptions import NeatoLoginException, NeatoRobotException +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -11,7 +12,6 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME # pylint: disable=unused-import from .const import CONF_VENDOR, NEATO_DOMAIN, VALID_VENDORS - DOCS_URL = "https://www.home-assistant.io/components/neato" DEFAULT_VENDOR = "neato" @@ -96,8 +96,6 @@ class NeatoConfigFlow(config_entries.ConfigFlow, domain=NEATO_DOMAIN): @staticmethod def try_login(username, password, vendor): """Try logging in to device and return any errors.""" - from pybotvac import Account, Neato, Vorwerk - this_vendor = None if vendor == "vorwerk": this_vendor = Vorwerk() diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index 4d4178a6875..6dbaeb10d36 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -3,11 +3,11 @@ NEATO_DOMAIN = "neato" CONF_VENDOR = "vendor" -NEATO_ROBOTS = "neato_robots" -NEATO_LOGIN = "neato_login" NEATO_CONFIG = "neato_config" +NEATO_LOGIN = "neato_login" NEATO_MAP_DATA = "neato_map_data" NEATO_PERSISTENT_MAPS = "neato_persistent_maps" +NEATO_ROBOTS = "neato_robots" SCAN_INTERVAL_MINUTES = 5 diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 0201012cc37..36175151e0e 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -1,13 +1,13 @@ """Support for Neato sensors.""" +from datetime import timedelta import logging -from datetime import timedelta from pybotvac.exceptions import NeatoRobotException from homeassistant.components.sensor import DEVICE_CLASS_BATTERY from homeassistant.helpers.entity import Entity -from .const import NEATO_ROBOTS, NEATO_LOGIN, NEATO_DOMAIN, SCAN_INTERVAL_MINUTES +from .const import NEATO_DOMAIN, NEATO_LOGIN, NEATO_ROBOTS, SCAN_INTERVAL_MINUTES _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index 5d8fd42a5f7..40ed79042c7 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -3,7 +3,6 @@ from datetime import timedelta import logging from pybotvac.exceptions import NeatoRobotException - import voluptuous as vol from homeassistant.components.vacuum import ( @@ -35,8 +34,8 @@ from .const import ( ALERTS, ERRORS, MODE, - NEATO_LOGIN, NEATO_DOMAIN, + NEATO_LOGIN, NEATO_MAP_DATA, NEATO_PERSISTENT_MAPS, NEATO_ROBOTS, diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 8eb67e5d3e1..3f4bd90d0c1 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -2,9 +2,11 @@ import pytest from unittest.mock import patch +from pybotvac.exceptions import NeatoLoginException, NeatoRobotException + from homeassistant import data_entry_flow from homeassistant.components.neato import config_flow -from homeassistant.components.neato.const import NEATO_DOMAIN, CONF_VENDOR +from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry @@ -19,7 +21,7 @@ VENDOR_INVALID = "invalid" @pytest.fixture(name="account") def mock_controller_login(): """Mock a successful login.""" - with patch("pybotvac.Account", return_value=True): + with patch("homeassistant.components.neato.config_flow.Account", return_value=True): yield @@ -103,11 +105,12 @@ async def test_abort_if_already_setup(hass, account): async def test_abort_on_invalid_credentials(hass): """Test when we have invalid credentials.""" - from pybotvac.exceptions import NeatoLoginException - flow = init_config_flow(hass) - with patch("pybotvac.Account", side_effect=NeatoLoginException()): + with patch( + "homeassistant.components.neato.config_flow.Account", + side_effect=NeatoLoginException(), + ): result = await flow.async_step_user( { CONF_USERNAME: USERNAME, @@ -131,11 +134,12 @@ async def test_abort_on_invalid_credentials(hass): async def test_abort_on_unexpected_error(hass): """Test when we have an unexpected error.""" - from pybotvac.exceptions import NeatoRobotException - flow = init_config_flow(hass) - with patch("pybotvac.Account", side_effect=NeatoRobotException()): + with patch( + "homeassistant.components.neato.config_flow.Account", + side_effect=NeatoRobotException(), + ): result = await flow.async_step_user( { CONF_USERNAME: USERNAME, diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py index be7e43fdc0a..361f9eab1db 100644 --- a/tests/components/neato/test_init.py +++ b/tests/components/neato/test_init.py @@ -2,7 +2,7 @@ import pytest from unittest.mock import patch -from homeassistant.components.neato.const import NEATO_DOMAIN, CONF_VENDOR +from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component @@ -30,7 +30,7 @@ INVALID_CONFIG = { @pytest.fixture(name="account") def mock_controller_login(): """Mock a successful login.""" - with patch("pybotvac.Account", return_value=True): + with patch("homeassistant.components.neato.config_flow.Account", return_value=True): yield From 829cffd5def92f53786ba552eb2e2788e28c02bd Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Thu, 10 Oct 2019 03:05:46 -0400 Subject: [PATCH 121/639] Fix ecobee weather platform (#27369) * Fix ecobee weather platform * Remove custom forecast attributes * Tidy up process forecast method * Fix lint complaints * Add missed weather symbol --- homeassistant/components/ecobee/const.py | 28 +++++++ homeassistant/components/ecobee/weather.py | 94 +++++++++++++--------- 2 files changed, 83 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index 411f5ddeeeb..a6141d874f1 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -24,3 +24,31 @@ ECOBEE_MODEL_TO_NAME = { ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] MANUFACTURER = "ecobee" + +# Translates ecobee API weatherSymbol to HASS usable names +# https://www.ecobee.com/home/developer/api/documentation/v1/objects/WeatherForecast.shtml +ECOBEE_WEATHER_SYMBOL_TO_HASS = { + 0: "sunny", + 1: "partlycloudy", + 2: "partlycloudy", + 3: "cloudy", + 4: "cloudy", + 5: "cloudy", + 6: "rainy", + 7: "snowy-rainy", + 8: "pouring", + 9: "hail", + 10: "snowy", + 11: "snowy", + 12: "snowy-rainy", + 13: "snowy-heavy", + 14: "hail", + 15: "lightning-rainy", + 16: "windy", + 17: "tornado", + 18: "fog", + 19: "hazy", + 20: "hazy", + 21: "hazy", + -2: None, +} diff --git a/homeassistant/components/ecobee/weather.py b/homeassistant/components/ecobee/weather.py index 53e9842aae7..7b057f09a0c 100644 --- a/homeassistant/components/ecobee/weather.py +++ b/homeassistant/components/ecobee/weather.py @@ -8,17 +8,19 @@ from homeassistant.components.weather import ( ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, + ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, WeatherEntity, ) from homeassistant.const import TEMP_FAHRENHEIT -from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER, _LOGGER - -ATTR_FORECAST_TEMP_HIGH = "temphigh" -ATTR_FORECAST_PRESSURE = "pressure" -ATTR_FORECAST_VISIBILITY = "visibility" -ATTR_FORECAST_HUMIDITY = "humidity" +from .const import ( + DOMAIN, + ECOBEE_MODEL_TO_NAME, + ECOBEE_WEATHER_SYMBOL_TO_HASS, + MANUFACTURER, + _LOGGER, +) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -94,7 +96,7 @@ class EcobeeWeather(WeatherEntity): def condition(self): """Return the current condition.""" try: - return self.get_forecast(0, "condition") + return ECOBEE_WEATHER_SYMBOL_TO_HASS[self.get_forecast(0, "weatherSymbol")] except ValueError: return None @@ -131,7 +133,7 @@ class EcobeeWeather(WeatherEntity): def visibility(self): """Return the visibility.""" try: - return int(self.get_forecast(0, "visibility")) + return int(self.get_forecast(0, "visibility")) / 1000 except ValueError: return None @@ -154,45 +156,59 @@ class EcobeeWeather(WeatherEntity): @property def attribution(self): """Return the attribution.""" - if self.weather: - station = self.weather.get("weatherStation", "UNKNOWN") - time = self.weather.get("timestamp", "UNKNOWN") - return f"Ecobee weather provided by {station} at {time}" - return None + if not self.weather: + return None + + station = self.weather.get("weatherStation", "UNKNOWN") + time = self.weather.get("timestamp", "UNKNOWN") + return f"Ecobee weather provided by {station} at {time} UTC" @property def forecast(self): """Return the forecast array.""" - try: - forecasts = [] - for day in self.weather["forecasts"]: - date_time = datetime.strptime( - day["dateTime"], "%Y-%m-%d %H:%M:%S" - ).isoformat() - forecast = { - ATTR_FORECAST_TIME: date_time, - ATTR_FORECAST_CONDITION: day["condition"], - ATTR_FORECAST_TEMP: float(day["tempHigh"]) / 10, - } - if day["tempHigh"] == ECOBEE_STATE_UNKNOWN: - break - if day["tempLow"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_TEMP_LOW] = float(day["tempLow"]) / 10 - if day["pressure"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_PRESSURE] = int(day["pressure"]) - if day["windSpeed"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_WIND_SPEED] = int(day["windSpeed"]) - if day["visibility"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_WIND_SPEED] = int(day["visibility"]) - if day["relativeHumidity"] != ECOBEE_STATE_UNKNOWN: - forecast[ATTR_FORECAST_HUMIDITY] = int(day["relativeHumidity"]) - forecasts.append(forecast) - return forecasts - except (ValueError, IndexError, KeyError): + if "forecasts" not in self.weather: return None + forecasts = list() + for day in range(1, 5): + forecast = _process_forecast(self.weather["forecasts"][day]) + if forecast is None: + continue + forecasts.append(forecast) + + if forecasts: + return forecasts + return None + async def async_update(self): """Get the latest weather data.""" await self.data.update() thermostat = self.data.ecobee.get_thermostat(self._index) self.weather = thermostat.get("weather", None) + + +def _process_forecast(json): + """Process a single ecobee API forecast to return expected values.""" + forecast = dict() + try: + forecast[ATTR_FORECAST_TIME] = datetime.strptime( + json["dateTime"], "%Y-%m-%d %H:%M:%S" + ).isoformat() + forecast[ATTR_FORECAST_CONDITION] = ECOBEE_WEATHER_SYMBOL_TO_HASS[ + json["weatherSymbol"] + ] + if json["tempHigh"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_TEMP] = float(json["tempHigh"]) / 10 + if json["tempLow"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_TEMP_LOW] = float(json["tempLow"]) / 10 + if json["windBearing"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_WIND_BEARING] = int(json["windBearing"]) + if json["windSpeed"] != ECOBEE_STATE_UNKNOWN: + forecast[ATTR_FORECAST_WIND_SPEED] = int(json["windSpeed"]) + + except (ValueError, IndexError, KeyError): + return None + + if forecast: + return forecast + return None From a2591e696cfb5112cc30c7af505949e8092afedf Mon Sep 17 00:00:00 2001 From: Markus Nigbur Date: Thu, 10 Oct 2019 09:19:46 +0200 Subject: [PATCH 122/639] Move imports in vlc component (#27361) --- homeassistant/components/vlc/media_player.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vlc/media_player.py b/homeassistant/components/vlc/media_player.py index aaef128f33d..30b316cb4e8 100644 --- a/homeassistant/components/vlc/media_player.py +++ b/homeassistant/components/vlc/media_player.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import vlc from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -17,6 +18,7 @@ from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYI import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util + _LOGGER = logging.getLogger(__name__) CONF_ARGUMENTS = "arguments" @@ -51,8 +53,6 @@ class VlcDevice(MediaPlayerDevice): def __init__(self, name, arguments): """Initialize the vlc device.""" - import vlc - self._instance = vlc.Instance(arguments) self._vlc = self._instance.media_player_new() self._name = name @@ -65,8 +65,6 @@ class VlcDevice(MediaPlayerDevice): def update(self): """Get the latest details from the device.""" - import vlc - status = self._vlc.get_state() if status == vlc.State.Playing: self._state = STATE_PLAYING From 549c79b6ce7b477e5a224b812cd428052fca9ada Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 09:21:18 +0200 Subject: [PATCH 123/639] Move imports in season component (#27358) --- homeassistant/components/season/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/season/sensor.py b/homeassistant/components/season/sensor.py index cdd6af57617..46d2291cf81 100644 --- a/homeassistant/components/season/sensor.py +++ b/homeassistant/components/season/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import datetime +import ephem import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -67,7 +68,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def get_season(date, hemisphere, season_tracking_type): """Calculate the current season.""" - import ephem if hemisphere == "equator": return None From 6c739f4be5913ba46c25527c5883f6945716526c Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 09:21:40 +0200 Subject: [PATCH 124/639] Move imports in nissan_leaf component (#27359) --- homeassistant/components/nissan_leaf/__init__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index 38b7018af6c..0c72f4f43ea 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta import asyncio import logging import sys - +from pycarwings2 import CarwingsError, Session import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -95,7 +95,6 @@ START_CHARGE_LEAF_SCHEMA = vol.Schema({vol.Required(ATTR_VIN): cv.string}) def setup(hass, config): """Set up the Nissan Leaf component.""" - import pycarwings2 async def async_handle_update(service): """Handle service to update leaf data from Nissan servers.""" @@ -148,7 +147,7 @@ def setup(hass, config): try: # This might need to be made async (somehow) causes # homeassistant to be slow to start - sess = pycarwings2.Session(username, password, region) + sess = Session(username, password, region) leaf = sess.get_leaf() except KeyError: _LOGGER.error( @@ -156,7 +155,7 @@ def setup(hass, config): " do you actually have a Leaf connected to your account?" ) return False - except pycarwings2.CarwingsError: + except CarwingsError: _LOGGER.error( "An unknown error occurred while connecting to Nissan: %s", sys.exc_info()[0], @@ -274,7 +273,6 @@ class LeafDataStore: async def async_refresh_data(self, now): """Refresh the leaf data and update the datastore.""" - from pycarwings2 import CarwingsError if self.request_in_progress: _LOGGER.debug("Refresh currently in progress for %s", self.leaf.nickname) @@ -339,7 +337,6 @@ class LeafDataStore: async def async_get_battery(self): """Request battery update from Nissan servers.""" - from pycarwings2 import CarwingsError try: # Request battery update from the car @@ -389,7 +386,6 @@ class LeafDataStore: async def async_get_climate(self): """Request climate data from Nissan servers.""" - from pycarwings2 import CarwingsError try: return await self.hass.async_add_executor_job( From 7718d61cd7911fe5f3a351f63b3c84b1dcb7115f Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 09:22:10 +0200 Subject: [PATCH 125/639] Move imports in netatmo component (#27360) --- homeassistant/components/netatmo/__init__.py | 4 +--- homeassistant/components/netatmo/binary_sensor.py | 5 ++--- homeassistant/components/netatmo/camera.py | 4 ++-- homeassistant/components/netatmo/climate.py | 9 +-------- homeassistant/components/netatmo/sensor.py | 9 ++------- 5 files changed, 8 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 28d422557da..4b9f0690ac5 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -3,6 +3,7 @@ import logging from datetime import timedelta from urllib.error import HTTPError +import pyatmo import voluptuous as vol from homeassistant.const import ( @@ -89,7 +90,6 @@ SCHEMA_SERVICE_DROPWEBHOOK = vol.Schema({}) def setup(hass, config): """Set up the Netatmo devices.""" - import pyatmo hass.data[DATA_PERSONS] = {} try: @@ -254,8 +254,6 @@ class CameraData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the Netatmo API to update the data.""" - import pyatmo - self.camera_data = pyatmo.CameraData(self.auth, size=100) @Throttle(MIN_TIME_BETWEEN_EVENT_UPDATES) diff --git a/homeassistant/components/netatmo/binary_sensor.py b/homeassistant/components/netatmo/binary_sensor.py index 591cd790ecf..1a40d3952e9 100644 --- a/homeassistant/components/netatmo/binary_sensor.py +++ b/homeassistant/components/netatmo/binary_sensor.py @@ -1,6 +1,7 @@ """Support for the Netatmo binary sensors.""" import logging +from pyatmo import NoDevice import voluptuous as vol from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice @@ -58,15 +59,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): module_name = None - import pyatmo - auth = hass.data[DATA_NETATMO_AUTH] try: data = CameraData(hass, auth, home) if not data.get_camera_names(): return None - except pyatmo.NoDevice: + except NoDevice: return None welcome_sensors = config.get(CONF_WELCOME_SENSORS, WELCOME_SENSOR_TYPES) diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 60428961cb9..ecc38add3b4 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -1,6 +1,7 @@ """Support for the Netatmo cameras.""" import logging +from pyatmo import NoDevice import requests import voluptuous as vol @@ -38,7 +39,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): home = config.get(CONF_HOME) verify_ssl = config.get(CONF_VERIFY_SSL, True) quality = config.get(CONF_QUALITY, DEFAULT_QUALITY) - import pyatmo auth = hass.data[DATA_NETATMO_AUTH] @@ -60,7 +60,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] ) data.get_persons() - except pyatmo.NoDevice: + except NoDevice: return None diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 1465058652d..8ba13a03889 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from typing import Optional, List +import pyatmo import requests import voluptuous as vol @@ -103,8 +104,6 @@ NA_VALVE = "NRV" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NetAtmo Thermostat.""" - import pyatmo - homes_conf = config.get(CONF_HOMES) auth = hass.data[DATA_NETATMO_AUTH] @@ -365,8 +364,6 @@ class HomeData: def setup(self): """Retrieve HomeData by NetAtmo API.""" - import pyatmo - try: self.homedata = pyatmo.HomeData(self.auth) self.home_id = self.homedata.gethomeId(self.home) @@ -408,8 +405,6 @@ class ThermostatData: def setup(self): """Retrieve HomeData and HomeStatus by NetAtmo API.""" - import pyatmo - try: self.homedata = pyatmo.HomeData(self.auth) self.homestatus = pyatmo.HomeStatus(self.auth, home_id=self.home_id) @@ -423,8 +418,6 @@ class ThermostatData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Call the NetAtmo API to update the data.""" - import pyatmo - try: self.homestatus = pyatmo.HomeStatus(self.auth, home_id=self.home_id) except TypeError: diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 9e68c078cdc..38e3753708e 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -4,6 +4,7 @@ import threading from datetime import timedelta from time import time +import pyatmo import requests import voluptuous as vol @@ -174,8 +175,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if _dev: add_entities(_dev, True) - import pyatmo - for data_class in [pyatmo.WeatherStationData, pyatmo.HomeCoachData]: try: data = NetatmoData(auth, data_class, config.get(CONF_STATION)) @@ -512,8 +511,6 @@ class NetatmoPublicData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Request an update from the Netatmo API.""" - import pyatmo - data = pyatmo.PublicData( self.auth, LAT_NE=self.lat_ne, @@ -559,12 +556,10 @@ class NetatmoData: if time() < self._next_update or not self._update_in_progress.acquire(False): return try: - from pyatmo import NoDevice - try: self.station_data = self.data_class(self.auth) _LOGGER.debug("%s detected!", str(self.data_class.__name__)) - except NoDevice: + except pyatmo.NoDevice: _LOGGER.warning( "No Weather or HomeCoach devices found for %s", str(self.station) ) From 0cc2d0d557d28bf76cc11a09c9966729176aaaa6 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 10 Oct 2019 18:24:39 +1100 Subject: [PATCH 126/639] move import to top-level (#27353) --- homeassistant/components/onkyo/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 92e5f01d486..d6117283da7 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -3,6 +3,8 @@ import logging from typing import List import voluptuous as vol +import eiscp +from eiscp import eISCP from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -133,9 +135,6 @@ def determine_zones(receiver): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Onkyo platform.""" - import eiscp - from eiscp import eISCP - host = config.get(CONF_HOST) hosts = [] From d337b71725d9964bc07e29772e68edd649f613d5 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Thu, 10 Oct 2019 18:25:21 +1100 Subject: [PATCH 127/639] move import to top-level (#27352) --- homeassistant/components/systemmonitor/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index ad2072baaa5..b4621c59798 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -3,6 +3,7 @@ import logging import os import socket +import psutil import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -134,8 +135,6 @@ class SystemMonitorSensor(Entity): def update(self): """Get the latest system information.""" - import psutil - if self.type == "disk_use_percent": self._state = psutil.disk_usage(self.argument).percent elif self.type == "disk_use": From c188ecf79b4b0a335ee55fae5a02704fb7f29ddc Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 10 Oct 2019 14:21:42 +0200 Subject: [PATCH 128/639] Revert "Fix connection issues with withings API by switching to a maintained codebase (#27310)" (#27385) This reverts commit 071476343c10cdf8fe4a448082021eca3df7c8d7. --- homeassistant/components/withings/common.py | 14 +- .../components/withings/config_flow.py | 4 +- .../components/withings/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/withings/common.py | 12 +- tests/components/withings/conftest.py | 139 +++++++++--------- tests/components/withings/test_common.py | 20 +-- tests/components/withings/test_sensor.py | 44 +++--- 9 files changed, 117 insertions(+), 130 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index 9acca6f0cd6..f2be849cbc7 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import logging import re import time -import withings_api as withings +import nokia from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import TokenUpdated @@ -68,9 +68,7 @@ class WithingsDataManager: service_available = None - def __init__( - self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi - ): + def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): """Constructor.""" self._hass = hass self._api = api @@ -255,7 +253,7 @@ def create_withings_data_manager( """Set up the sensor config entry.""" entry_creds = entry.data.get(const.CREDENTIALS) or {} profile = entry.data[const.PROFILE] - credentials = withings.WithingsCredentials( + credentials = nokia.NokiaCredentials( entry_creds.get("access_token"), entry_creds.get("token_expiry"), entry_creds.get("token_type"), @@ -268,7 +266,7 @@ def create_withings_data_manager( def credentials_saver(credentials_param): _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - # Sanitizing the data as sometimes a WithingsCredentials object + # Sanitizing the data as sometimes a NokiaCredentials object # is passed through from the API. cred_data = credentials_param if not isinstance(credentials_param, dict): @@ -277,8 +275,8 @@ def create_withings_data_manager( entry.data[const.CREDENTIALS] = cred_data hass.config_entries.async_update_entry(entry, data={**entry.data}) - _LOGGER.debug("Creating withings api instance") - api = withings.WithingsApi( + _LOGGER.debug("Creating nokia api instance") + api = nokia.NokiaApi( credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index c781e785f5e..f28a4f59d80 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ import logging from typing import Optional import aiohttp -import withings_api as withings +import nokia import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -75,7 +75,7 @@ class WithingsFlowHandler(config_entries.ConfigFlow): profile, ) - return withings.WithingsAuth( + return nokia.NokiaAuth( client_id, client_secret, callback_uri, diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index ae5cd4bcdd9..d38b69f2248 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "withings-api==2.0.0b7" + "nokia==1.2.0" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index cc107176261..01ef364fdc6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -868,6 +868,9 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 +# homeassistant.components.withings +nokia==1.2.0 + # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 @@ -1973,9 +1976,6 @@ websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 -# homeassistant.components.withings -withings-api==2.0.0b7 - # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ce7eeb54a4..b14232fd5cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -297,6 +297,9 @@ nessclient==0.9.15 # homeassistant.components.ssdp netdisco==2.6.0 +# homeassistant.components.withings +nokia==1.2.0 + # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 @@ -615,9 +618,6 @@ watchdog==0.8.3 # homeassistant.components.webostv websockets==6.0 -# homeassistant.components.withings -withings-api==2.0.0b7 - # homeassistant.components.bluesound # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index f3839a1be55..b8406c39711 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,7 +1,7 @@ """Common data for for the withings component tests.""" import time -import withings_api as withings +import nokia import homeassistant.components.withings.const as const @@ -92,7 +92,7 @@ def new_measure(type_str, value, unit): } -def withings_sleep_response(states): +def nokia_sleep_response(states): """Create a sleep response based on states.""" data = [] for state in states: @@ -104,10 +104,10 @@ def withings_sleep_response(states): ) ) - return withings.WithingsSleep(new_sleep_data("aa", data)) + return nokia.NokiaSleep(new_sleep_data("aa", data)) -WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( +NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( { "updatetime": "", "timezone": "", @@ -174,7 +174,7 @@ WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( ) -WITHINGS_SLEEP_RESPONSE = withings_sleep_response( +NOKIA_SLEEP_RESPONSE = nokia_sleep_response( [ const.MEASURE_TYPE_SLEEP_STATE_AWAKE, const.MEASURE_TYPE_SLEEP_STATE_LIGHT, @@ -183,7 +183,7 @@ WITHINGS_SLEEP_RESPONSE = withings_sleep_response( ] ) -WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( +NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( { "series": [ new_sleep_summary( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 0aa6af0d7c0..7cbe3dc1cd4 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ import time from typing import Awaitable, Callable, List import asynctest -import withings_api as withings +import nokia import pytest import homeassistant.components.api as api @@ -15,9 +15,9 @@ from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC from homeassistant.setup import async_setup_component from .common import ( - WITHINGS_MEASURES_RESPONSE, - WITHINGS_SLEEP_RESPONSE, - WITHINGS_SLEEP_SUMMARY_RESPONSE, + NOKIA_MEASURES_RESPONSE, + NOKIA_SLEEP_RESPONSE, + NOKIA_SLEEP_SUMMARY_RESPONSE, ) @@ -34,17 +34,17 @@ class WithingsFactoryConfig: measures: List[str] = None, unit_system: str = None, throttle_interval: int = const.THROTTLE_INTERVAL, - withings_request_response="DATA", - withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, - withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, - withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, + nokia_request_response="DATA", + nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, + nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, + nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, ) -> None: """Constructor.""" self._throttle_interval = throttle_interval - self._withings_request_response = withings_request_response - self._withings_measures_response = withings_measures_response - self._withings_sleep_response = withings_sleep_response - self._withings_sleep_summary_response = withings_sleep_summary_response + self._nokia_request_response = nokia_request_response + self._nokia_measures_response = nokia_measures_response + self._nokia_sleep_response = nokia_sleep_response + self._nokia_sleep_summary_response = nokia_sleep_summary_response self._withings_config = { const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret", @@ -103,24 +103,24 @@ class WithingsFactoryConfig: return self._throttle_interval @property - def withings_request_response(self): + def nokia_request_response(self): """Request response.""" - return self._withings_request_response + return self._nokia_request_response @property - def withings_measures_response(self) -> withings.WithingsMeasures: + def nokia_measures_response(self) -> nokia.NokiaMeasures: """Measures response.""" - return self._withings_measures_response + return self._nokia_measures_response @property - def withings_sleep_response(self) -> withings.WithingsSleep: + def nokia_sleep_response(self) -> nokia.NokiaSleep: """Sleep response.""" - return self._withings_sleep_response + return self._nokia_sleep_response @property - def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: + def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: """Sleep summary response.""" - return self._withings_sleep_summary_response + return self._nokia_sleep_summary_response class WithingsFactoryData: @@ -130,21 +130,21 @@ class WithingsFactoryData: self, hass, flow_id, - withings_auth_get_credentials_mock, - withings_api_request_mock, - withings_api_get_measures_mock, - withings_api_get_sleep_mock, - withings_api_get_sleep_summary_mock, + nokia_auth_get_credentials_mock, + nokia_api_request_mock, + nokia_api_get_measures_mock, + nokia_api_get_sleep_mock, + nokia_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ): """Constructor.""" self._hass = hass self._flow_id = flow_id - self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock - self._withings_api_request_mock = withings_api_request_mock - self._withings_api_get_measures_mock = withings_api_get_measures_mock - self._withings_api_get_sleep_mock = withings_api_get_sleep_mock - self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock + self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock + self._nokia_api_request_mock = nokia_api_request_mock + self._nokia_api_get_measures_mock = nokia_api_get_measures_mock + self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock + self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock self._data_manager_get_throttle_interval_mock = ( data_manager_get_throttle_interval_mock ) @@ -160,29 +160,29 @@ class WithingsFactoryData: return self._flow_id @property - def withings_auth_get_credentials_mock(self): + def nokia_auth_get_credentials_mock(self): """Get auth credentials mock.""" - return self._withings_auth_get_credentials_mock + return self._nokia_auth_get_credentials_mock @property - def withings_api_request_mock(self): + def nokia_api_request_mock(self): """Get request mock.""" - return self._withings_api_request_mock + return self._nokia_api_request_mock @property - def withings_api_get_measures_mock(self): + def nokia_api_get_measures_mock(self): """Get measures mock.""" - return self._withings_api_get_measures_mock + return self._nokia_api_get_measures_mock @property - def withings_api_get_sleep_mock(self): + def nokia_api_get_sleep_mock(self): """Get sleep mock.""" - return self._withings_api_get_sleep_mock + return self._nokia_api_get_sleep_mock @property - def withings_api_get_sleep_summary_mock(self): + def nokia_api_get_sleep_summary_mock(self): """Get sleep summary mock.""" - return self._withings_api_get_sleep_summary_mock + return self._nokia_api_get_sleep_summary_mock @property def data_manager_get_throttle_interval_mock(self): @@ -243,9 +243,9 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: assert await async_setup_component(hass, http.DOMAIN, config.hass_config) assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - withings_auth_get_credentials_patch = asynctest.patch( - "withings_api.WithingsAuth.get_credentials", - return_value=withings.WithingsCredentials( + nokia_auth_get_credentials_patch = asynctest.patch( + "nokia.NokiaAuth.get_credentials", + return_value=nokia.NokiaCredentials( access_token="my_access_token", token_expiry=time.time() + 600, token_type="my_token_type", @@ -255,33 +255,28 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: consumer_secret=config.withings_config.get(const.CLIENT_SECRET), ), ) - withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() + nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() - withings_api_request_patch = asynctest.patch( - "withings_api.WithingsApi.request", - return_value=config.withings_request_response, + nokia_api_request_patch = asynctest.patch( + "nokia.NokiaApi.request", return_value=config.nokia_request_response ) - withings_api_request_mock = withings_api_request_patch.start() + nokia_api_request_mock = nokia_api_request_patch.start() - withings_api_get_measures_patch = asynctest.patch( - "withings_api.WithingsApi.get_measures", - return_value=config.withings_measures_response, + nokia_api_get_measures_patch = asynctest.patch( + "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response ) - withings_api_get_measures_mock = withings_api_get_measures_patch.start() + nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() - withings_api_get_sleep_patch = asynctest.patch( - "withings_api.WithingsApi.get_sleep", - return_value=config.withings_sleep_response, + nokia_api_get_sleep_patch = asynctest.patch( + "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response ) - withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() + nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() - withings_api_get_sleep_summary_patch = asynctest.patch( - "withings_api.WithingsApi.get_sleep_summary", - return_value=config.withings_sleep_summary_response, - ) - withings_api_get_sleep_summary_mock = ( - withings_api_get_sleep_summary_patch.start() + nokia_api_get_sleep_summary_patch = asynctest.patch( + "nokia.NokiaApi.get_sleep_summary", + return_value=config.nokia_sleep_summary_response, ) + nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() data_manager_get_throttle_interval_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" @@ -300,11 +295,11 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: patches.extend( [ - withings_auth_get_credentials_patch, - withings_api_request_patch, - withings_api_get_measures_patch, - withings_api_get_sleep_patch, - withings_api_get_sleep_summary_patch, + nokia_auth_get_credentials_patch, + nokia_api_request_patch, + nokia_api_get_measures_patch, + nokia_api_get_sleep_patch, + nokia_api_get_sleep_summary_patch, data_manager_get_throttle_interval_patch, get_measures_patch, ] @@ -333,11 +328,11 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: return WithingsFactoryData( hass, flow_id, - withings_auth_get_credentials_mock, - withings_api_request_mock, - withings_api_get_measures_mock, - withings_api_get_sleep_mock, - withings_api_get_sleep_summary_mock, + nokia_auth_get_credentials_mock, + nokia_api_request_mock, + nokia_api_get_measures_mock, + nokia_api_get_sleep_mock, + nokia_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 9f2480f9094..a22689f92bb 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,6 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import withings_api as withings +import nokia from oauthlib.oauth2.rfc6749.errors import MissingTokenError import pytest from requests_oauthlib import TokenUpdated @@ -13,19 +13,19 @@ from homeassistant.components.withings.common import ( from homeassistant.exceptions import PlatformNotReady -@pytest.fixture(name="withings_api") -def withings_api_fixture(): - """Provide withings api.""" - withings_api = withings.WithingsApi.__new__(withings.WithingsApi) - withings_api.get_measures = MagicMock() - withings_api.get_sleep = MagicMock() - return withings_api +@pytest.fixture(name="nokia_api") +def nokia_api_fixture(): + """Provide nokia api.""" + nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) + nokia_api.get_measures = MagicMock() + nokia_api.get_sleep = MagicMock() + return nokia_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, withings_api: withings.WithingsApi): +def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): """Provide data manager.""" - return WithingsDataManager(hass, "My Profile", withings_api) + return WithingsDataManager(hass, "My Profile", nokia_api) def test_print_service(): diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 697d0a8b864..da77910097b 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,12 +2,7 @@ from unittest.mock import MagicMock, patch import asynctest -from withings_api import ( - WithingsApi, - WithingsMeasures, - WithingsSleep, - WithingsSleepSummary, -) +from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary import pytest from homeassistant.components.withings import DOMAIN @@ -20,7 +15,7 @@ from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from .common import withings_sleep_response +from .common import nokia_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig @@ -125,9 +120,9 @@ async def test_health_sensor_state_none( data = await withings_factory( WithingsFactoryConfig( measures=measure, - withings_measures_response=None, - withings_sleep_response=None, - withings_sleep_summary_response=None, + nokia_measures_response=None, + nokia_sleep_response=None, + nokia_sleep_summary_response=None, ) ) @@ -158,9 +153,9 @@ async def test_health_sensor_state_empty( data = await withings_factory( WithingsFactoryConfig( measures=measure, - withings_measures_response=WithingsMeasures({"measuregrps": []}), - withings_sleep_response=WithingsSleep({"series": []}), - withings_sleep_summary_response=WithingsSleepSummary({"series": []}), + nokia_measures_response=NokiaMeasures({"measuregrps": []}), + nokia_sleep_response=NokiaSleep({"series": []}), + nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), ) ) @@ -206,8 +201,7 @@ async def test_sleep_state_throttled( data = await withings_factory( WithingsFactoryConfig( - measures=[measure], - withings_sleep_response=withings_sleep_response(sleep_states), + measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) ) ) @@ -263,16 +257,16 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): "expires_in": "2", } - original_withings_api = WithingsApi - withings_api_instance = None + original_nokia_api = NokiaApi + nokia_api_instance = None - def new_withings_api(*args, **kwargs): - nonlocal withings_api_instance - withings_api_instance = original_withings_api(*args, **kwargs) - withings_api_instance.request = MagicMock() - return withings_api_instance + def new_nokia_api(*args, **kwargs): + nonlocal nokia_api_instance + nokia_api_instance = original_nokia_api(*args, **kwargs) + nokia_api_instance.request = MagicMock() + return nokia_api_instance - withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) + nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( @@ -281,7 +275,7 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): wraps=hass.config_entries.async_update_entry, ) - with session_patch, client_patch, withings_api_patch, update_entry_patch: + with session_patch, client_patch, nokia_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( @@ -304,7 +298,7 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): await async_setup_entry(hass, config_entry, async_add_entities) - withings_api_instance.set_token(expected_creds) + nokia_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2" From 95c537bee88fd2db7507024a1e32eb52364facbb Mon Sep 17 00:00:00 2001 From: Ryan Ewen Date: Thu, 10 Oct 2019 10:53:52 -0400 Subject: [PATCH 129/639] Allow Google Assistant relative volume control (#26585) * Allow Google Assistant volume control without volume_level * Add test for relative volume control w/o volume_level --- .../components/google_assistant/trait.py | 37 +++++++++++++------ .../components/google_assistant/test_trait.py | 29 +++++++++++++++ 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 7d6e79a8237..26c2e2ee002 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1427,18 +1427,33 @@ class VolumeTrait(_Trait): async def _execute_volume_relative(self, data, params): # This could also support up/down commands using relativeSteps relative = params["volumeRelativeLevel"] - current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) - await self.hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_VOLUME_SET, - { - ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_MEDIA_VOLUME_LEVEL: current + relative / 100, - }, - blocking=True, - context=data.context, - ) + # if we have access to current volume level, do a single 'set' call + if media_player.ATTR_MEDIA_VOLUME_LEVEL in self.state.attributes: + current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) + + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_VOLUME_SET, + { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: current + relative / 100, + }, + blocking=True, + context=data.context, + ) + # otherwise do multiple 'up' or 'down' calls + else: + for _ in range(abs(relative)): + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_VOLUME_UP + if relative > 0 + else media_player.SERVICE_VOLUME_DOWN, + {ATTR_ENTITY_ID: self.state.entity_id}, + blocking=True, + context=data.context, + ) async def execute(self, command, data, params, challenge): """Execute a brightness command.""" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a5c527dacfe..d58281b0e11 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1599,6 +1599,35 @@ async def test_volume_media_player_relative(hass): } +async def test_volume_media_player_relative_no_vol_lvl(hass): + """Test volume trait support for media player domain.""" + trt = trait.VolumeTrait( + hass, State("media_player.bla", media_player.STATE_PLAYING, {}), BASIC_CONFIG + ) + + assert trt.sync_attributes() == {} + + assert trt.query_attributes() == {} + + up_calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_UP + ) + + await trt.execute( + trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"volumeRelativeLevel": 2}, {} + ) + assert len(up_calls) == 2 + + down_calls = async_mock_service( + hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_DOWN + ) + + await trt.execute( + trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"volumeRelativeLevel": -2}, {} + ) + assert len(down_calls) == 2 + + async def test_temperature_setting_sensor(hass): """Test TemperatureSetting trait support for temperature sensor.""" assert ( From 1719bc6fd3e6ba780b584812c443e037584096ef Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 10 Oct 2019 18:30:15 +0200 Subject: [PATCH 130/639] Remove hipchat (#27399) * Delete hipchat integration * Remove hipchat --- .coveragerc | 1 - homeassistant/components/hipchat/__init__.py | 1 - .../components/hipchat/manifest.json | 10 -- homeassistant/components/hipchat/notify.py | 108 ------------------ requirements_all.txt | 3 - 5 files changed, 123 deletions(-) delete mode 100644 homeassistant/components/hipchat/__init__.py delete mode 100644 homeassistant/components/hipchat/manifest.json delete mode 100644 homeassistant/components/hipchat/notify.py diff --git a/.coveragerc b/.coveragerc index 3de008439de..d241260fdf0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -275,7 +275,6 @@ omit = homeassistant/components/heatmiser/climate.py homeassistant/components/hikvision/binary_sensor.py homeassistant/components/hikvisioncam/switch.py - homeassistant/components/hipchat/notify.py homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hive/* homeassistant/components/hlk_sw16/* diff --git a/homeassistant/components/hipchat/__init__.py b/homeassistant/components/hipchat/__init__.py deleted file mode 100644 index 8b79982fa43..00000000000 --- a/homeassistant/components/hipchat/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The hipchat component.""" diff --git a/homeassistant/components/hipchat/manifest.json b/homeassistant/components/hipchat/manifest.json deleted file mode 100644 index 9d563719a2e..00000000000 --- a/homeassistant/components/hipchat/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "hipchat", - "name": "Hipchat", - "documentation": "https://www.home-assistant.io/integrations/hipchat", - "requirements": [ - "hipnotify==1.0.8" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/hipchat/notify.py b/homeassistant/components/hipchat/notify.py deleted file mode 100644 index 03556db386a..00000000000 --- a/homeassistant/components/hipchat/notify.py +++ /dev/null @@ -1,108 +0,0 @@ -"""HipChat platform for notify component.""" -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_HOST, CONF_ROOM, CONF_TOKEN -import homeassistant.helpers.config_validation as cv - -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService, -) - -_LOGGER = logging.getLogger(__name__) - -CONF_COLOR = "color" -CONF_NOTIFY = "notify" -CONF_FORMAT = "format" - -DEFAULT_COLOR = "yellow" -DEFAULT_FORMAT = "text" -DEFAULT_HOST = "https://api.hipchat.com/" -DEFAULT_NOTIFY = False - -VALID_COLORS = {"yellow", "green", "red", "purple", "gray", "random"} -VALID_FORMATS = {"text", "html"} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_ROOM): vol.Coerce(int), - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_COLOR, default=DEFAULT_COLOR): vol.In(VALID_COLORS), - vol.Optional(CONF_FORMAT, default=DEFAULT_FORMAT): vol.In(VALID_FORMATS), - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_NOTIFY, default=DEFAULT_NOTIFY): cv.boolean, - } -) - - -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 01ef364fdc6..001f199cdb8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -627,9 +627,6 @@ herepy==0.6.3.1 # homeassistant.components.hikvisioncam hikvision==0.4 -# homeassistant.components.hipchat -hipnotify==1.0.8 - # homeassistant.components.harman_kardon_avr hkavr==0.0.5 From 6c945c845e2f121cb6e323245b6cdc9b06a076f7 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 10 Oct 2019 19:30:30 +0300 Subject: [PATCH 131/639] Bump python-songpal (#27398) Fixes #24269 and fixes #26776 - potentially also #22116 --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 3160c4cee4b..2f0c44da47b 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Songpal", "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ - "python-songpal==0.0.9.1" + "python-songpal==0.11" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 001f199cdb8..b079438e3bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1558,7 +1558,7 @@ python-ripple-api==0.0.3 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.0.9.1 +python-songpal==0.11 # homeassistant.components.synologydsm python-synology==0.2.0 From e93ffa56881e7f638ffb91afb6e5922c131de392 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 10 Oct 2019 18:48:59 +0200 Subject: [PATCH 132/639] Move imports in waze_travel_time component (#27384) --- homeassistant/components/waze_travel_time/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 340c0adbc97..4392a20d801 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -3,21 +3,22 @@ from datetime import timedelta import logging import re +import WazeRouteCalculator import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, - CONF_NAME, - CONF_REGION, - EVENT_HOMEASSISTANT_START, ATTR_LATITUDE, ATTR_LONGITUDE, - CONF_UNIT_SYSTEM_METRIC, + CONF_NAME, + CONF_REGION, CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, + EVENT_HOMEASSISTANT_START, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import location +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -237,7 +238,6 @@ class WazeTravelTimeData: vehicle_type, ): """Set up WazeRouteCalculator.""" - import WazeRouteCalculator self._calc = WazeRouteCalculator From 7b13f0caf71f510a5a3eb62ba577eafda1668e90 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:50:58 +0200 Subject: [PATCH 133/639] Move imports in wemo component (#27393) --- homeassistant/components/wemo/__init__.py | 2 +- homeassistant/components/wemo/binary_sensor.py | 2 +- homeassistant/components/wemo/fan.py | 6 +++--- homeassistant/components/wemo/light.py | 4 ++-- homeassistant/components/wemo/switch.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 9e479991d15..df2d8ed1f31 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,6 +1,7 @@ """Support for WeMo device discovery.""" import logging +import pywemo import requests import voluptuous as vol @@ -87,7 +88,6 @@ def setup(hass, config): async def async_setup_entry(hass, entry): """Set up a wemo config entry.""" - import pywemo config = hass.data[DOMAIN] diff --git a/homeassistant/components/wemo/binary_sensor.py b/homeassistant/components/wemo/binary_sensor.py index 4ef18f29021..bc300fde571 100644 --- a/homeassistant/components/wemo/binary_sensor.py +++ b/homeassistant/components/wemo/binary_sensor.py @@ -3,6 +3,7 @@ import asyncio import logging import async_timeout +from pywemo import discovery import requests from homeassistant.components.binary_sensor import BinarySensorDevice @@ -15,7 +16,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Register discovered WeMo binary sensors.""" - from pywemo import discovery if discovery_info is not None: location = discovery_info["ssdp_description"] diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index dde5aa1cd89..91273fa033f 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -3,11 +3,12 @@ import asyncio import logging from datetime import timedelta -import requests import async_timeout +from pywemo import discovery +import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv +import homeassistant.helpers.config_validation as cv from homeassistant.components.fan import ( DOMAIN, SUPPORT_SET_SPEED, @@ -96,7 +97,6 @@ RESET_FILTER_LIFE_SCHEMA = vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.entity_i def setup_platform(hass, config, add_entities, discovery_info=None): """Set up discovered WeMo humidifiers.""" - from pywemo import discovery if DATA_KEY not in hass.data: hass.data[DATA_KEY] = {} diff --git a/homeassistant/components/wemo/light.py b/homeassistant/components/wemo/light.py index be6aa6f47f7..dab96eb8c94 100644 --- a/homeassistant/components/wemo/light.py +++ b/homeassistant/components/wemo/light.py @@ -3,8 +3,9 @@ import asyncio import logging from datetime import timedelta -import requests import async_timeout +from pywemo import discovery +import requests from homeassistant import util from homeassistant.components.light import ( @@ -35,7 +36,6 @@ SUPPORT_WEMO = ( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up discovered WeMo switches.""" - from pywemo import discovery if discovery_info is not None: location = discovery_info["ssdp_description"] diff --git a/homeassistant/components/wemo/switch.py b/homeassistant/components/wemo/switch.py index 1bc85506987..c1d07a06902 100644 --- a/homeassistant/components/wemo/switch.py +++ b/homeassistant/components/wemo/switch.py @@ -2,9 +2,10 @@ import asyncio import logging from datetime import datetime, timedelta -import requests import async_timeout +from pywemo import discovery +import requests from homeassistant.components.switch import SwitchDevice from homeassistant.exceptions import PlatformNotReady @@ -32,7 +33,6 @@ WEMO_STANDBY = 8 def setup_platform(hass, config, add_entities, discovery_info=None): """Set up discovered WeMo switches.""" - from pywemo import discovery if discovery_info is not None: location = discovery_info["ssdp_description"] From 91379b0ff755a2138fd4d4ddeaba27940e16db6d Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:51:28 +0200 Subject: [PATCH 134/639] Move imports in wink component (#27392) --- homeassistant/components/wink/__init__.py | 13 +++++-------- .../components/wink/alarm_control_panel.py | 3 ++- homeassistant/components/wink/binary_sensor.py | 3 ++- homeassistant/components/wink/cover.py | 3 ++- homeassistant/components/wink/fan.py | 3 ++- homeassistant/components/wink/light.py | 3 ++- homeassistant/components/wink/lock.py | 2 +- homeassistant/components/wink/scene.py | 3 ++- homeassistant/components/wink/sensor.py | 3 ++- homeassistant/components/wink/switch.py | 3 ++- homeassistant/components/wink/water_heater.py | 3 ++- 11 files changed, 24 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/wink/__init__.py b/homeassistant/components/wink/__init__.py index d0bb27c06e1..e2eb98938bb 100644 --- a/homeassistant/components/wink/__init__.py +++ b/homeassistant/components/wink/__init__.py @@ -5,6 +5,9 @@ import logging import os import time +from aiohttp.web import Response +import pywink +from pubnubsubhandler import PubNubSubscriptionHandler import voluptuous as vol from homeassistant.components.http import HomeAssistantView @@ -279,8 +282,6 @@ def _request_oauth_completion(hass, config): def setup(hass, config): """Set up the Wink component.""" - import pywink - from pubnubsubhandler import PubNubSubscriptionHandler if hass.data.get(DOMAIN) is None: hass.data[DOMAIN] = { @@ -689,8 +690,6 @@ class WinkAuthCallbackView(HomeAssistantView): @callback def get(self, request): """Finish OAuth callback request.""" - from aiohttp import web - hass = request.app["hass"] data = request.query @@ -715,15 +714,13 @@ class WinkAuthCallbackView(HomeAssistantView): hass.async_add_job(setup, hass, self.config) - return web.Response( + return Response( text=html_response.format(response_message), content_type="text/html" ) error_msg = "No code returned from Wink API" _LOGGER.error(error_msg) - return web.Response( - text=html_response.format(error_msg), content_type="text/html" - ) + return Response(text=html_response.format(error_msg), content_type="text/html") class WinkDevice(Entity): diff --git a/homeassistant/components/wink/alarm_control_panel.py b/homeassistant/components/wink/alarm_control_panel.py index 4708b6efee8..654252f5ffe 100644 --- a/homeassistant/components/wink/alarm_control_panel.py +++ b/homeassistant/components/wink/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support Wink alarm control panels.""" import logging +import pywink + import homeassistant.components.alarm_control_panel as alarm from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -17,7 +19,6 @@ STATE_ALARM_PRIVACY = "Private" def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for camera in pywink.get_cameras(): # get_cameras returns multiple device types. diff --git a/homeassistant/components/wink/binary_sensor.py b/homeassistant/components/wink/binary_sensor.py index e82a767fde8..6dd22a3f7b8 100644 --- a/homeassistant/components/wink/binary_sensor.py +++ b/homeassistant/components/wink/binary_sensor.py @@ -1,6 +1,8 @@ """Support for Wink binary sensors.""" import logging +import pywink + from homeassistant.components.binary_sensor import BinarySensorDevice from . import DOMAIN, WinkDevice @@ -26,7 +28,6 @@ SENSOR_TYPES = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink binary sensor platform.""" - import pywink for sensor in pywink.get_sensors(): _id = sensor.object_id() + sensor.name() diff --git a/homeassistant/components/wink/cover.py b/homeassistant/components/wink/cover.py index fa39909512a..1ce7f9b8875 100644 --- a/homeassistant/components/wink/cover.py +++ b/homeassistant/components/wink/cover.py @@ -1,4 +1,6 @@ """Support for Wink covers.""" +import pywink + from homeassistant.components.cover import ATTR_POSITION, CoverDevice from . import DOMAIN, WinkDevice @@ -6,7 +8,6 @@ from . import DOMAIN, WinkDevice def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink cover platform.""" - import pywink for shade in pywink.get_shades(): _id = shade.object_id() + shade.name() diff --git a/homeassistant/components/wink/fan.py b/homeassistant/components/wink/fan.py index 9f5f2f9b3a0..d1d4e30ada3 100644 --- a/homeassistant/components/wink/fan.py +++ b/homeassistant/components/wink/fan.py @@ -1,6 +1,8 @@ """Support for Wink fans.""" import logging +import pywink + from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -21,7 +23,6 @@ SUPPORTED_FEATURES = SUPPORT_DIRECTION + SUPPORT_SET_SPEED def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for fan in pywink.get_fans(): if fan.object_id() + fan.name() not in hass.data[DOMAIN]["unique_ids"]: diff --git a/homeassistant/components/wink/light.py b/homeassistant/components/wink/light.py index 76576f804fa..bd125e6a7c2 100644 --- a/homeassistant/components/wink/light.py +++ b/homeassistant/components/wink/light.py @@ -1,4 +1,6 @@ """Support for Wink lights.""" +import pywink + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -18,7 +20,6 @@ from . import DOMAIN, WinkDevice def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink lights.""" - import pywink for light in pywink.get_light_bulbs(): _id = light.object_id() + light.name() diff --git a/homeassistant/components/wink/lock.py b/homeassistant/components/wink/lock.py index 5246fb49eed..37b27c0d500 100644 --- a/homeassistant/components/wink/lock.py +++ b/homeassistant/components/wink/lock.py @@ -1,6 +1,7 @@ """Support for Wink locks.""" import logging +import pywink import voluptuous as vol from homeassistant.components.lock import LockDevice @@ -70,7 +71,6 @@ ADD_KEY_SCHEMA = vol.Schema( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for lock in pywink.get_locks(): _id = lock.object_id() + lock.name() diff --git a/homeassistant/components/wink/scene.py b/homeassistant/components/wink/scene.py index a00600ad784..ff083598b2e 100644 --- a/homeassistant/components/wink/scene.py +++ b/homeassistant/components/wink/scene.py @@ -1,6 +1,8 @@ """Support for Wink scenes.""" import logging +import pywink + from homeassistant.components.scene import Scene from . import DOMAIN, WinkDevice @@ -10,7 +12,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for scene in pywink.get_scenes(): _id = scene.object_id() + scene.name() diff --git a/homeassistant/components/wink/sensor.py b/homeassistant/components/wink/sensor.py index 030a1e5b9ec..2d0313ec211 100644 --- a/homeassistant/components/wink/sensor.py +++ b/homeassistant/components/wink/sensor.py @@ -1,6 +1,8 @@ """Support for Wink sensors.""" import logging +import pywink + from homeassistant.const import TEMP_CELSIUS from . import DOMAIN, WinkDevice @@ -12,7 +14,6 @@ SENSOR_TYPES = ["temperature", "humidity", "balance", "proximity"] def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for sensor in pywink.get_sensors(): _id = sensor.object_id() + sensor.name() diff --git a/homeassistant/components/wink/switch.py b/homeassistant/components/wink/switch.py index 07d3ff4becc..cf2264e7eeb 100644 --- a/homeassistant/components/wink/switch.py +++ b/homeassistant/components/wink/switch.py @@ -1,6 +1,8 @@ """Support for Wink switches.""" import logging +import pywink + from homeassistant.helpers.entity import ToggleEntity from . import DOMAIN, WinkDevice @@ -10,7 +12,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink platform.""" - import pywink for switch in pywink.get_switches(): _id = switch.object_id() + switch.name() diff --git a/homeassistant/components/wink/water_heater.py b/homeassistant/components/wink/water_heater.py index 4fceeeb313d..11330c7c9a5 100644 --- a/homeassistant/components/wink/water_heater.py +++ b/homeassistant/components/wink/water_heater.py @@ -1,6 +1,8 @@ """Support for Wink water heaters.""" import logging +import pywink + from homeassistant.components.water_heater import ( ATTR_TEMPERATURE, STATE_ECO, @@ -42,7 +44,6 @@ WINK_STATE_TO_HA = {value: key for key, value in HA_STATE_TO_WINK.items()} def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Wink water heater devices.""" - import pywink for water_heater in pywink.get_water_heaters(): _id = water_heater.object_id() + water_heater.name() From 84d1c0ca31ec6ca308ea41c30af0560c30aa5b72 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:52:03 +0200 Subject: [PATCH 135/639] Move imports in wunderlist component (#27391) --- homeassistant/components/wunderlist/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py index ce044499c63..122d09feaa4 100644 --- a/homeassistant/components/wunderlist/__init__.py +++ b/homeassistant/components/wunderlist/__init__.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +from wunderpy2 import WunderApi import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_ACCESS_TOKEN @@ -59,9 +60,7 @@ class Wunderlist: def __init__(self, access_token, client_id): """Create new instance of Wunderlist component.""" - import wunderpy2 - - api = wunderpy2.WunderApi() + api = WunderApi() self._client = api.get_client(access_token, client_id) _LOGGER.debug("Instance created") From a5ee138d563f096002aadbd0e14b0eff767f5ff6 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:52:19 +0200 Subject: [PATCH 136/639] Move imports in xmpp component (#27390) --- homeassistant/components/xmpp/notify.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/xmpp/notify.py b/homeassistant/components/xmpp/notify.py index 3719113f7c9..5aa9dbfffd1 100644 --- a/homeassistant/components/xmpp/notify.py +++ b/homeassistant/components/xmpp/notify.py @@ -7,6 +7,14 @@ import random import string import requests +import slixmpp +from slixmpp.exceptions import IqError, IqTimeout, XMPPError +from slixmpp.xmlstream.xmlstream import NotConnectedError +from slixmpp.plugins.xep_0363.http_upload import ( + FileTooBig, + FileUploadError, + UploadServiceNotFound, +) import voluptuous as vol from homeassistant.const import ( @@ -118,14 +126,6 @@ async def async_send_message( data=None, ): """Send a message over XMPP.""" - import slixmpp - from slixmpp.exceptions import IqError, IqTimeout, XMPPError - from slixmpp.xmlstream.xmlstream import NotConnectedError - from slixmpp.plugins.xep_0363.http_upload import ( - FileTooBig, - FileUploadError, - UploadServiceNotFound, - ) class SendNotificationBot(slixmpp.ClientXMPP): """Service for sending Jabber (XMPP) messages.""" From 19c8710698726ab2a57346b49628c14256cce4a7 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:53:27 +0200 Subject: [PATCH 137/639] Move imports in yamaha + yamaha_musiccast component (#27389) --- homeassistant/components/yamaha/media_player.py | 4 +--- homeassistant/components/yamaha_musiccast/media_player.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index e699ab74e68..eabb1ef34f1 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -2,6 +2,7 @@ import logging import requests +import rxv import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -82,7 +83,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yamaha platform.""" - import rxv # Keep track of configured receivers so that we don't end up # discovering a receiver dynamically that we have static config @@ -336,8 +336,6 @@ class YamahaDevice(MediaPlayerDevice): self._call_playback_function(self.receiver.next, "next track") def _call_playback_function(self, function, function_text): - import rxv - try: function() except rxv.exceptions.ResponseException: diff --git a/homeassistant/components/yamaha_musiccast/media_player.py b/homeassistant/components/yamaha_musiccast/media_player.py index 38e606a0962..18b80cc4085 100644 --- a/homeassistant/components/yamaha_musiccast/media_player.py +++ b/homeassistant/components/yamaha_musiccast/media_player.py @@ -1,6 +1,8 @@ """Support for Yamaha MusicCast Receivers.""" import logging +import socket +import pymusiccast import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -61,8 +63,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yamaha MusicCast platform.""" - import socket - import pymusiccast known_hosts = hass.data.get(KNOWN_HOSTS_KEY) if known_hosts is None: From f5560e2b18d5526a75166213af38f50c5ededc1b Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:53:52 +0200 Subject: [PATCH 138/639] Move imports in zengge component (#27387) --- homeassistant/components/zengge/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zengge/light.py b/homeassistant/components/zengge/light.py index a75cbba5f42..d890b193d72 100644 --- a/homeassistant/components/zengge/light.py +++ b/homeassistant/components/zengge/light.py @@ -1,6 +1,7 @@ """Support for Zengge lights.""" import logging +from zengge import zengge import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME @@ -47,12 +48,11 @@ class ZenggeLight(Light): def __init__(self, device): """Initialize the light.""" - import zengge self._name = device["name"] self._address = device["address"] self.is_valid = True - self._bulb = zengge.zengge(self._address) + self._bulb = zengge(self._address) self._white = 0 self._brightness = 0 self._hs_color = (0, 0) From ec08c251eaae3a76c7c62c614ddb1e447eba7535 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:54:20 +0200 Subject: [PATCH 139/639] Move imports in zestimate component (#27386) --- homeassistant/components/zestimate/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zestimate/sensor.py b/homeassistant/components/zestimate/sensor.py index 703e3bf25a0..4b8bdf5fa2e 100644 --- a/homeassistant/components/zestimate/sensor.py +++ b/homeassistant/components/zestimate/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging import requests +import xmltodict import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -101,7 +102,6 @@ class ZestimateDataSensor(Entity): def update(self): """Get the latest data and update the states.""" - import xmltodict try: response = requests.get(_RESOURCE, params=self.params, timeout=5) From 6364da115004f4f0074ff6ab17173526237ab3c7 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:56:07 +0200 Subject: [PATCH 140/639] Move imports in zigbee component (#27383) --- homeassistant/components/zigbee/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zigbee/__init__.py b/homeassistant/components/zigbee/__init__.py index 31cbc0c65b6..e74726a70f9 100644 --- a/homeassistant/components/zigbee/__init__.py +++ b/homeassistant/components/zigbee/__init__.py @@ -2,6 +2,11 @@ import logging from binascii import hexlify, unhexlify +import xbee_helper.const as xb_const +from xbee_helper import ZigBee +from xbee_helper.device import convert_adc +from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure +from serial import Serial, SerialException import voluptuous as vol from homeassistant.const import ( @@ -75,12 +80,6 @@ def setup(hass, config): global ZIGBEE_EXCEPTION global ZIGBEE_TX_FAILURE - import xbee_helper.const as xb_const - from xbee_helper import ZigBee - from xbee_helper.device import convert_adc - from xbee_helper.exceptions import ZigBeeException, ZigBeeTxFailure - from serial import Serial, SerialException - GPIO_DIGITAL_OUTPUT_LOW = xb_const.GPIO_DIGITAL_OUTPUT_LOW GPIO_DIGITAL_OUTPUT_HIGH = xb_const.GPIO_DIGITAL_OUTPUT_HIGH ADC_PERCENTAGE = xb_const.ADC_PERCENTAGE From fc7a20d1805134cbc0646a66ba298a66ef8274c1 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 18:57:00 +0200 Subject: [PATCH 141/639] Move imports in yr component (#27382) --- homeassistant/components/yr/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/yr/sensor.py b/homeassistant/components/yr/sensor.py index 3d8c63621be..f562f519ab5 100644 --- a/homeassistant/components/yr/sensor.py +++ b/homeassistant/components/yr/sensor.py @@ -7,6 +7,7 @@ from xml.parsers.expat import ExpatError import aiohttp import async_timeout +import xmltodict import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -155,7 +156,6 @@ class YrData: async def fetching_data(self, *_): """Get the latest data from yr.no.""" - import xmltodict def try_again(err: str): """Retry in 15 to 20 minutes.""" From 99885b9acfe5679bfb5c192dfb74606095c1c0b3 Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Thu, 10 Oct 2019 18:57:14 +0200 Subject: [PATCH 142/639] Move imports in google_travel_time component (#27381) --- .../components/google_travel_time/sensor.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/google_travel_time/sensor.py b/homeassistant/components/google_travel_time/sensor.py index 32947867958..3ee72928fc1 100644 --- a/homeassistant/components/google_travel_time/sensor.py +++ b/homeassistant/components/google_travel_time/sensor.py @@ -1,25 +1,25 @@ """Support for Google travel time sensors.""" +from datetime import datetime, timedelta import logging -from datetime import datetime -from datetime import timedelta +import googlemaps import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, - EVENT_HOMEASSISTANT_START, + ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, - ATTR_ATTRIBUTION, + CONF_API_KEY, CONF_MODE, + CONF_NAME, + EVENT_HOMEASSISTANT_START, ) from homeassistant.helpers import location +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -203,8 +203,6 @@ class GoogleTravelTimeSensor(Entity): else: self._destination = destination - import googlemaps - self._client = googlemaps.Client(api_key, timeout=10) try: self.update() From 13ac6ac315413fc24b0916c67961b35a0d6887c7 Mon Sep 17 00:00:00 2001 From: Markus Nigbur Date: Thu, 10 Oct 2019 20:16:19 +0200 Subject: [PATCH 143/639] Move imports in github component (#27406) --- homeassistant/components/github/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index a85364ebeca..5e8200b41ab 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -1,6 +1,7 @@ """Support for GitHub.""" from datetime import timedelta import logging +import github import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -148,8 +149,6 @@ class GitHubData: def __init__(self, repository, access_token=None, server_url=None): """Set up GitHub.""" - import github - self._github = github self.setup_error = False From 27f036c691c0fd53cc54ee59e263117968d546b9 Mon Sep 17 00:00:00 2001 From: Markus Nigbur Date: Thu, 10 Oct 2019 20:16:30 +0200 Subject: [PATCH 144/639] Move imports in eufy component (#27405) --- homeassistant/components/eufy/__init__.py | 2 +- homeassistant/components/eufy/light.py | 2 +- homeassistant/components/eufy/switch.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/eufy/__init__.py b/homeassistant/components/eufy/__init__.py index df6aed3582f..191d6ab5315 100644 --- a/homeassistant/components/eufy/__init__.py +++ b/homeassistant/components/eufy/__init__.py @@ -1,5 +1,6 @@ """Support for Eufy devices.""" import logging +import lakeside import voluptuous as vol @@ -56,7 +57,6 @@ EUFY_DISPATCH = { def setup(hass, config): """Set up Eufy devices.""" - import lakeside if CONF_USERNAME in config[DOMAIN] and CONF_PASSWORD in config[DOMAIN]: data = lakeside.get_devices( diff --git a/homeassistant/components/eufy/light.py b/homeassistant/components/eufy/light.py index f5359e6f2f6..21c26606bdd 100644 --- a/homeassistant/components/eufy/light.py +++ b/homeassistant/components/eufy/light.py @@ -1,5 +1,6 @@ """Support for Eufy lights.""" import logging +import lakeside from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -36,7 +37,6 @@ class EufyLight(Light): def __init__(self, device): """Initialize the light.""" - import lakeside self._temp = None self._brightness = None diff --git a/homeassistant/components/eufy/switch.py b/homeassistant/components/eufy/switch.py index 3d05ef5d351..2e13886dd2a 100644 --- a/homeassistant/components/eufy/switch.py +++ b/homeassistant/components/eufy/switch.py @@ -1,5 +1,6 @@ """Support for Eufy switches.""" import logging +import lakeside from homeassistant.components.switch import SwitchDevice @@ -18,7 +19,6 @@ class EufySwitch(SwitchDevice): def __init__(self, device): """Initialize the light.""" - import lakeside self._state = None self._name = device["name"] From 77490a3246b98f9f34c296e50d8277b8b57b38a7 Mon Sep 17 00:00:00 2001 From: Robert Van Gorkom Date: Thu, 10 Oct 2019 11:22:36 -0700 Subject: [PATCH 145/639] Vangorra withings fix (#27404) * Fixing connection issues with withings API by switching to a maintained client codebase. * Updating requirements files. * Adding withings api to requirements script. * Using version of withings api with static version in setup.py. * Updating requirements files. --- homeassistant/components/withings/common.py | 14 +- .../components/withings/config_flow.py | 4 +- .../components/withings/manifest.json | 2 +- requirements_all.txt | 6 +- requirements_test_all.txt | 6 +- tests/components/withings/common.py | 12 +- tests/components/withings/conftest.py | 139 +++++++++--------- tests/components/withings/test_common.py | 20 +-- tests/components/withings/test_sensor.py | 44 +++--- 9 files changed, 130 insertions(+), 117 deletions(-) diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index f2be849cbc7..9acca6f0cd6 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -4,7 +4,7 @@ import logging import re import time -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError from requests_oauthlib import TokenUpdated @@ -68,7 +68,9 @@ class WithingsDataManager: service_available = None - def __init__(self, hass: HomeAssistantType, profile: str, api: nokia.NokiaApi): + def __init__( + self, hass: HomeAssistantType, profile: str, api: withings.WithingsApi + ): """Constructor.""" self._hass = hass self._api = api @@ -253,7 +255,7 @@ def create_withings_data_manager( """Set up the sensor config entry.""" entry_creds = entry.data.get(const.CREDENTIALS) or {} profile = entry.data[const.PROFILE] - credentials = nokia.NokiaCredentials( + credentials = withings.WithingsCredentials( entry_creds.get("access_token"), entry_creds.get("token_expiry"), entry_creds.get("token_type"), @@ -266,7 +268,7 @@ def create_withings_data_manager( def credentials_saver(credentials_param): _LOGGER.debug("Saving updated credentials of type %s", type(credentials_param)) - # Sanitizing the data as sometimes a NokiaCredentials object + # Sanitizing the data as sometimes a WithingsCredentials object # is passed through from the API. cred_data = credentials_param if not isinstance(credentials_param, dict): @@ -275,8 +277,8 @@ def create_withings_data_manager( entry.data[const.CREDENTIALS] = cred_data hass.config_entries.async_update_entry(entry, data={**entry.data}) - _LOGGER.debug("Creating nokia api instance") - api = nokia.NokiaApi( + _LOGGER.debug("Creating withings api instance") + api = withings.WithingsApi( credentials, refresh_cb=(lambda token: credentials_saver(api.credentials)) ) diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index f28a4f59d80..c781e785f5e 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -4,7 +4,7 @@ import logging from typing import Optional import aiohttp -import nokia +import withings_api as withings import voluptuous as vol from homeassistant import config_entries, data_entry_flow @@ -75,7 +75,7 @@ class WithingsFlowHandler(config_entries.ConfigFlow): profile, ) - return nokia.NokiaAuth( + return withings.WithingsAuth( client_id, client_secret, callback_uri, diff --git a/homeassistant/components/withings/manifest.json b/homeassistant/components/withings/manifest.json index d38b69f2248..7c6e4ec044a 100644 --- a/homeassistant/components/withings/manifest.json +++ b/homeassistant/components/withings/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/withings", "requirements": [ - "nokia==1.2.0" + "withings-api==2.0.0b8" ], "dependencies": [ "api", diff --git a/requirements_all.txt b/requirements_all.txt index b079438e3bf..7e9d78afb46 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -865,9 +865,6 @@ niko-home-control==0.2.1 # homeassistant.components.nilu niluclient==0.1.2 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.nederlandse_spoorwegen nsapi==2.7.4 @@ -1973,6 +1970,9 @@ websockets==6.0 # homeassistant.components.wirelesstag wirelesstagpy==0.4.0 +# homeassistant.components.withings +withings-api==2.0.0b8 + # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b14232fd5cd..0e40702b9d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -297,9 +297,6 @@ nessclient==0.9.15 # homeassistant.components.ssdp netdisco==2.6.0 -# homeassistant.components.withings -nokia==1.2.0 - # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 @@ -618,6 +615,9 @@ watchdog==0.8.3 # homeassistant.components.webostv websockets==6.0 +# homeassistant.components.withings +withings-api==2.0.0b8 + # homeassistant.components.bluesound # homeassistant.components.startca # homeassistant.components.ted5000 diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index b8406c39711..f3839a1be55 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,7 +1,7 @@ """Common data for for the withings component tests.""" import time -import nokia +import withings_api as withings import homeassistant.components.withings.const as const @@ -92,7 +92,7 @@ def new_measure(type_str, value, unit): } -def nokia_sleep_response(states): +def withings_sleep_response(states): """Create a sleep response based on states.""" data = [] for state in states: @@ -104,10 +104,10 @@ def nokia_sleep_response(states): ) ) - return nokia.NokiaSleep(new_sleep_data("aa", data)) + return withings.WithingsSleep(new_sleep_data("aa", data)) -NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( +WITHINGS_MEASURES_RESPONSE = withings.WithingsMeasures( { "updatetime": "", "timezone": "", @@ -174,7 +174,7 @@ NOKIA_MEASURES_RESPONSE = nokia.NokiaMeasures( ) -NOKIA_SLEEP_RESPONSE = nokia_sleep_response( +WITHINGS_SLEEP_RESPONSE = withings_sleep_response( [ const.MEASURE_TYPE_SLEEP_STATE_AWAKE, const.MEASURE_TYPE_SLEEP_STATE_LIGHT, @@ -183,7 +183,7 @@ NOKIA_SLEEP_RESPONSE = nokia_sleep_response( ] ) -NOKIA_SLEEP_SUMMARY_RESPONSE = nokia.NokiaSleepSummary( +WITHINGS_SLEEP_SUMMARY_RESPONSE = withings.WithingsSleepSummary( { "series": [ new_sleep_summary( diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 7cbe3dc1cd4..0aa6af0d7c0 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -3,7 +3,7 @@ import time from typing import Awaitable, Callable, List import asynctest -import nokia +import withings_api as withings import pytest import homeassistant.components.api as api @@ -15,9 +15,9 @@ from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_METRIC from homeassistant.setup import async_setup_component from .common import ( - NOKIA_MEASURES_RESPONSE, - NOKIA_SLEEP_RESPONSE, - NOKIA_SLEEP_SUMMARY_RESPONSE, + WITHINGS_MEASURES_RESPONSE, + WITHINGS_SLEEP_RESPONSE, + WITHINGS_SLEEP_SUMMARY_RESPONSE, ) @@ -34,17 +34,17 @@ class WithingsFactoryConfig: measures: List[str] = None, unit_system: str = None, throttle_interval: int = const.THROTTLE_INTERVAL, - nokia_request_response="DATA", - nokia_measures_response: nokia.NokiaMeasures = NOKIA_MEASURES_RESPONSE, - nokia_sleep_response: nokia.NokiaSleep = NOKIA_SLEEP_RESPONSE, - nokia_sleep_summary_response: nokia.NokiaSleepSummary = NOKIA_SLEEP_SUMMARY_RESPONSE, + withings_request_response="DATA", + withings_measures_response: withings.WithingsMeasures = WITHINGS_MEASURES_RESPONSE, + withings_sleep_response: withings.WithingsSleep = WITHINGS_SLEEP_RESPONSE, + withings_sleep_summary_response: withings.WithingsSleepSummary = WITHINGS_SLEEP_SUMMARY_RESPONSE, ) -> None: """Constructor.""" self._throttle_interval = throttle_interval - self._nokia_request_response = nokia_request_response - self._nokia_measures_response = nokia_measures_response - self._nokia_sleep_response = nokia_sleep_response - self._nokia_sleep_summary_response = nokia_sleep_summary_response + self._withings_request_response = withings_request_response + self._withings_measures_response = withings_measures_response + self._withings_sleep_response = withings_sleep_response + self._withings_sleep_summary_response = withings_sleep_summary_response self._withings_config = { const.CLIENT_ID: "my_client_id", const.CLIENT_SECRET: "my_client_secret", @@ -103,24 +103,24 @@ class WithingsFactoryConfig: return self._throttle_interval @property - def nokia_request_response(self): + def withings_request_response(self): """Request response.""" - return self._nokia_request_response + return self._withings_request_response @property - def nokia_measures_response(self) -> nokia.NokiaMeasures: + def withings_measures_response(self) -> withings.WithingsMeasures: """Measures response.""" - return self._nokia_measures_response + return self._withings_measures_response @property - def nokia_sleep_response(self) -> nokia.NokiaSleep: + def withings_sleep_response(self) -> withings.WithingsSleep: """Sleep response.""" - return self._nokia_sleep_response + return self._withings_sleep_response @property - def nokia_sleep_summary_response(self) -> nokia.NokiaSleepSummary: + def withings_sleep_summary_response(self) -> withings.WithingsSleepSummary: """Sleep summary response.""" - return self._nokia_sleep_summary_response + return self._withings_sleep_summary_response class WithingsFactoryData: @@ -130,21 +130,21 @@ class WithingsFactoryData: self, hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ): """Constructor.""" self._hass = hass self._flow_id = flow_id - self._nokia_auth_get_credentials_mock = nokia_auth_get_credentials_mock - self._nokia_api_request_mock = nokia_api_request_mock - self._nokia_api_get_measures_mock = nokia_api_get_measures_mock - self._nokia_api_get_sleep_mock = nokia_api_get_sleep_mock - self._nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_mock + self._withings_auth_get_credentials_mock = withings_auth_get_credentials_mock + self._withings_api_request_mock = withings_api_request_mock + self._withings_api_get_measures_mock = withings_api_get_measures_mock + self._withings_api_get_sleep_mock = withings_api_get_sleep_mock + self._withings_api_get_sleep_summary_mock = withings_api_get_sleep_summary_mock self._data_manager_get_throttle_interval_mock = ( data_manager_get_throttle_interval_mock ) @@ -160,29 +160,29 @@ class WithingsFactoryData: return self._flow_id @property - def nokia_auth_get_credentials_mock(self): + def withings_auth_get_credentials_mock(self): """Get auth credentials mock.""" - return self._nokia_auth_get_credentials_mock + return self._withings_auth_get_credentials_mock @property - def nokia_api_request_mock(self): + def withings_api_request_mock(self): """Get request mock.""" - return self._nokia_api_request_mock + return self._withings_api_request_mock @property - def nokia_api_get_measures_mock(self): + def withings_api_get_measures_mock(self): """Get measures mock.""" - return self._nokia_api_get_measures_mock + return self._withings_api_get_measures_mock @property - def nokia_api_get_sleep_mock(self): + def withings_api_get_sleep_mock(self): """Get sleep mock.""" - return self._nokia_api_get_sleep_mock + return self._withings_api_get_sleep_mock @property - def nokia_api_get_sleep_summary_mock(self): + def withings_api_get_sleep_summary_mock(self): """Get sleep summary mock.""" - return self._nokia_api_get_sleep_summary_mock + return self._withings_api_get_sleep_summary_mock @property def data_manager_get_throttle_interval_mock(self): @@ -243,9 +243,9 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: assert await async_setup_component(hass, http.DOMAIN, config.hass_config) assert await async_setup_component(hass, api.DOMAIN, config.hass_config) - nokia_auth_get_credentials_patch = asynctest.patch( - "nokia.NokiaAuth.get_credentials", - return_value=nokia.NokiaCredentials( + withings_auth_get_credentials_patch = asynctest.patch( + "withings_api.WithingsAuth.get_credentials", + return_value=withings.WithingsCredentials( access_token="my_access_token", token_expiry=time.time() + 600, token_type="my_token_type", @@ -255,28 +255,33 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: consumer_secret=config.withings_config.get(const.CLIENT_SECRET), ), ) - nokia_auth_get_credentials_mock = nokia_auth_get_credentials_patch.start() + withings_auth_get_credentials_mock = withings_auth_get_credentials_patch.start() - nokia_api_request_patch = asynctest.patch( - "nokia.NokiaApi.request", return_value=config.nokia_request_response + withings_api_request_patch = asynctest.patch( + "withings_api.WithingsApi.request", + return_value=config.withings_request_response, ) - nokia_api_request_mock = nokia_api_request_patch.start() + withings_api_request_mock = withings_api_request_patch.start() - nokia_api_get_measures_patch = asynctest.patch( - "nokia.NokiaApi.get_measures", return_value=config.nokia_measures_response + withings_api_get_measures_patch = asynctest.patch( + "withings_api.WithingsApi.get_measures", + return_value=config.withings_measures_response, ) - nokia_api_get_measures_mock = nokia_api_get_measures_patch.start() + withings_api_get_measures_mock = withings_api_get_measures_patch.start() - nokia_api_get_sleep_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep", return_value=config.nokia_sleep_response + withings_api_get_sleep_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep", + return_value=config.withings_sleep_response, ) - nokia_api_get_sleep_mock = nokia_api_get_sleep_patch.start() + withings_api_get_sleep_mock = withings_api_get_sleep_patch.start() - nokia_api_get_sleep_summary_patch = asynctest.patch( - "nokia.NokiaApi.get_sleep_summary", - return_value=config.nokia_sleep_summary_response, + withings_api_get_sleep_summary_patch = asynctest.patch( + "withings_api.WithingsApi.get_sleep_summary", + return_value=config.withings_sleep_summary_response, + ) + withings_api_get_sleep_summary_mock = ( + withings_api_get_sleep_summary_patch.start() ) - nokia_api_get_sleep_summary_mock = nokia_api_get_sleep_summary_patch.start() data_manager_get_throttle_interval_patch = asynctest.patch( "homeassistant.components.withings.common.WithingsDataManager" @@ -295,11 +300,11 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: patches.extend( [ - nokia_auth_get_credentials_patch, - nokia_api_request_patch, - nokia_api_get_measures_patch, - nokia_api_get_sleep_patch, - nokia_api_get_sleep_summary_patch, + withings_auth_get_credentials_patch, + withings_api_request_patch, + withings_api_get_measures_patch, + withings_api_get_sleep_patch, + withings_api_get_sleep_summary_patch, data_manager_get_throttle_interval_patch, get_measures_patch, ] @@ -328,11 +333,11 @@ def withings_factory_fixture(request, hass) -> WithingsFactory: return WithingsFactoryData( hass, flow_id, - nokia_auth_get_credentials_mock, - nokia_api_request_mock, - nokia_api_get_measures_mock, - nokia_api_get_sleep_mock, - nokia_api_get_sleep_summary_mock, + withings_auth_get_credentials_mock, + withings_api_request_mock, + withings_api_get_measures_mock, + withings_api_get_sleep_mock, + withings_api_get_sleep_summary_mock, data_manager_get_throttle_interval_mock, ) diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index a22689f92bb..9f2480f9094 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,6 @@ """Tests for the Withings component.""" from asynctest import MagicMock -import nokia +import withings_api as withings from oauthlib.oauth2.rfc6749.errors import MissingTokenError import pytest from requests_oauthlib import TokenUpdated @@ -13,19 +13,19 @@ from homeassistant.components.withings.common import ( from homeassistant.exceptions import PlatformNotReady -@pytest.fixture(name="nokia_api") -def nokia_api_fixture(): - """Provide nokia api.""" - nokia_api = nokia.NokiaApi.__new__(nokia.NokiaApi) - nokia_api.get_measures = MagicMock() - nokia_api.get_sleep = MagicMock() - return nokia_api +@pytest.fixture(name="withings_api") +def withings_api_fixture(): + """Provide withings api.""" + withings_api = withings.WithingsApi.__new__(withings.WithingsApi) + withings_api.get_measures = MagicMock() + withings_api.get_sleep = MagicMock() + return withings_api @pytest.fixture(name="data_manager") -def data_manager_fixture(hass, nokia_api: nokia.NokiaApi): +def data_manager_fixture(hass, withings_api: withings.WithingsApi): """Provide data manager.""" - return WithingsDataManager(hass, "My Profile", nokia_api) + return WithingsDataManager(hass, "My Profile", withings_api) def test_print_service(): diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index da77910097b..697d0a8b864 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -2,7 +2,12 @@ from unittest.mock import MagicMock, patch import asynctest -from nokia import NokiaApi, NokiaMeasures, NokiaSleep, NokiaSleepSummary +from withings_api import ( + WithingsApi, + WithingsMeasures, + WithingsSleep, + WithingsSleepSummary, +) import pytest from homeassistant.components.withings import DOMAIN @@ -15,7 +20,7 @@ from homeassistant.helpers.entity_component import async_update_entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import slugify -from .common import nokia_sleep_response +from .common import withings_sleep_response from .conftest import WithingsFactory, WithingsFactoryConfig @@ -120,9 +125,9 @@ async def test_health_sensor_state_none( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=None, - nokia_sleep_response=None, - nokia_sleep_summary_response=None, + withings_measures_response=None, + withings_sleep_response=None, + withings_sleep_summary_response=None, ) ) @@ -153,9 +158,9 @@ async def test_health_sensor_state_empty( data = await withings_factory( WithingsFactoryConfig( measures=measure, - nokia_measures_response=NokiaMeasures({"measuregrps": []}), - nokia_sleep_response=NokiaSleep({"series": []}), - nokia_sleep_summary_response=NokiaSleepSummary({"series": []}), + withings_measures_response=WithingsMeasures({"measuregrps": []}), + withings_sleep_response=WithingsSleep({"series": []}), + withings_sleep_summary_response=WithingsSleepSummary({"series": []}), ) ) @@ -201,7 +206,8 @@ async def test_sleep_state_throttled( data = await withings_factory( WithingsFactoryConfig( - measures=[measure], nokia_sleep_response=nokia_sleep_response(sleep_states) + measures=[measure], + withings_sleep_response=withings_sleep_response(sleep_states), ) ) @@ -257,16 +263,16 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): "expires_in": "2", } - original_nokia_api = NokiaApi - nokia_api_instance = None + original_withings_api = WithingsApi + withings_api_instance = None - def new_nokia_api(*args, **kwargs): - nonlocal nokia_api_instance - nokia_api_instance = original_nokia_api(*args, **kwargs) - nokia_api_instance.request = MagicMock() - return nokia_api_instance + def new_withings_api(*args, **kwargs): + nonlocal withings_api_instance + withings_api_instance = original_withings_api(*args, **kwargs) + withings_api_instance.request = MagicMock() + return withings_api_instance - nokia_api_patch = patch("nokia.NokiaApi", side_effect=new_nokia_api) + withings_api_patch = patch("withings_api.WithingsApi", side_effect=new_withings_api) session_patch = patch("requests_oauthlib.OAuth2Session") client_patch = patch("oauthlib.oauth2.WebApplicationClient") update_entry_patch = patch.object( @@ -275,7 +281,7 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): wraps=hass.config_entries.async_update_entry, ) - with session_patch, client_patch, nokia_api_patch, update_entry_patch: + with session_patch, client_patch, withings_api_patch, update_entry_patch: async_add_entities = MagicMock() hass.config_entries.async_update_entry = MagicMock() config_entry = ConfigEntry( @@ -298,7 +304,7 @@ async def test_async_setup_entry_credentials_saver(hass: HomeAssistantType): await async_setup_entry(hass, config_entry, async_add_entities) - nokia_api_instance.set_token(expected_creds) + withings_api_instance.set_token(expected_creds) new_creds = config_entry.data[const.CREDENTIALS] assert new_creds["access_token"] == "my_access_token2" From 7e916773627ef889444d71675644099976ca5bc5 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 10 Oct 2019 20:39:09 +0200 Subject: [PATCH 146/639] Move imports in apple_tv component (#27356) * Move imports in apple_tv component * Fix pylint --- homeassistant/components/apple_tv/__init__.py | 19 +++++++------- .../components/apple_tv/media_player.py | 26 +++++++++---------- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/apple_tv/__init__.py b/homeassistant/components/apple_tv/__init__.py index 51c2ee7e1a5..38d520f73da 100644 --- a/homeassistant/components/apple_tv/__init__.py +++ b/homeassistant/components/apple_tv/__init__.py @@ -3,13 +3,15 @@ import asyncio import logging from typing import Sequence, TypeVar, Union +from pyatv import AppleTVDevice, connect_to_apple_tv, scan_for_apple_tvs +from pyatv.exceptions import DeviceAuthenticationError import voluptuous as vol +import homeassistant.helpers.config_validation as cv from homeassistant.components.discovery import SERVICE_APPLE_TV from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -80,7 +82,6 @@ def request_configuration(hass, config, atv, credentials): async def configuration_callback(callback_data): """Handle the submitted configuration.""" - from pyatv import exceptions pin = callback_data.get("pin") @@ -93,7 +94,7 @@ def request_configuration(hass, config, atv, credentials): title=NOTIFICATION_AUTH_TITLE, notification_id=NOTIFICATION_AUTH_ID, ) - except exceptions.DeviceAuthenticationError as ex: + except DeviceAuthenticationError as ex: hass.components.persistent_notification.async_create( "Authentication failed! Did you enter correct PIN?

" "Details: {0}".format(ex), @@ -112,11 +113,10 @@ def request_configuration(hass, config, atv, credentials): ) -async def scan_for_apple_tvs(hass): +async def scan_apple_tvs(hass): """Scan for devices and present a notification of the ones found.""" - import pyatv - atvs = await pyatv.scan_for_apple_tvs(hass.loop, timeout=3) + atvs = await scan_for_apple_tvs(hass.loop, timeout=3) devices = [] for atv in atvs: @@ -149,7 +149,7 @@ async def async_setup(hass, config): entity_ids = service.data.get(ATTR_ENTITY_ID) if service.service == SERVICE_SCAN: - hass.async_add_job(scan_for_apple_tvs, hass) + hass.async_add_job(scan_apple_tvs, hass) return if entity_ids: @@ -207,7 +207,6 @@ async def async_setup(hass, config): async def _setup_atv(hass, hass_config, atv_config): """Set up an Apple TV.""" - import pyatv name = atv_config.get(CONF_NAME) host = atv_config.get(CONF_HOST) @@ -218,9 +217,9 @@ async def _setup_atv(hass, hass_config, atv_config): if host in hass.data[DATA_APPLE_TV]: return - details = pyatv.AppleTVDevice(name, host, login_id) + details = AppleTVDevice(name, host, login_id) session = async_get_clientsession(hass) - atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session) + atv = connect_to_apple_tv(details, hass.loop, session=session) if credentials: await atv.airplay.load_credentials(credentials) diff --git a/homeassistant/components/apple_tv/media_player.py b/homeassistant/components/apple_tv/media_player.py index 9ac5ba77f98..c816be52259 100644 --- a/homeassistant/components/apple_tv/media_player.py +++ b/homeassistant/components/apple_tv/media_player.py @@ -1,6 +1,8 @@ """Support for Apple TV media player.""" import logging +import pyatv.const as atv_const + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, @@ -112,22 +114,21 @@ class AppleTvDevice(MediaPlayerDevice): return STATE_OFF if self._playing: - from pyatv import const state = self._playing.play_state if state in ( - const.PLAY_STATE_IDLE, - const.PLAY_STATE_NO_MEDIA, - const.PLAY_STATE_LOADING, + atv_const.PLAY_STATE_IDLE, + atv_const.PLAY_STATE_NO_MEDIA, + atv_const.PLAY_STATE_LOADING, ): return STATE_IDLE - if state == const.PLAY_STATE_PLAYING: + if state == atv_const.PLAY_STATE_PLAYING: return STATE_PLAYING if state in ( - const.PLAY_STATE_PAUSED, - const.PLAY_STATE_FAST_FORWARD, - const.PLAY_STATE_FAST_BACKWARD, - const.PLAY_STATE_STOPPED, + atv_const.PLAY_STATE_PAUSED, + atv_const.PLAY_STATE_FAST_FORWARD, + atv_const.PLAY_STATE_FAST_BACKWARD, + atv_const.PLAY_STATE_STOPPED, ): # Catch fast forward/backward here so "play" is default action return STATE_PAUSED @@ -156,14 +157,13 @@ class AppleTvDevice(MediaPlayerDevice): def media_content_type(self): """Content type of current playing media.""" if self._playing: - from pyatv import const media_type = self._playing.media_type - if media_type == const.MEDIA_TYPE_VIDEO: + if media_type == atv_const.MEDIA_TYPE_VIDEO: return MEDIA_TYPE_VIDEO - if media_type == const.MEDIA_TYPE_MUSIC: + if media_type == atv_const.MEDIA_TYPE_MUSIC: return MEDIA_TYPE_MUSIC - if media_type == const.MEDIA_TYPE_TV: + if media_type == atv_const.MEDIA_TYPE_TV: return MEDIA_TYPE_TVSHOW @property From 2e9e8a16bd001358c2f865c998fdad3a68f2edfa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Oct 2019 20:51:04 +0200 Subject: [PATCH 147/639] Remove hydroquebec integration (ADR-0004) (#27407) --- .../components/hydroquebec/__init__.py | 1 - .../components/hydroquebec/manifest.json | 10 - .../components/hydroquebec/sensor.py | 227 ------------------ requirements_all.txt | 3 - requirements_test_all.txt | 3 - tests/components/hydroquebec/__init__.py | 1 - tests/components/hydroquebec/test_sensor.py | 102 -------- 7 files changed, 347 deletions(-) delete mode 100644 homeassistant/components/hydroquebec/__init__.py delete mode 100644 homeassistant/components/hydroquebec/manifest.json delete mode 100644 homeassistant/components/hydroquebec/sensor.py delete mode 100644 tests/components/hydroquebec/__init__.py delete mode 100644 tests/components/hydroquebec/test_sensor.py diff --git a/homeassistant/components/hydroquebec/__init__.py b/homeassistant/components/hydroquebec/__init__.py deleted file mode 100644 index 08a12f7955e..00000000000 --- a/homeassistant/components/hydroquebec/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The hydroquebec component.""" diff --git a/homeassistant/components/hydroquebec/manifest.json b/homeassistant/components/hydroquebec/manifest.json deleted file mode 100644 index dbe8af0b41b..00000000000 --- a/homeassistant/components/hydroquebec/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "hydroquebec", - "name": "Hydroquebec", - "documentation": "https://www.home-assistant.io/integrations/hydroquebec", - "requirements": [ - "pyhydroquebec==2.2.2" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/hydroquebec/sensor.py b/homeassistant/components/hydroquebec/sensor.py deleted file mode 100644 index c3ad79c1c98..00000000000 --- a/homeassistant/components/hydroquebec/sensor.py +++ /dev/null @@ -1,227 +0,0 @@ -""" -Support for HydroQuebec. - -Get data from 'My Consumption Profile' page: -https://www.hydroquebec.com/portail/en/group/clientele/portrait-de-consommation - -For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.hydroquebec/ -""" -import logging -from datetime import timedelta - -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - ENERGY_KILO_WATT_HOUR, - CONF_NAME, - CONF_MONITORED_VARIABLES, - TEMP_CELSIUS, -) -from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -KILOWATT_HOUR = ENERGY_KILO_WATT_HOUR -PRICE = "CAD" -DAYS = "days" -CONF_CONTRACT = "contract" - -DEFAULT_NAME = "HydroQuebec" - -REQUESTS_TIMEOUT = 15 -MIN_TIME_BETWEEN_UPDATES = timedelta(hours=1) -SCAN_INTERVAL = timedelta(hours=1) - -SENSOR_TYPES = { - "balance": ["Balance", PRICE, "mdi:square-inc-cash"], - "period_total_bill": ["Period total bill", PRICE, "mdi:square-inc-cash"], - "period_length": ["Period length", DAYS, "mdi:calendar-today"], - "period_total_days": ["Period total days", DAYS, "mdi:calendar-today"], - "period_mean_daily_bill": ["Period mean daily bill", PRICE, "mdi:square-inc-cash"], - "period_mean_daily_consumption": [ - "Period mean daily consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "period_total_consumption": [ - "Period total consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "period_lower_price_consumption": [ - "Period lower price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "period_higher_price_consumption": [ - "Period higher price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_total_consumption": [ - "Yesterday total consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_lower_price_consumption": [ - "Yesterday lower price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_higher_price_consumption": [ - "Yesterday higher price consumption", - KILOWATT_HOUR, - "mdi:flash", - ], - "yesterday_average_temperature": [ - "Yesterday average temperature", - TEMP_CELSIUS, - "mdi:thermometer", - ], - "period_average_temperature": [ - "Period average temperature", - TEMP_CELSIUS, - "mdi:thermometer", - ], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_MONITORED_VARIABLES): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_CONTRACT): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - -HOST = "https://www.hydroquebec.com" -HOME_URL = f"{HOST}/portail/web/clientele/authentification" -PROFILE_URL = "{}/portail/fr/group/clientele/" "portrait-de-consommation".format(HOST) -MONTHLY_MAP = ( - ("period_total_bill", "montantFacturePeriode"), - ("period_length", "nbJourLecturePeriode"), - ("period_total_days", "nbJourPrevuPeriode"), - ("period_mean_daily_bill", "moyenneDollarsJourPeriode"), - ("period_mean_daily_consumption", "moyenneKwhJourPeriode"), - ("period_total_consumption", "consoTotalPeriode"), - ("period_lower_price_consumption", "consoRegPeriode"), - ("period_higher_price_consumption", "consoHautPeriode"), -) -DAILY_MAP = ( - ("yesterday_total_consumption", "consoTotalQuot"), - ("yesterday_lower_price_consumption", "consoRegQuot"), - ("yesterday_higher_price_consumption", "consoHautQuot"), -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the HydroQuebec sensor.""" - # Create a data fetcher to support all of the configured sensors. Then make - # the first call to init the data. - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - contract = config.get(CONF_CONTRACT) - - httpsession = hass.helpers.aiohttp_client.async_get_clientsession() - hydroquebec_data = HydroquebecData(username, password, httpsession, contract) - contracts = await hydroquebec_data.get_contract_list() - if not contracts: - return - _LOGGER.info("Contract list: %s", ", ".join(contracts)) - - name = config.get(CONF_NAME) - - sensors = [] - for variable in config[CONF_MONITORED_VARIABLES]: - sensors.append(HydroQuebecSensor(hydroquebec_data, variable, name)) - - async_add_entities(sensors, True) - - -class HydroQuebecSensor(Entity): - """Implementation of a HydroQuebec sensor.""" - - def __init__(self, hydroquebec_data, sensor_type, name): - """Initialize the sensor.""" - self.client_name = name - self.type = sensor_type - self._name = SENSOR_TYPES[sensor_type][0] - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] - self._icon = SENSOR_TYPES[sensor_type][2] - self.hydroquebec_data = hydroquebec_data - self._state = None - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self.client_name} {self._name}" - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return self._unit_of_measurement - - @property - def icon(self): - """Icon to use in the frontend, if any.""" - return self._icon - - async def async_update(self): - """Get the latest data from Hydroquebec and update the state.""" - await self.hydroquebec_data.async_update() - if self.hydroquebec_data.data.get(self.type) is not None: - self._state = round(self.hydroquebec_data.data[self.type], 2) - - -class HydroquebecData: - """Get data from HydroQuebec.""" - - def __init__(self, username, password, httpsession, contract=None): - """Initialize the data object.""" - from pyhydroquebec import HydroQuebecClient - - self.client = HydroQuebecClient( - username, password, REQUESTS_TIMEOUT, httpsession - ) - self._contract = contract - self.data = {} - - async def get_contract_list(self): - """Return the contract list.""" - # Fetch data - ret = await self._fetch_data() - if ret: - return self.client.get_contracts() - return [] - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def _fetch_data(self): - """Fetch latest data from HydroQuebec.""" - from pyhydroquebec.client import PyHydroQuebecError - - try: - await self.client.fetch_data() - except PyHydroQuebecError as exp: - _LOGGER.error("Error on receive last Hydroquebec data: %s", exp) - return False - return True - - async def async_update(self): - """Return the latest collected data from HydroQuebec.""" - await self._fetch_data() - self.data = self.client.get_data(self._contract)[self._contract] diff --git a/requirements_all.txt b/requirements_all.txt index 7e9d78afb46..8a215b6350b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1231,9 +1231,6 @@ pyhomematic==0.1.60 # homeassistant.components.homeworks pyhomeworks==0.0.6 -# homeassistant.components.hydroquebec -pyhydroquebec==2.2.2 - # homeassistant.components.ialarm pyialarm==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0e40702b9d9..1bc01f6e73e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -419,9 +419,6 @@ pyheos==0.6.0 # homeassistant.components.homematic pyhomematic==0.1.60 -# homeassistant.components.hydroquebec -pyhydroquebec==2.2.2 - # homeassistant.components.ipma pyipma==1.2.1 diff --git a/tests/components/hydroquebec/__init__.py b/tests/components/hydroquebec/__init__.py deleted file mode 100644 index 1342395d265..00000000000 --- a/tests/components/hydroquebec/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the hydroquebec component.""" diff --git a/tests/components/hydroquebec/test_sensor.py b/tests/components/hydroquebec/test_sensor.py deleted file mode 100644 index 9b2dd5ab5b5..00000000000 --- a/tests/components/hydroquebec/test_sensor.py +++ /dev/null @@ -1,102 +0,0 @@ -"""The test for the hydroquebec sensor platform.""" -import asyncio -import logging -import sys -from unittest.mock import MagicMock - -from homeassistant.bootstrap import async_setup_component -from homeassistant.components.hydroquebec import sensor as hydroquebec -from tests.common import assert_setup_component - - -CONTRACT = "123456789" - - -class HydroQuebecClientMock: - """Fake Hydroquebec client.""" - - def __init__(self, username, password, contract=None, httpsession=None): - """Fake Hydroquebec client init.""" - pass - - def get_data(self, contract): - """Return fake hydroquebec data.""" - return {CONTRACT: {"balance": 160.12}} - - def get_contracts(self): - """Return fake hydroquebec contracts.""" - return [CONTRACT] - - @asyncio.coroutine - def fetch_data(self): - """Return fake fetching data.""" - pass - - -class HydroQuebecClientMockError(HydroQuebecClientMock): - """Fake Hydroquebec client error.""" - - def get_contracts(self): - """Return fake hydroquebec contracts.""" - return [] - - @asyncio.coroutine - def fetch_data(self): - """Return fake fetching data.""" - raise PyHydroQuebecErrorMock("Fake Error") - - -class PyHydroQuebecErrorMock(BaseException): - """Fake PyHydroquebec Error.""" - - -class PyHydroQuebecClientFakeModule: - """Fake pyfido.client module.""" - - PyHydroQuebecError = PyHydroQuebecErrorMock - - -class PyHydroQuebecFakeModule: - """Fake pyfido module.""" - - HydroQuebecClient = HydroQuebecClientMockError - - -@asyncio.coroutine -def test_hydroquebec_sensor(loop, hass): - """Test the Hydroquebec number sensor.""" - sys.modules["pyhydroquebec"] = MagicMock() - sys.modules["pyhydroquebec.client"] = MagicMock() - sys.modules["pyhydroquebec.client.PyHydroQuebecError"] = PyHydroQuebecErrorMock - import pyhydroquebec.client - - pyhydroquebec.HydroQuebecClient = HydroQuebecClientMock - pyhydroquebec.client.PyHydroQuebecError = PyHydroQuebecErrorMock - config = { - "sensor": { - "platform": "hydroquebec", - "name": "hydro", - "contract": CONTRACT, - "username": "myusername", - "password": "password", - "monitored_variables": ["balance"], - } - } - with assert_setup_component(1): - yield from async_setup_component(hass, "sensor", config) - state = hass.states.get("sensor.hydro_balance") - assert state.state == "160.12" - assert state.attributes.get("unit_of_measurement") == "CAD" - - -@asyncio.coroutine -def test_error(hass, caplog): - """Test the Hydroquebec sensor errors.""" - caplog.set_level(logging.ERROR) - sys.modules["pyhydroquebec"] = PyHydroQuebecFakeModule() - sys.modules["pyhydroquebec.client"] = PyHydroQuebecClientFakeModule() - - config = {} - fake_async_add_entities = MagicMock() - yield from hydroquebec.async_setup_platform(hass, config, fake_async_add_entities) - assert fake_async_add_entities.called is False From 9e3005133a6e72e7895bdf38a64b0eab3512cdd6 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Thu, 10 Oct 2019 21:57:48 +0300 Subject: [PATCH 148/639] Standardize times in time sensors Jewish calendar (#26940) * Standardize times in time sensors Jewish calendar * Fix pylint errors * Add non-default time format test * Make black happy * Remove timestamp device class Timestamp device class requires ISO 8601 format * Revert "Remove timestamp device class" This reverts commit 8a2fda39831bc750c3a77aa774b84b054d78032c. * Remove time_format As this is part of the UI decision, it should be decided by lovelace. A nice addition for a future PR, might be the option to hint to lovelace the preferred way to display some data. * Update name of state_attributes * State of timestamp variable to be shown in UTC Although I don't understand it, I give up :) * Remove unnecessary attributes I don't really see the value in these attributes, if there are any they should be implemented in the sensor component for the timestamp device class --- .../components/jewish_calendar/sensor.py | 112 ++++++++++++------ .../components/jewish_calendar/test_sensor.py | 16 ++- 2 files changed, 87 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 405838b1fb1..453b3de4bae 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -23,7 +23,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for sensor, sensor_info in SENSOR_TYPES["data"].items() ] sensors.extend( - JewishCalendarSensor(hass.data[DOMAIN], sensor, sensor_info) + JewishCalendarTimeSensor(hass.data[DOMAIN], sensor, sensor_info) for sensor, sensor_info in SENSOR_TYPES["time"].items() ) @@ -63,7 +63,7 @@ class JewishCalendarSensor(Entity): async def async_update(self): """Update the state of the sensor.""" now = dt_util.now() - _LOGGER.debug("Now: %s Timezone = %s", now, now.tzinfo) + _LOGGER.debug("Now: %s Location: %r", now, self._location) today = now.date() sunset = dt_util.as_local( @@ -72,16 +72,6 @@ class JewishCalendarSensor(Entity): _LOGGER.debug("Now: %s Sunset: %s", now, sunset) - def make_zmanim(date): - """Create a Zmanim object.""" - return hdate.Zmanim( - date=date, - location=self._location, - candle_lighting_offset=self._candle_lighting_offset, - havdalah_offset=self._havdalah_offset, - hebrew=self._hebrew, - ) - date = hdate.HDate(today, diaspora=self._diaspora, hebrew=self._hebrew) # The Jewish day starts after darkness (called "tzais") and finishes at @@ -92,7 +82,7 @@ class JewishCalendarSensor(Entity): # tomorrow based on sunset ("shkia"), for others based on "tzais". # Hence the following variables. after_tzais_date = after_shkia_date = date - today_times = make_zmanim(today) + today_times = self.make_zmanim(today) if now > sunset: after_shkia_date = date.next_day @@ -100,37 +90,83 @@ class JewishCalendarSensor(Entity): if today_times.havdalah and now > today_times.havdalah: after_tzais_date = date.next_day + self._state = self.get_state(after_shkia_date, after_tzais_date) + _LOGGER.debug("New value for %s: %s", self._type, self._state) + + def make_zmanim(self, date): + """Create a Zmanim object.""" + return hdate.Zmanim( + date=date, + location=self._location, + candle_lighting_offset=self._candle_lighting_offset, + havdalah_offset=self._havdalah_offset, + hebrew=self._hebrew, + ) + + def get_state(self, after_shkia_date, after_tzais_date): + """For a given type of sensor, return the state.""" # Terminology note: by convention in py-libhdate library, "upcoming" # refers to "current" or "upcoming" dates. if self._type == "date": - self._state = after_shkia_date.hebrew_date - elif self._type == "weekly_portion": + return after_shkia_date.hebrew_date + if self._type == "weekly_portion": # Compute the weekly portion based on the upcoming shabbat. - self._state = after_tzais_date.upcoming_shabbat.parasha - elif self._type == "holiday_name": - self._state = after_shkia_date.holiday_description - elif self._type == "holiday_type": - self._state = after_shkia_date.holiday_type - elif self._type == "upcoming_shabbat_candle_lighting": - times = make_zmanim(after_tzais_date.upcoming_shabbat.previous_day.gdate) - self._state = times.candle_lighting - elif self._type == "upcoming_candle_lighting": - times = make_zmanim( + return after_tzais_date.upcoming_shabbat.parasha + if self._type == "holiday_name": + return after_shkia_date.holiday_description + if self._type == "holiday_type": + return after_shkia_date.holiday_type + if self._type == "omer_count": + return after_shkia_date.omer_day + + return None + + +class JewishCalendarTimeSensor(JewishCalendarSensor): + """Implement attrbutes for sensors returning times.""" + + @property + def state(self): + """Return the state of the sensor.""" + return dt_util.as_utc(self._state) if self._state is not None else None + + @property + def device_class(self): + """Return the class of this sensor.""" + return "timestamp" + + @property + def device_state_attributes(self): + """Return the state attributes.""" + attrs = {} + + if self._state is None: + return attrs + + attrs["timestamp"] = self._state.timestamp() + + return attrs + + def get_state(self, after_shkia_date, after_tzais_date): + """For a given type of sensor, return the state.""" + if self._type == "upcoming_shabbat_candle_lighting": + times = self.make_zmanim( + after_tzais_date.upcoming_shabbat.previous_day.gdate + ) + return times.candle_lighting + if self._type == "upcoming_candle_lighting": + times = self.make_zmanim( after_tzais_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate ) - self._state = times.candle_lighting - elif self._type == "upcoming_shabbat_havdalah": - times = make_zmanim(after_tzais_date.upcoming_shabbat.gdate) - self._state = times.havdalah - elif self._type == "upcoming_havdalah": - times = make_zmanim( + return times.candle_lighting + if self._type == "upcoming_shabbat_havdalah": + times = self.make_zmanim(after_tzais_date.upcoming_shabbat.gdate) + return times.havdalah + if self._type == "upcoming_havdalah": + times = self.make_zmanim( after_tzais_date.upcoming_shabbat_or_yom_tov.last_day.gdate ) - self._state = times.havdalah - elif self._type == "omer_count": - self._state = after_shkia_date.omer_day - else: - times = make_zmanim(today).zmanim - self._state = times[self._type].time() + return times.havdalah - _LOGGER.debug("New value: %s", self._state) + times = self.make_zmanim(dt_util.now()).zmanim + return times[self._type] diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 8d72830b369..94b26f80d2d 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -1,5 +1,5 @@ """The tests for the Jewish calendar sensors.""" -from datetime import time, timedelta +from datetime import timedelta from datetime import datetime as dt import pytest @@ -81,7 +81,7 @@ TEST_PARAMS = [ "hebrew", "t_set_hakochavim", True, - time(19, 48), + dt(2018, 9, 8, 19, 48), ), ( dt(2018, 9, 8), @@ -91,7 +91,7 @@ TEST_PARAMS = [ "hebrew", "t_set_hakochavim", False, - time(19, 21), + dt(2018, 9, 8, 19, 21), ), ( dt(2018, 10, 14), @@ -183,6 +183,10 @@ async def test_jewish_calendar_sensor( async_fire_time_changed(hass, future) await hass.async_block_till_done() + result = ( + dt_util.as_utc(time_zone.localize(result)) if isinstance(result, dt) else result + ) + assert hass.states.get(f"sensor.test_{sensor}").state == str(result) @@ -524,6 +528,12 @@ async def test_shabbat_times_sensor( sensor_type = sensor_type.replace(f"{language}_", "") + result_value = ( + dt_util.as_utc(result_value) + if isinstance(result_value, dt) + else result_value + ) + assert hass.states.get(f"sensor.test_{sensor_type}").state == str( result_value ), f"Value for {sensor_type}" From 2ab6eb4fa03a0ab698e4ec6cba8c6c5915a72b04 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Oct 2019 12:46:40 -0700 Subject: [PATCH 149/639] Revert "Allow Google Assistant relative volume control (#26585)" (#27416) This reverts commit 95c537bee88fd2db7507024a1e32eb52364facbb. --- .../components/google_assistant/trait.py | 37 ++++++------------- .../components/google_assistant/test_trait.py | 29 --------------- 2 files changed, 11 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 26c2e2ee002..7d6e79a8237 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1427,33 +1427,18 @@ class VolumeTrait(_Trait): async def _execute_volume_relative(self, data, params): # This could also support up/down commands using relativeSteps relative = params["volumeRelativeLevel"] + current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) - # if we have access to current volume level, do a single 'set' call - if media_player.ATTR_MEDIA_VOLUME_LEVEL in self.state.attributes: - current = self.state.attributes.get(media_player.ATTR_MEDIA_VOLUME_LEVEL) - - await self.hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_VOLUME_SET, - { - ATTR_ENTITY_ID: self.state.entity_id, - media_player.ATTR_MEDIA_VOLUME_LEVEL: current + relative / 100, - }, - blocking=True, - context=data.context, - ) - # otherwise do multiple 'up' or 'down' calls - else: - for _ in range(abs(relative)): - await self.hass.services.async_call( - media_player.DOMAIN, - media_player.SERVICE_VOLUME_UP - if relative > 0 - else media_player.SERVICE_VOLUME_DOWN, - {ATTR_ENTITY_ID: self.state.entity_id}, - blocking=True, - context=data.context, - ) + await self.hass.services.async_call( + media_player.DOMAIN, + media_player.SERVICE_VOLUME_SET, + { + ATTR_ENTITY_ID: self.state.entity_id, + media_player.ATTR_MEDIA_VOLUME_LEVEL: current + relative / 100, + }, + blocking=True, + context=data.context, + ) async def execute(self, command, data, params, challenge): """Execute a brightness command.""" diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index d58281b0e11..a5c527dacfe 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1599,35 +1599,6 @@ async def test_volume_media_player_relative(hass): } -async def test_volume_media_player_relative_no_vol_lvl(hass): - """Test volume trait support for media player domain.""" - trt = trait.VolumeTrait( - hass, State("media_player.bla", media_player.STATE_PLAYING, {}), BASIC_CONFIG - ) - - assert trt.sync_attributes() == {} - - assert trt.query_attributes() == {} - - up_calls = async_mock_service( - hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_UP - ) - - await trt.execute( - trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"volumeRelativeLevel": 2}, {} - ) - assert len(up_calls) == 2 - - down_calls = async_mock_service( - hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_DOWN - ) - - await trt.execute( - trait.COMMAND_VOLUME_RELATIVE, BASIC_DATA, {"volumeRelativeLevel": -2}, {} - ) - assert len(down_calls) == 2 - - async def test_temperature_setting_sensor(hass): """Test TemperatureSetting trait support for temperature sensor.""" assert ( From 4f4bbedc581fc7d538e1e1f05e5af968f67cab74 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 10 Oct 2019 22:52:29 +0300 Subject: [PATCH 150/639] bump songpal to fix attrs usage when using its most recent version (#27410) --- homeassistant/components/songpal/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/songpal/manifest.json b/homeassistant/components/songpal/manifest.json index 2f0c44da47b..55b02b66a59 100644 --- a/homeassistant/components/songpal/manifest.json +++ b/homeassistant/components/songpal/manifest.json @@ -3,7 +3,7 @@ "name": "Songpal", "documentation": "https://www.home-assistant.io/integrations/songpal", "requirements": [ - "python-songpal==0.11" + "python-songpal==0.11.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8a215b6350b..bdb130ac9aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1552,7 +1552,7 @@ python-ripple-api==0.0.3 python-sochain-api==0.0.2 # homeassistant.components.songpal -python-songpal==0.11 +python-songpal==0.11.1 # homeassistant.components.synologydsm python-synology==0.2.0 From aecf7e65ff11e97f0762f54613ac42b17a7c935f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Oct 2019 21:52:40 +0200 Subject: [PATCH 151/639] Bump aiohttp to 3.6.2 (#27409) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fb6239c8070..4bdad0b62f3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiohttp==3.6.1 +aiohttp==3.6.2 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index bdb130ac9aa..d58d61c0188 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.6.1 +aiohttp==3.6.2 astral==1.10.1 async_timeout==3.0.1 attrs==19.2.0 diff --git a/setup.py b/setup.py index f4e94faf553..33d4ede2551 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ PROJECT_URLS = { PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.6.1", + "aiohttp==3.6.2", "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.2.0", From ed3516186bead77af07391dd2220c47fa002c0ab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 10 Oct 2019 21:52:54 +0200 Subject: [PATCH 152/639] Bump sqlalchemy to 1.3.10 (#27408) --- homeassistant/components/recorder/manifest.json | 4 ++-- homeassistant/components/sql/manifest.json | 4 ++-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index d349560e385..1f00cf89f15 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -3,8 +3,8 @@ "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", "requirements": [ - "sqlalchemy==1.3.9" + "sqlalchemy==1.3.10" ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 5a87c813bc5..fa641adc839 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -3,10 +3,10 @@ "name": "Sql", "documentation": "https://www.home-assistant.io/integrations/sql", "requirements": [ - "sqlalchemy==1.3.9" + "sqlalchemy==1.3.10" ], "dependencies": [], "codeowners": [ "@dgomes" ] -} +} \ No newline at end of file diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4bdad0b62f3..f74bdac2337 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -21,7 +21,7 @@ pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.9 +sqlalchemy==1.3.10 voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.23.0 diff --git a/requirements_all.txt b/requirements_all.txt index d58d61c0188..2a0987c596b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1814,7 +1814,7 @@ spotipy-homeassistant==2.4.4.dev1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.9 +sqlalchemy==1.3.10 # homeassistant.components.starlingbank starlingbank==3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1bc01f6e73e..84cef21a1fe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -564,7 +564,7 @@ somecomfort==0.5.2 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.9 +sqlalchemy==1.3.10 # homeassistant.components.statsd statsd==3.2.1 From 4b8a35dffb003e593480a0213252794afc155407 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Thu, 10 Oct 2019 22:53:05 +0300 Subject: [PATCH 153/639] move songpal imports to top (#27402) * move songpal imports to top * Update media_player.py --- homeassistant/components/songpal/media_player.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/songpal/media_player.py b/homeassistant/components/songpal/media_player.py index 9f5ae9d1ac6..0567cd0ea6a 100644 --- a/homeassistant/components/songpal/media_player.py +++ b/homeassistant/components/songpal/media_player.py @@ -4,6 +4,14 @@ import logging from collections import OrderedDict import voluptuous as vol +from songpal import ( + Device, + SongpalException, + VolumeChange, + ContentChange, + PowerChange, + ConnectChange, +) from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -60,8 +68,6 @@ SET_SOUND_SCHEMA = vol.Schema( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Songpal platform.""" - from songpal import SongpalException - if PLATFORM not in hass.data: hass.data[PLATFORM] = {} @@ -117,8 +123,6 @@ class SongpalDevice(MediaPlayerDevice): def __init__(self, name, endpoint, poll=False): """Init.""" - from songpal import Device - self._name = name self._endpoint = endpoint self._poll = poll @@ -151,7 +155,6 @@ class SongpalDevice(MediaPlayerDevice): async def async_activate_websocket(self): """Activate websocket for listening if wanted.""" _LOGGER.info("Activating websocket connection..") - from songpal import VolumeChange, ContentChange, PowerChange, ConnectChange async def _volume_changed(volume: VolumeChange): _LOGGER.debug("Volume changed: %s", volume) @@ -230,8 +233,6 @@ class SongpalDevice(MediaPlayerDevice): async def async_update(self): """Fetch updates from the device.""" - from songpal import SongpalException - try: volumes = await self.dev.get_volume_information() if not volumes: From 4c71c6df6f584bf220d1f9051ba7df55317c46b1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 11 Oct 2019 00:31:40 +0000 Subject: [PATCH 154/639] [ci skip] Translation update --- .../components/airly/.translations/fr.json | 1 + .../binary_sensor/.translations/da.json | 2 ++ .../binary_sensor/.translations/fr.json | 22 +++++++++++++++++++ .../binary_sensor/.translations/it.json | 2 ++ .../components/ecobee/.translations/fr.json | 1 + .../opentherm_gw/.translations/fr.json | 1 + .../components/plex/.translations/fr.json | 1 + .../components/zha/.translations/fr.json | 19 ++++++++++++++++ 8 files changed, 49 insertions(+) diff --git a/homeassistant/components/airly/.translations/fr.json b/homeassistant/components/airly/.translations/fr.json index cf756a9f492..374e578eed2 100644 --- a/homeassistant/components/airly/.translations/fr.json +++ b/homeassistant/components/airly/.translations/fr.json @@ -13,6 +13,7 @@ "longitude": "Longitude", "name": "Nom de l'int\u00e9gration" }, + "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air Airly. Pour g\u00e9n\u00e9rer une cl\u00e9 API, rendez-vous sur https://developer.airly.eu/register.", "title": "Airly" } }, diff --git a/homeassistant/components/binary_sensor/.translations/da.json b/homeassistant/components/binary_sensor/.translations/da.json index 56822c2365c..f7bd834561c 100644 --- a/homeassistant/components/binary_sensor/.translations/da.json +++ b/homeassistant/components/binary_sensor/.translations/da.json @@ -39,6 +39,7 @@ "closed": "{entity_name} lukket", "cold": "{entity_name} blev kold", "connected": "{entity_name} tilsluttet", + "moist": "{entity_name} blev fugtig", "moist\u00a7": "{entity_name} blev fugtig", "motion": "{entity_name} begyndte at registrere bev\u00e6gelse", "moving": "{entity_name} begyndte at bev\u00e6ge sig", @@ -53,6 +54,7 @@ "not_hot": "{entity_name} blev ikke varm", "not_locked": "{entity_name} l\u00e5st op", "not_moist": "{entity_name} blev t\u00f8r", + "not_opened": "{entity_name} lukket", "not_present": "{entity_name} ikke til stede", "not_unsafe": "{entity_name} blev sikker", "occupied": "{entity_name} blev optaget", diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json index 1a11bfa4bc2..9a04ea17747 100644 --- a/homeassistant/components/binary_sensor/.translations/fr.json +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -50,7 +50,29 @@ "cold": "{entity_name} est devenu froid", "connected": "{entity_name} connect\u00e9", "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz", + "hot": "{entity_name} est devenu chaud", + "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter la lumi\u00e8re", + "locked": "{entity_name} verrouill\u00e9", + "moist": "{entity_name} est devenu humide", + "moist\u00a7": "{entity_name} est devenu humide", + "motion": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du mouvement", + "moving": "{entity_name} a commenc\u00e9 \u00e0 se d\u00e9placer", + "no_gas": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le gaz", + "no_light": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter la lumi\u00e8re", + "no_motion": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le mouvement", + "no_problem": "{entity_name} a cess\u00e9 de d\u00e9tecter un probl\u00e8me", + "no_smoke": "{entity_name} a cess\u00e9 de d\u00e9tecter de la fum\u00e9e", + "no_sound": "{entity_name} a cess\u00e9 de d\u00e9tecter du bruit", + "no_vibration": "{entity_name} a cess\u00e9 de d\u00e9tecter des vibrations", + "not_bat_low": "{entity_name} batterie normale", + "not_cold": "{entity_name} n'est plus froid", + "not_connected": "{entity_name} d\u00e9connect\u00e9", + "not_hot": "{entity_name} n'est plus chaud", + "not_locked": "{entity_name} d\u00e9verrouill\u00e9", + "not_moist": "{entity_name} est devenu sec", + "not_moving": "{entity_name} a cess\u00e9 de bouger", "not_occupied": "{entity_name} est devenu non occup\u00e9", + "not_opened": "{entity_name} ferm\u00e9", "not_plugged_in": "{entity_name} d\u00e9branch\u00e9", "not_powered": "{entity_name} non aliment\u00e9", "not_present": "{entity_name} non pr\u00e9sent", diff --git a/homeassistant/components/binary_sensor/.translations/it.json b/homeassistant/components/binary_sensor/.translations/it.json index 0583a4d4f74..c69f5a07a41 100644 --- a/homeassistant/components/binary_sensor/.translations/it.json +++ b/homeassistant/components/binary_sensor/.translations/it.json @@ -53,6 +53,7 @@ "hot": "{entity_name} \u00e8 diventato caldo", "light": "{entity_name} ha iniziato a rilevare la luce", "locked": "{entity_name} bloccato", + "moist": "{entity_name} diventato umido", "moist\u00a7": "{entity_name} \u00e8 diventato umido", "motion": "{entity_name} ha iniziato a rilevare il movimento", "moving": "{entity_name} ha iniziato a muoversi", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} \u00e8 diventato asciutto", "not_moving": "{entity_name} ha smesso di muoversi", "not_occupied": "{entity_name} non \u00e8 occupato", + "not_opened": "{entity_name} chiuso", "not_plugged_in": "{entity_name} \u00e8 scollegato", "not_powered": "{entity_name} non \u00e8 alimentato", "not_present": "{entity_name} non \u00e8 presente", diff --git a/homeassistant/components/ecobee/.translations/fr.json b/homeassistant/components/ecobee/.translations/fr.json index 85da5b3a4ec..7f308fdf3a3 100644 --- a/homeassistant/components/ecobee/.translations/fr.json +++ b/homeassistant/components/ecobee/.translations/fr.json @@ -9,6 +9,7 @@ }, "step": { "authorize": { + "description": "Veuillez autoriser cette application \u00e0 https://www.ecobee.com/consumerportal/index.html avec un code PIN :\n\n{pin}\n\nEnsuite, appuyez sur Soumettre.", "title": "Autoriser l'application sur ecobee.com" }, "user": { diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json index 82b9a7aee88..f5f25da48bd 100644 --- a/homeassistant/components/opentherm_gw/.translations/fr.json +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -10,6 +10,7 @@ "init": { "data": { "device": "Chemin ou URL", + "floor_temperature": "Temp\u00e9rature du sol", "id": "ID", "name": "Nom", "precision": "Pr\u00e9cision de la temp\u00e9rature climatique" diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 3854b19b5d2..c06d314ec72 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -34,6 +34,7 @@ "title": "S\u00e9lectionnez le serveur Plex" }, "start_website_auth": { + "description": "Continuer d'autoriser sur plex.tv.", "title": "Connecter un serveur Plex" }, "user": { diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index d7b0a783116..b4adac8e997 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -31,6 +31,15 @@ "button_5": "Cinqui\u00e8me bouton", "button_6": "Sixi\u00e8me bouton", "close": "Fermer", + "dim_down": "Assombrir", + "dim_up": "\u00c9claircir", + "face_1": "avec face 1 activ\u00e9e", + "face_2": "avec face 2 activ\u00e9e", + "face_3": "avec face 3 activ\u00e9e", + "face_4": "avec face 4 activ\u00e9e", + "face_5": "avec face 5 activ\u00e9e", + "face_6": "avec face 6 activ\u00e9e", + "face_any": "Avec n'importe quelle face / face sp\u00e9cifi\u00e9e(s) activ\u00e9e", "left": "Gauche", "open": "Ouvert", "right": "Droite", @@ -39,8 +48,18 @@ }, "trigger_type": { "device_dropped": "Appareil tomb\u00e9", + "device_flipped": "Appareil retourn\u00e9 \"{subtype}\"", + "device_knocked": "Appareil frapp\u00e9 \"{subtype}\"", + "device_rotated": "Appareil tourn\u00e9 \"{subtype}\"", "device_shaken": "Appareil secou\u00e9", + "device_slid": "Appareil gliss\u00e9 \"{subtype}\"", "device_tilted": "Dispositif inclin\u00e9", + "remote_button_double_press": "Bouton \"{subtype}\" cliqu\u00e9", + "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", + "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", + "remote_button_quadruple_press": "bouton \" {subtype} \" quadruple clic", + "remote_button_quintuple_press": "bouton \" {subtype} \" quintuple clic", + "remote_button_short_press": "bouton \" {subtype} \" enfonc\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", "remote_button_triple_press": "Bouton\"{sous-type}\" \u00e0 trois clics" } From 0c8e208fd850c1b13e60c407f5d999dc7dca174b Mon Sep 17 00:00:00 2001 From: quthla Date: Fri, 11 Oct 2019 09:27:07 +0200 Subject: [PATCH 155/639] Bump python-slugify to 3.0.6 (#27430) * Bump python-slugify to 3.0.6 * Bump python-slugify to 3.0.6 * Bump python-slugify to 3.0.6 --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f74bdac2337..2259f3053fe 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -16,7 +16,7 @@ importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 pip>=8.0.3 -python-slugify==3.0.4 +python-slugify==3.0.6 pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2a0987c596b..56616498662 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -11,7 +11,7 @@ jinja2>=2.10.1 PyJWT==1.7.1 cryptography==2.7 pip>=8.0.3 -python-slugify==3.0.4 +python-slugify==3.0.6 pytz>=2019.03 pyyaml==5.1.2 requests==2.22.0 diff --git a/setup.py b/setup.py index 33d4ede2551..5b9988cff27 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ REQUIRES = [ # PyJWT has loose dependency. We want the latest one. "cryptography==2.7", "pip>=8.0.3", - "python-slugify==3.0.4", + "python-slugify==3.0.6", "pytz>=2019.03", "pyyaml==5.1.2", "requests==2.22.0", From 8bbf26130246a3532e5634eda4a6a38ef46a02c1 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 11 Oct 2019 16:36:46 +0200 Subject: [PATCH 156/639] Refactor home --> hap for Homematic IP Cloud (#27368) * Refactor home to hap for Homematic IP Cloud * Add some tests * Rename ha_entity --> ha_state * use asynctest.Mock --- .../homematicip_cloud/alarm_control_panel.py | 12 +- .../homematicip_cloud/binary_sensor.py | 58 ++-- .../components/homematicip_cloud/climate.py | 12 +- .../components/homematicip_cloud/cover.py | 8 +- .../components/homematicip_cloud/device.py | 13 +- .../components/homematicip_cloud/hap.py | 1 + .../components/homematicip_cloud/light.py | 30 +- .../components/homematicip_cloud/sensor.py | 62 ++-- .../components/homematicip_cloud/switch.py | 32 +- .../components/homematicip_cloud/weather.py | 22 +- .../components/homematicip_cloud/conftest.py | 31 +- tests/components/homematicip_cloud/helper.py | 88 +++--- .../homematicip_cloud/test_binary_sensor.py | 289 ++++++++++++++++++ .../homematicip_cloud/test_binary_sensors.py | 41 --- .../homematicip_cloud/test_light.py | 196 ++++++++++++ .../homematicip_cloud/test_lights.py | 77 ----- tests/fixtures/homematicip_cloud.json | 189 ++++++++++++ 17 files changed, 872 insertions(+), 289 deletions(-) create mode 100644 tests/components/homematicip_cloud/test_binary_sensor.py delete mode 100644 tests/components/homematicip_cloud/test_binary_sensors.py create mode 100644 tests/components/homematicip_cloud/test_light.py delete mode 100644 tests/components/homematicip_cloud/test_lights.py diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 592d234225c..bb5999108ce 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -2,7 +2,6 @@ import logging from homematicip.aio.group import AsyncSecurityZoneGroup -from homematicip.aio.home import AsyncHome from homematicip.base.enums import WindowState from homeassistant.components.alarm_control_panel import AlarmControlPanel @@ -16,6 +15,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -31,15 +31,15 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] security_zones = [] - for group in home.groups: + for group in hap.home.groups: if isinstance(group, AsyncSecurityZoneGroup): security_zones.append(group) if security_zones: - devices.append(HomematicipAlarmControlPanel(home, security_zones)) + devices.append(HomematicipAlarmControlPanel(hap, security_zones)) if devices: async_add_entities(devices) @@ -48,9 +48,9 @@ async def async_setup_entry( class HomematicipAlarmControlPanel(AlarmControlPanel): """Representation of an alarm control panel.""" - def __init__(self, home: AsyncHome, security_zones) -> None: + def __init__(self, hap: HomematicipHAP, security_zones) -> None: """Initialize the alarm control panel.""" - self._home = home + self._home = hap.home self.alarm_state = STATE_ALARM_DISARMED for security_zone in security_zones: diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 1114f10b622..964ab4d8234 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -20,7 +20,6 @@ from homematicip.aio.device import ( AsyncWeatherSensorPro, ) from homematicip.aio.group import AsyncSecurityGroup, AsyncSecurityZoneGroup -from homematicip.aio.home import AsyncHome from homematicip.base.enums import SmokeDetectorAlarmType, WindowState from homeassistant.components.binary_sensor import ( @@ -41,6 +40,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -85,18 +85,18 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncAccelerationSensor): - devices.append(HomematicipAccelerationSensor(home, device)) + devices.append(HomematicipAccelerationSensor(hap, device)) if isinstance(device, (AsyncContactInterface, AsyncFullFlushContactInterface)): - devices.append(HomematicipContactInterface(home, device)) + devices.append(HomematicipContactInterface(hap, device)) if isinstance( device, (AsyncShutterContact, AsyncShutterContactMagnetic, AsyncRotaryHandleSensor), ): - devices.append(HomematicipShutterContact(home, device)) + devices.append(HomematicipShutterContact(hap, device)) if isinstance( device, ( @@ -105,28 +105,28 @@ async def async_setup_entry( AsyncMotionDetectorPushButton, ), ): - devices.append(HomematicipMotionDetector(home, device)) + devices.append(HomematicipMotionDetector(hap, device)) if isinstance(device, AsyncPresenceDetectorIndoor): - devices.append(HomematicipPresenceDetector(home, device)) + devices.append(HomematicipPresenceDetector(hap, device)) if isinstance(device, AsyncSmokeDetector): - devices.append(HomematicipSmokeDetector(home, device)) + devices.append(HomematicipSmokeDetector(hap, device)) if isinstance(device, AsyncWaterSensor): - devices.append(HomematicipWaterDetector(home, device)) + devices.append(HomematicipWaterDetector(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipRainSensor(home, device)) + devices.append(HomematicipRainSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipStormSensor(home, device)) - devices.append(HomematicipSunshineSensor(home, device)) + devices.append(HomematicipStormSensor(hap, device)) + devices.append(HomematicipSunshineSensor(hap, device)) if isinstance(device, AsyncDevice) and device.lowBat is not None: - devices.append(HomematicipBatterySensor(home, device)) + devices.append(HomematicipBatterySensor(hap, device)) - for group in home.groups: + for group in hap.home.groups: if isinstance(group, AsyncSecurityGroup): - devices.append(HomematicipSecuritySensorGroup(home, group)) + devices.append(HomematicipSecuritySensorGroup(hap, group)) elif isinstance(group, AsyncSecurityZoneGroup): - devices.append(HomematicipSecurityZoneSensorGroup(home, group)) + devices.append(HomematicipSecurityZoneSensorGroup(hap, group)) if devices: async_add_entities(devices) @@ -249,9 +249,9 @@ class HomematicipWaterDetector(HomematicipGenericDevice, BinarySensorDevice): class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud storm sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize storm sensor.""" - super().__init__(home, device, "Storm") + super().__init__(hap, device, "Storm") @property def icon(self) -> str: @@ -267,9 +267,9 @@ class HomematicipStormSensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud rain sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize rain sensor.""" - super().__init__(home, device, "Raining") + super().__init__(hap, device, "Raining") @property def device_class(self) -> str: @@ -285,9 +285,9 @@ class HomematicipRainSensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud sunshine sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize sunshine sensor.""" - super().__init__(home, device, "Sunshine") + super().__init__(hap, device, "Sunshine") @property def device_class(self) -> str: @@ -314,9 +314,9 @@ class HomematicipSunshineSensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud low battery sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize battery sensor.""" - super().__init__(home, device, "Battery") + super().__init__(hap, device, "Battery") @property def device_class(self) -> str: @@ -332,10 +332,10 @@ class HomematicipBatterySensor(HomematicipGenericDevice, BinarySensorDevice): class HomematicipSecurityZoneSensorGroup(HomematicipGenericDevice, BinarySensorDevice): """Representation of a HomematicIP Cloud security zone group.""" - def __init__(self, home: AsyncHome, device, post: str = "SecurityZone") -> None: + def __init__(self, hap: HomematicipHAP, device, post: str = "SecurityZone") -> None: """Initialize security zone group.""" device.modelType = f"HmIP-{post}" - super().__init__(home, device, post) + super().__init__(hap, device, post) @property def device_class(self) -> str: @@ -389,9 +389,9 @@ class HomematicipSecuritySensorGroup( ): """Representation of a HomematicIP security group.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize security group.""" - super().__init__(home, device, "Sensors") + super().__init__(hap, device, "Sensors") @property def device_state_attributes(self): diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 794a8b44cbc..cf1c1baabe0 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -4,7 +4,6 @@ from typing import Awaitable from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup -from homematicip.aio.home import AsyncHome from homematicip.base.enums import AbsenceType from homematicip.functionalHomes import IndoorClimateHome @@ -24,6 +23,7 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -41,11 +41,11 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP climate from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.groups: + for device in hap.home.groups: if isinstance(device, AsyncHeatingGroup): - devices.append(HomematicipHeatingGroup(home, device)) + devices.append(HomematicipHeatingGroup(hap, device)) if devices: async_add_entities(devices) @@ -54,13 +54,13 @@ async def async_setup_entry( class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """Representation of a HomematicIP heating group.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize heating group.""" device.modelType = "Group-Heating" self._simple_heating = None if device.actualTemperature is None: self._simple_heating = _get_first_heating_thermostat(device) - super().__init__(home, device) + super().__init__(hap, device) @property def temperature_unit(self) -> str: diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index 9252c4322d9..c5821c4f75e 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -31,13 +31,13 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP cover from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncFullFlushBlind): - devices.append(HomematicipCoverSlats(home, device)) + devices.append(HomematicipCoverSlats(hap, device)) elif isinstance(device, AsyncFullFlushShutter): - devices.append(HomematicipCoverShutter(home, device)) + devices.append(HomematicipCoverShutter(hap, device)) if devices: async_add_entities(devices) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 0389e0b9935..3d64014883d 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -4,17 +4,17 @@ from typing import Optional from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup -from homematicip.aio.home import AsyncHome from homeassistant.components import homematicip_cloud from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import Entity +from .hap import HomematicipHAP + _LOGGER = logging.getLogger(__name__) ATTR_MODEL_TYPE = "model_type" -ATTR_GROUP_ID = "group_id" ATTR_ID = "id" ATTR_IS_GROUP = "is_group" # RSSI HAP -> Device @@ -46,15 +46,16 @@ DEVICE_ATTRIBUTES = { "id": ATTR_ID, } -GROUP_ATTRIBUTES = {"modelType": ATTR_MODEL_TYPE, "id": ATTR_GROUP_ID} +GROUP_ATTRIBUTES = {"modelType": ATTR_MODEL_TYPE} class HomematicipGenericDevice(Entity): """Representation of an HomematicIP generic device.""" - def __init__(self, home: AsyncHome, device, post: Optional[str] = None) -> None: + def __init__(self, hap: HomematicipHAP, device, post: Optional[str] = None) -> None: """Initialize the generic device.""" - self._home = home + self._hap = hap + self._home = hap.home self._device = device self.post = post # Marker showing that the HmIP device hase been removed. @@ -81,6 +82,7 @@ class HomematicipGenericDevice(Entity): async def async_added_to_hass(self): """Register callbacks.""" + self._hap.hmip_device_by_entity_id[self.entity_id] = self._device self._device.on_update(self._async_device_changed) self._device.on_remove(self._async_device_removed) @@ -104,6 +106,7 @@ class HomematicipGenericDevice(Entity): # Only go further if the device/entity should be removed from registries # due to a removal of the HmIP device. if self.hmip_device_removed: + del self._hap.hmip_device_by_entity_id[self.entity_id] await self.async_remove_from_registries() async def async_remove_from_registries(self) -> None: diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index abba183d339..22ab1fd617c 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -79,6 +79,7 @@ class HomematicipHAP: self._retry_task = None self._tries = 0 self._accesspoint_connected = True + self.hmip_device_by_entity_id = {} async def async_setup(self, tries: int = 0): """Initialize connection.""" diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 42ff6d30478..80ee4cc5743 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -9,7 +9,6 @@ from homematicip.aio.device import ( AsyncFullFlushDimmer, AsyncPluggableDimmer, ) -from homematicip.aio.home import AsyncHome from homematicip.base.enums import RGBColorState from homematicip.base.functionalChannels import NotificationLightChannel @@ -25,6 +24,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -41,26 +41,26 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): - devices.append(HomematicipLightMeasuring(home, device)) + devices.append(HomematicipLightMeasuring(hap, device)) elif isinstance(device, AsyncBrandSwitchNotificationLight): - devices.append(HomematicipLight(home, device)) + devices.append(HomematicipLight(hap, device)) devices.append( - HomematicipNotificationLight(home, device, device.topLightChannelIndex) + HomematicipNotificationLight(hap, device, device.topLightChannelIndex) ) devices.append( HomematicipNotificationLight( - home, device, device.bottomLightChannelIndex + hap, device, device.bottomLightChannelIndex ) ) elif isinstance( device, (AsyncDimmer, AsyncPluggableDimmer, AsyncBrandDimmer, AsyncFullFlushDimmer), ): - devices.append(HomematicipDimmer(home, device)) + devices.append(HomematicipDimmer(hap, device)) if devices: async_add_entities(devices) @@ -69,9 +69,9 @@ async def async_setup_entry( class HomematicipLight(HomematicipGenericDevice, Light): """Representation of a HomematicIP Cloud light device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the light device.""" - super().__init__(home, device) + super().__init__(hap, device) @property def is_on(self) -> bool: @@ -107,9 +107,9 @@ class HomematicipLightMeasuring(HomematicipLight): class HomematicipDimmer(HomematicipGenericDevice, Light): """Representation of HomematicIP Cloud dimmer light device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the dimmer light device.""" - super().__init__(home, device) + super().__init__(hap, device) @property def is_on(self) -> bool: @@ -143,13 +143,13 @@ class HomematicipDimmer(HomematicipGenericDevice, Light): class HomematicipNotificationLight(HomematicipGenericDevice, Light): """Representation of HomematicIP Cloud dimmer light device.""" - def __init__(self, home: AsyncHome, device, channel: int) -> None: + def __init__(self, hap: HomematicipHAP, device, channel: int) -> None: """Initialize the dimmer light device.""" self.channel = channel if self.channel == 2: - super().__init__(home, device, "Top") + super().__init__(hap, device, "Top") else: - super().__init__(home, device, "Bottom") + super().__init__(hap, device, "Bottom") self._color_switcher = { RGBColorState.WHITE: [0.0, 0.0], diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index ceb7fc39fd7..18d483f6adf 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -20,7 +20,6 @@ from homematicip.aio.device import ( AsyncWeatherSensorPlus, AsyncWeatherSensorPro, ) -from homematicip.aio.home import AsyncHome from homematicip.base.enums import ValveState from homeassistant.config_entries import ConfigEntry @@ -36,6 +35,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -55,12 +55,12 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home - devices = [HomematicipAccesspointStatus(home)] - for device in home.devices: + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] + devices = [HomematicipAccesspointStatus(hap)] + for device in hap.home.devices: if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): - devices.append(HomematicipHeatingThermostat(home, device)) - devices.append(HomematicipTemperatureSensor(home, device)) + devices.append(HomematicipHeatingThermostat(hap, device)) + devices.append(HomematicipTemperatureSensor(hap, device)) if isinstance( device, ( @@ -72,8 +72,8 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipTemperatureSensor(home, device)) - devices.append(HomematicipHumiditySensor(home, device)) + devices.append(HomematicipTemperatureSensor(hap, device)) + devices.append(HomematicipHumiditySensor(hap, device)) if isinstance( device, ( @@ -87,7 +87,7 @@ async def async_setup_entry( AsyncWeatherSensorPro, ), ): - devices.append(HomematicipIlluminanceSensor(home, device)) + devices.append(HomematicipIlluminanceSensor(hap, device)) if isinstance( device, ( @@ -96,15 +96,15 @@ async def async_setup_entry( AsyncFullFlushSwitchMeasuring, ), ): - devices.append(HomematicipPowerSensor(home, device)) + devices.append(HomematicipPowerSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) ): - devices.append(HomematicipWindspeedSensor(home, device)) + devices.append(HomematicipWindspeedSensor(hap, device)) if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): - devices.append(HomematicipTodayRainSensor(home, device)) + devices.append(HomematicipTodayRainSensor(hap, device)) if isinstance(device, AsyncPassageDetector): - devices.append(HomematicipPassageDetectorDeltaCounter(home, device)) + devices.append(HomematicipPassageDetectorDeltaCounter(hap, device)) if devices: async_add_entities(devices) @@ -113,9 +113,9 @@ async def async_setup_entry( class HomematicipAccesspointStatus(HomematicipGenericDevice): """Representation of an HomeMaticIP Cloud access point.""" - def __init__(self, home: AsyncHome) -> None: + def __init__(self, hap: HomematicipHAP) -> None: """Initialize access point device.""" - super().__init__(home, home) + super().__init__(hap, hap.home) @property def device_info(self): @@ -162,9 +162,9 @@ class HomematicipAccesspointStatus(HomematicipGenericDevice): class HomematicipHeatingThermostat(HomematicipGenericDevice): """Representation of a HomematicIP heating thermostat device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize heating thermostat device.""" - super().__init__(home, device, "Heating") + super().__init__(hap, device, "Heating") @property def icon(self) -> str: @@ -191,9 +191,9 @@ class HomematicipHeatingThermostat(HomematicipGenericDevice): class HomematicipHumiditySensor(HomematicipGenericDevice): """Representation of a HomematicIP Cloud humidity device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" - super().__init__(home, device, "Humidity") + super().__init__(hap, device, "Humidity") @property def device_class(self) -> str: @@ -214,9 +214,9 @@ class HomematicipHumiditySensor(HomematicipGenericDevice): class HomematicipTemperatureSensor(HomematicipGenericDevice): """Representation of a HomematicIP Cloud thermometer device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the thermometer device.""" - super().__init__(home, device, "Temperature") + super().__init__(hap, device, "Temperature") @property def device_class(self) -> str: @@ -251,9 +251,9 @@ class HomematicipTemperatureSensor(HomematicipGenericDevice): class HomematicipIlluminanceSensor(HomematicipGenericDevice): """Representation of a HomematicIP Illuminance device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Illuminance") + super().__init__(hap, device, "Illuminance") @property def device_class(self) -> str: @@ -277,9 +277,9 @@ class HomematicipIlluminanceSensor(HomematicipGenericDevice): class HomematicipPowerSensor(HomematicipGenericDevice): """Representation of a HomematicIP power measuring device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Power") + super().__init__(hap, device, "Power") @property def device_class(self) -> str: @@ -300,9 +300,9 @@ class HomematicipPowerSensor(HomematicipGenericDevice): class HomematicipWindspeedSensor(HomematicipGenericDevice): """Representation of a HomematicIP wind speed sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Windspeed") + super().__init__(hap, device, "Windspeed") @property def state(self) -> float: @@ -333,9 +333,9 @@ class HomematicipWindspeedSensor(HomematicipGenericDevice): class HomematicipTodayRainSensor(HomematicipGenericDevice): """Representation of a HomematicIP rain counter of a day sensor.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the device.""" - super().__init__(home, device, "Today Rain") + super().__init__(hap, device, "Today Rain") @property def state(self) -> float: @@ -351,10 +351,6 @@ class HomematicipTodayRainSensor(HomematicipGenericDevice): class HomematicipPassageDetectorDeltaCounter(HomematicipGenericDevice): """Representation of a HomematicIP passage detector delta counter.""" - def __init__(self, home: AsyncHome, device) -> None: - """Initialize the device.""" - super().__init__(home, device) - @property def state(self) -> int: """Representation of the HomematicIP passage detector delta counter value.""" diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 7994aa446b8..3b54d3fc279 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -12,7 +12,6 @@ from homematicip.aio.device import ( AsyncPrintedCircuitBoardSwitchBattery, ) from homematicip.aio.group import AsyncSwitchingGroup -from homematicip.aio.home import AsyncHome from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry @@ -20,6 +19,7 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_GROUP_MEMBER_UNREACHABLE +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -33,9 +33,9 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP switch from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncBrandSwitchMeasuring): # BrandSwitchMeasuring inherits PlugableSwitchMeasuring # This device is implemented in the light platform and will @@ -44,24 +44,24 @@ async def async_setup_entry( elif isinstance( device, (AsyncPlugableSwitchMeasuring, AsyncFullFlushSwitchMeasuring) ): - devices.append(HomematicipSwitchMeasuring(home, device)) + devices.append(HomematicipSwitchMeasuring(hap, device)) elif isinstance( device, (AsyncPlugableSwitch, AsyncPrintedCircuitBoardSwitchBattery) ): - devices.append(HomematicipSwitch(home, device)) + devices.append(HomematicipSwitch(hap, device)) elif isinstance(device, AsyncOpenCollector8Module): for channel in range(1, 9): - devices.append(HomematicipMultiSwitch(home, device, channel)) + devices.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncMultiIOBox): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(home, device, channel)) + devices.append(HomematicipMultiSwitch(hap, device, channel)) elif isinstance(device, AsyncPrintedCircuitBoardSwitch2): for channel in range(1, 3): - devices.append(HomematicipMultiSwitch(home, device, channel)) + devices.append(HomematicipMultiSwitch(hap, device, channel)) - for group in home.groups: + for group in hap.home.groups: if isinstance(group, AsyncSwitchingGroup): - devices.append(HomematicipGroupSwitch(home, group)) + devices.append(HomematicipGroupSwitch(hap, group)) if devices: async_add_entities(devices) @@ -70,9 +70,9 @@ async def async_setup_entry( class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): """representation of a HomematicIP Cloud switch device.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the switch device.""" - super().__init__(home, device) + super().__init__(hap, device) @property def is_on(self) -> bool: @@ -91,10 +91,10 @@ class HomematicipSwitch(HomematicipGenericDevice, SwitchDevice): class HomematicipGroupSwitch(HomematicipGenericDevice, SwitchDevice): """representation of a HomematicIP switching group.""" - def __init__(self, home: AsyncHome, device, post: str = "Group") -> None: + def __init__(self, hap: HomematicipHAP, device, post: str = "Group") -> None: """Initialize switching group.""" device.modelType = f"HmIP-{post}" - super().__init__(home, device, post) + super().__init__(hap, device, post) @property def is_on(self) -> bool: @@ -148,10 +148,10 @@ class HomematicipSwitchMeasuring(HomematicipSwitch): class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): """Representation of a HomematicIP Cloud multi switch device.""" - def __init__(self, home: AsyncHome, device, channel: int): + def __init__(self, hap: HomematicipHAP, device, channel: int): """Initialize the multi switch device.""" self.channel = channel - super().__init__(home, device, f"Channel{channel}") + super().__init__(hap, device, f"Channel{channel}") @property def unique_id(self) -> str: diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 0d020312fe9..6b92b639c7a 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -6,7 +6,6 @@ from homematicip.aio.device import ( AsyncWeatherSensorPlus, AsyncWeatherSensorPro, ) -from homematicip.aio.home import AsyncHome from homematicip.base.enums import WeatherCondition from homeassistant.components.weather import WeatherEntity @@ -15,6 +14,7 @@ from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice +from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -46,15 +46,15 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" - home = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]].home + hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] devices = [] - for device in home.devices: + for device in hap.home.devices: if isinstance(device, AsyncWeatherSensorPro): - devices.append(HomematicipWeatherSensorPro(home, device)) + devices.append(HomematicipWeatherSensorPro(hap, device)) elif isinstance(device, (AsyncWeatherSensor, AsyncWeatherSensorPlus)): - devices.append(HomematicipWeatherSensor(home, device)) + devices.append(HomematicipWeatherSensor(hap, device)) - devices.append(HomematicipHomeWeather(home)) + devices.append(HomematicipHomeWeather(hap)) if devices: async_add_entities(devices) @@ -63,9 +63,9 @@ async def async_setup_entry( class HomematicipWeatherSensor(HomematicipGenericDevice, WeatherEntity): """representation of a HomematicIP Cloud weather sensor plus & basic.""" - def __init__(self, home: AsyncHome, device) -> None: + def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize the weather sensor.""" - super().__init__(home, device) + super().__init__(hap, device) @property def name(self) -> str: @@ -121,10 +121,10 @@ class HomematicipWeatherSensorPro(HomematicipWeatherSensor): class HomematicipHomeWeather(HomematicipGenericDevice, WeatherEntity): """representation of a HomematicIP Cloud home weather.""" - def __init__(self, home: AsyncHome) -> None: + def __init__(self, hap: HomematicipHAP) -> None: """Initialize the home weather.""" - home.modelType = "HmIP-Home-Weather" - super().__init__(home, home) + hap.home.modelType = "HmIP-Home-Weather" + super().__init__(hap, hap.home) @property def available(self) -> bool: diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index c301c73b4d0..2c2b020f3a0 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -6,10 +6,14 @@ import pytest from homeassistant import config_entries from homeassistant.components.homematicip_cloud import ( + CONF_ACCESSPOINT, + CONF_AUTHTOKEN, DOMAIN as HMIPC_DOMAIN, + async_setup as hmip_async_setup, const as hmipc, hap as hmip_hap, ) +from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant from .helper import AUTH_TOKEN, HAPID, HomeTemplate @@ -19,7 +23,7 @@ from tests.common import MockConfigEntry, mock_coro @pytest.fixture(name="mock_connection") def mock_connection_fixture(): - """Return a mockked connection.""" + """Return a mocked connection.""" connection = MagicMock(spec=AsyncConnection) def _rest_call_side_effect(path, body=None): @@ -39,7 +43,7 @@ def default_mock_home_fixture(mock_connection): @pytest.fixture(name="hmip_config_entry") def hmip_config_entry_fixture(): - """Create a fake config entriy for homematic ip cloud.""" + """Create a mock config entriy for homematic ip cloud.""" entry_data = { hmipc.HMIPC_HAPID: HAPID, hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, @@ -67,9 +71,32 @@ async def default_mock_hap_fixture( hap = hmip_hap.HomematicipHAP(hass, hmip_config_entry) with patch.object(hap, "get_hap", return_value=mock_coro(default_mock_home)): assert await hap.async_setup() is True + default_mock_home.on_update(hap.async_update) + default_mock_home.on_create(hap.async_create_entity) hass.data[HMIPC_DOMAIN] = {HAPID: hap} await hass.async_block_till_done() return hap + + +@pytest.fixture(name="hmip_config") +def hmip_config_fixture(): + """Create a config for homematic ip cloud.""" + + entry_data = {CONF_ACCESSPOINT: HAPID, CONF_AUTHTOKEN: AUTH_TOKEN, CONF_NAME: ""} + + return {hmipc.DOMAIN: [entry_data]} + + +@pytest.fixture(name="mock_hap_with_service") +async def mock_hap_with_service_fixture( + hass: HomeAssistant, default_mock_hap, hmip_config +): + """Create a fake homematic access point with hass services.""" + + await hmip_async_setup(hass, hmip_config) + await hass.async_block_till_done() + hass.data[HMIPC_DOMAIN] = {HAPID: default_mock_hap} + return default_mock_hap diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index 79a5bc0b201..b5e41a6ae86 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -1,18 +1,25 @@ """Helper for HomematicIP Cloud Tests.""" import json -from unittest.mock import Mock +from asynctest import Mock from homematicip.aio.class_maps import ( TYPE_CLASS_MAP, TYPE_GROUP_MAP, TYPE_SECURITY_EVENT_MAP, ) +from homematicip.aio.device import AsyncDevice +from homematicip.aio.group import AsyncGroup from homematicip.aio.home import AsyncHome from homematicip.home import Home +from homeassistant.components.homematicip_cloud.device import ( + ATTR_IS_GROUP, + ATTR_MODEL_TYPE, +) + from tests.common import load_fixture -HAPID = "Mock_HAP" +HAPID = "3014F7110000000000000001" AUTH_TOKEN = "1234" HOME_JSON = "homematicip_cloud.json" @@ -21,28 +28,38 @@ def get_and_check_entity_basics( hass, default_mock_hap, entity_id, entity_name, device_model ): """Get and test basic device.""" - ha_entity = hass.states.get(entity_id) - assert ha_entity is not None - assert ha_entity.attributes["model_type"] == device_model - assert ha_entity.name == entity_name + ha_state = hass.states.get(entity_id) + assert ha_state is not None + if device_model: + assert ha_state.attributes[ATTR_MODEL_TYPE] == device_model + assert ha_state.name == entity_name - hmip_device = default_mock_hap.home.template.search_mock_device_by_id( - ha_entity.attributes["id"] - ) - assert hmip_device is not None - return ha_entity, hmip_device + hmip_device = default_mock_hap.hmip_device_by_entity_id.get(entity_id) + if hmip_device: + if isinstance(hmip_device, AsyncDevice): + assert ha_state.attributes[ATTR_IS_GROUP] is False + elif isinstance(hmip_device, AsyncGroup): + assert ha_state.attributes[ATTR_IS_GROUP] is True + return ha_state, hmip_device async def async_manipulate_test_data( - hass, hmip_device, attribute, new_value, channel=1 + hass, hmip_device, attribute, new_value, channel=1, fire_device=None ): """Set new value on hmip device.""" if channel == 1: setattr(hmip_device, attribute, new_value) - functional_channel = hmip_device.functionalChannels[channel] - setattr(functional_channel, attribute, new_value) + if hasattr(hmip_device, "functionalChannels"): + functional_channel = hmip_device.functionalChannels[channel] + setattr(functional_channel, attribute, new_value) + + fire_target = hmip_device if fire_device is None else fire_device + + if isinstance(fire_target, AsyncHome): + fire_target.fire_update_event(fire_target._rawJSONData) # pylint: disable=W0212 + else: + fire_target.fire_update_event() - hmip_device.fire_update_event() await hass.async_block_till_done() @@ -66,8 +83,8 @@ class HomeTemplate(Home): def __init__(self, connection=None): """Init template with connection.""" super().__init__(connection=connection) - self.mock_devices = [] - self.mock_groups = [] + self.label = "Access Point" + self.model_type = "HmIP-HAP" def init_home(self, json_path=HOME_JSON): """Init template with json.""" @@ -78,24 +95,15 @@ class HomeTemplate(Home): def _generate_mocks(self): """Generate mocks for groups and devices.""" + mock_devices = [] for device in self.devices: - self.mock_devices.append(_get_mock(device)) + mock_devices.append(_get_mock(device)) + self.devices = mock_devices + + mock_groups = [] for group in self.groups: - self.mock_groups.append(_get_mock(group)) - - def search_mock_device_by_id(self, device_id): - """Search a device by given id.""" - for device in self.mock_devices: - if device.id == device_id: - return device - return None - - def search_mock_group_by_id(self, group_id): - """Search a group by given id.""" - for group in self.mock_groups: - if group.id == group_id: - return group - return None + mock_groups.append(_get_mock(group)) + self.groups = mock_groups def get_async_home_mock(self): """ @@ -105,19 +113,11 @@ class HomeTemplate(Home): and sets reuired attributes. """ mock_home = Mock( - check_connection=self._connection, - id=HAPID, - connected=True, - dutyCycle=self.dutyCycle, - devices=self.mock_devices, - groups=self.mock_groups, - weather=self.weather, - location=self.location, - label="home label", - template=self, - spec=AsyncHome, + spec=AsyncHome, wraps=self, label="Access Point", modelType="HmIP-HAP" ) + mock_home.__dict__.update(self.__dict__) mock_home.name = "" + return mock_home diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py new file mode 100644 index 00000000000..0de2101d287 --- /dev/null +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -0,0 +1,289 @@ +"""Tests for HomematicIP Cloud binary sensor.""" +from homematicip.base.enums import SmokeDetectorAlarmType, WindowState + +from homeassistant.components.homematicip_cloud.binary_sensor import ( + ATTR_ACCELERATION_SENSOR_MODE, + ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION, + ATTR_ACCELERATION_SENSOR_SENSITIVITY, + ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE, + ATTR_LOW_BATTERY, + ATTR_MOTION_DETECTED, +) +from homeassistant.const import STATE_OFF, STATE_ON + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_acceleration_sensor(hass, default_mock_hap): + """Test HomematicipAccelerationSensor.""" + entity_id = "binary_sensor.garagentor" + entity_name = "Garagentor" + device_model = "HmIP-SAM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_MODE] == "FLAT_DECT" + assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION] == "VERTICAL" + assert ( + ha_state.attributes[ATTR_ACCELERATION_SENSOR_SENSITIVITY] == "SENSOR_RANGE_4G" + ) + assert ha_state.attributes[ATTR_ACCELERATION_SENSOR_TRIGGER_ANGLE] == 45 + service_call_counter = len(hmip_device.mock_calls) + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", False + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert len(hmip_device.mock_calls) == service_call_counter + 1 + + await async_manipulate_test_data( + hass, hmip_device, "accelerationSensorTriggered", True + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert len(hmip_device.mock_calls) == service_call_counter + 2 + + +async def test_hmip_contact_interface(hass, default_mock_hap): + """Test HomematicipContactInterface.""" + entity_id = "binary_sensor.kontakt_schnittstelle_unterputz_1_fach" + entity_name = "Kontakt-Schnittstelle Unterputz – 1-fach" + device_model = "HmIP-FCI1" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "windowState", WindowState.OPEN) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "windowState", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_shutter_contact(hass, default_mock_hap): + """Test HomematicipShutterContact.""" + entity_id = "binary_sensor.fenstergriffsensor" + entity_name = "Fenstergriffsensor" + device_model = "HmIP-SRH" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + await async_manipulate_test_data( + hass, hmip_device, "windowState", WindowState.CLOSED + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await async_manipulate_test_data(hass, hmip_device, "windowState", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_motion_detector(hass, default_mock_hap): + """Test HomematicipMotionDetector.""" + entity_id = "binary_sensor.bewegungsmelder_fur_55er_rahmen_innen" + entity_name = "Bewegungsmelder für 55er Rahmen – innen" + device_model = "HmIP-SMI55" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "motionDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_presence_detector(hass, default_mock_hap): + """Test HomematicipPresenceDetector.""" + entity_id = "binary_sensor.spi_1" + entity_name = "SPI_1" + device_model = "HmIP-SPI" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "presenceDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_smoke_detector(hass, default_mock_hap): + """Test HomematicipSmokeDetector.""" + entity_id = "binary_sensor.rauchwarnmelder" + entity_name = "Rauchwarnmelder" + device_model = "HmIP-SWSD" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data( + hass, + hmip_device, + "smokeDetectorAlarmType", + SmokeDetectorAlarmType.PRIMARY_ALARM, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_water_detector(hass, default_mock_hap): + """Test HomematicipWaterDetector.""" + entity_id = "binary_sensor.wassersensor" + entity_name = "Wassersensor" + device_model = "HmIP-SWD" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", True) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", True) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", False) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await async_manipulate_test_data(hass, hmip_device, "waterlevelDetected", False) + await async_manipulate_test_data(hass, hmip_device, "moistureDetected", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_storm_sensor(hass, default_mock_hap): + """Test HomematicipStormSensor.""" + entity_id = "binary_sensor.weather_sensor_plus_storm" + entity_name = "Weather Sensor – plus Storm" + device_model = "HmIP-SWO-PL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "storm", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_rain_sensor(hass, default_mock_hap): + """Test HomematicipRainSensor.""" + entity_id = "binary_sensor.wettersensor_pro_raining" + entity_name = "Wettersensor - pro Raining" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "raining", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_sunshine_sensor(hass, default_mock_hap): + """Test HomematicipSunshineSensor.""" + entity_id = "binary_sensor.wettersensor_pro_sunshine" + entity_name = "Wettersensor - pro Sunshine" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert ha_state.attributes["today_sunshine_duration_in_minutes"] == 100 + await async_manipulate_test_data(hass, hmip_device, "sunshine", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_battery_sensor(hass, default_mock_hap): + """Test HomematicipSunshineSensor.""" + entity_id = "binary_sensor.wohnungsture_battery" + entity_name = "Wohnungstüre Battery" + device_model = "HMIP-SWDO" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "lowBat", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_security_zone_sensor_group(hass, default_mock_hap): + """Test HomematicipSecurityZoneSensorGroup.""" + entity_id = "binary_sensor.internal_securityzone" + entity_name = "INTERNAL SecurityZone" + device_model = "HmIP-SecurityZone" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "motionDetected", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_MOTION_DETECTED] is True + + +async def test_hmip_security_sensor_group(hass, default_mock_hap): + """Test HomematicipSecuritySensorGroup.""" + entity_id = "binary_sensor.buro_sensors" + entity_name = "Büro Sensors" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get("low_bat") + await async_manipulate_test_data(hass, hmip_device, "lowBat", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_LOW_BATTERY] is True + + await async_manipulate_test_data(hass, hmip_device, "lowBat", False) + await async_manipulate_test_data( + hass, + hmip_device, + "smokeDetectorAlarmType", + SmokeDetectorAlarmType.PRIMARY_ALARM, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ( + ha_state.attributes["smoke_detector_alarm"] + == SmokeDetectorAlarmType.PRIMARY_ALARM + ) diff --git a/tests/components/homematicip_cloud/test_binary_sensors.py b/tests/components/homematicip_cloud/test_binary_sensors.py deleted file mode 100644 index 4471c5dd7f3..00000000000 --- a/tests/components/homematicip_cloud/test_binary_sensors.py +++ /dev/null @@ -1,41 +0,0 @@ -"""Tests for HomematicIP Cloud lights.""" -import logging - -from tests.components.homematicip_cloud.helper import ( - async_manipulate_test_data, - get_and_check_entity_basics, -) - -_LOGGER = logging.getLogger(__name__) - - -async def test_hmip_sam(hass, default_mock_hap): - """Test HomematicipLight.""" - entity_id = "binary_sensor.garagentor" - entity_name = "Garagentor" - device_model = "HmIP-SAM" - - ha_entity, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model - ) - - assert ha_entity.state == "on" - assert ha_entity.attributes["acceleration_sensor_mode"] == "FLAT_DECT" - assert ha_entity.attributes["acceleration_sensor_neutral_position"] == "VERTICAL" - assert ha_entity.attributes["acceleration_sensor_sensitivity"] == "SENSOR_RANGE_4G" - assert ha_entity.attributes["acceleration_sensor_trigger_angle"] == 45 - service_call_counter = len(hmip_device.mock_calls) - - await async_manipulate_test_data( - hass, hmip_device, "accelerationSensorTriggered", False - ) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "off" - assert len(hmip_device.mock_calls) == service_call_counter + 1 - - await async_manipulate_test_data( - hass, hmip_device, "accelerationSensorTriggered", True - ) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "on" - assert len(hmip_device.mock_calls) == service_call_counter + 2 diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py new file mode 100644 index 00000000000..a8d4984520c --- /dev/null +++ b/tests/components/homematicip_cloud/test_light.py @@ -0,0 +1,196 @@ +"""Tests for HomematicIP Cloud light.""" +from homematicip.base.enums import RGBColorState + +from homeassistant.components.homematicip_cloud.light import ( + ATTR_ENERGY_COUNTER, + ATTR_POWER_CONSUMPTION, +) +from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_NAME +from homeassistant.const import STATE_OFF, STATE_ON + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_light(hass, default_mock_hap): + """Test HomematicipLight.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + + service_call_counter = len(hmip_device.mock_calls) + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_notification_light(hass, default_mock_hap): + """Test HomematicipNotificationLight.""" + entity_id = "light.treppe_top_notification" + entity_name = "Treppe Top Notification" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + # Send all color via service call. + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.RED, 1.0) + + color_list = { + RGBColorState.WHITE: [0.0, 0.0], + RGBColorState.RED: [0.0, 100.0], + RGBColorState.YELLOW: [60.0, 100.0], + RGBColorState.GREEN: [120.0, 100.0], + RGBColorState.TURQUOISE: [180.0, 100.0], + RGBColorState.BLUE: [240.0, 100.0], + RGBColorState.PURPLE: [300.0, 100.0], + } + + for color, hs_color in color_list.items(): + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "hs_color": hs_color}, + blocking=True, + ) + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == (2, color, 0.0392156862745098) + + assert len(hmip_device.mock_calls) == service_call_counter + 8 + + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == ( + 2, + RGBColorState.PURPLE, + 0.0392156862745098, + ) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1, 2) + await async_manipulate_test_data( + hass, hmip_device, "simpleRGBColorState", RGBColorState.PURPLE, 2 + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_COLOR_NAME] == RGBColorState.PURPLE + assert ha_state.attributes[ATTR_BRIGHTNESS] == 255 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 11 + assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" + assert hmip_device.mock_calls[-1][1] == (2, RGBColorState.PURPLE, 0.0) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, 2) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_dimmer(hass, default_mock_hap): + """Test HomematicipDimmer.""" + entity_id = "light.schlafzimmerlicht" + entity_name = "Schlafzimmerlicht" + device_model = "HmIP-BDT" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (1,) + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": entity_id, "brightness_pct": "100"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 2 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (1.0,) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_BRIGHTNESS] == 255 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "set_dim_level" + assert hmip_device.mock_calls[-1][1] == (0,) + await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + +async def test_hmip_light_measuring(hass, default_mock_hap): + """Test HomematicipLightMeasuring.""" + entity_id = "light.flur_oben" + entity_name = "Flur oben" + device_model = "HmIP-BSM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "light", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_POWER_CONSUMPTION] == 50 + assert ha_state.attributes[ATTR_ENERGY_COUNTER] == 6.33 + + await hass.services.async_call( + "light", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF diff --git a/tests/components/homematicip_cloud/test_lights.py b/tests/components/homematicip_cloud/test_lights.py deleted file mode 100644 index dcf5f76d0a0..00000000000 --- a/tests/components/homematicip_cloud/test_lights.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Tests for HomematicIP Cloud lights.""" -import logging - -from tests.components.homematicip_cloud.helper import ( - async_manipulate_test_data, - get_and_check_entity_basics, -) - -_LOGGER = logging.getLogger(__name__) - - -async def test_hmip_light(hass, default_mock_hap): - """Test HomematicipLight.""" - entity_id = "light.treppe" - entity_name = "Treppe" - device_model = "HmIP-BSL" - - ha_entity, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model - ) - - assert ha_entity.state == "on" - - service_call_counter = len(hmip_device.mock_calls) - await hass.services.async_call( - "light", "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert len(hmip_device.mock_calls) == service_call_counter + 1 - assert hmip_device.mock_calls[-1][0] == "turn_off" - await async_manipulate_test_data(hass, hmip_device, "on", False) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "off" - - await hass.services.async_call( - "light", "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert len(hmip_device.mock_calls) == service_call_counter + 3 - assert hmip_device.mock_calls[-1][0] == "turn_on" - await async_manipulate_test_data(hass, hmip_device, "on", True) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "on" - - -# HomematicipLightMeasuring -# HomematicipDimmer - - -async def test_hmip_notification_light(hass, default_mock_hap): - """Test HomematicipNotificationLight.""" - entity_id = "light.treppe_top_notification" - entity_name = "Treppe Top Notification" - device_model = "HmIP-BSL" - - ha_entity, hmip_device = get_and_check_entity_basics( - hass, default_mock_hap, entity_id, entity_name, device_model - ) - - assert ha_entity.state == "off" - service_call_counter = len(hmip_device.mock_calls) - - await hass.services.async_call( - "light", "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert len(hmip_device.mock_calls) == service_call_counter + 1 - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - await async_manipulate_test_data(hass, hmip_device, "dimLevel", 100, 2) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "on" - - await hass.services.async_call( - "light", "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert len(hmip_device.mock_calls) == service_call_counter + 3 - assert hmip_device.mock_calls[-1][0] == "set_rgb_dim_level" - await async_manipulate_test_data(hass, hmip_device, "dimLevel", 0, 2) - ha_entity = hass.states.get(entity_id) - assert ha_entity.state == "off" diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index b96bff8fac9..1d3d5bfd8f4 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -2077,6 +2077,144 @@ "type": "SHUTTER_CONTACT", "updateState": "UP_TO_DATE" }, + "3014F7110000000000000108": { + "availableFirmwareVersion": "1.12.6", + "firmwareVersion": "1.12.6", + "firmwareVersionInteger": 68614, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000108", + "deviceOverheated": false, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000009" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -68, + "rssiPeerValue": -63, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "currentPowerConsumption": 0.0, + "deviceId": "3014F7110000000000000108", + "energyCounter": 6.333200000000001, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000023" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000108", + "label": "Flur oben", + "lastStatusUpdate": 1570365990392, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 288, + "modelType": "HmIP-BSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000108", + "type": "BRAND_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, + "3014F7110000000000000109": { + "availableFirmwareVersion": "1.6.2", + "firmwareVersion": "1.6.2", + "firmwareVersionInteger": 67074, + "functionalChannels": { + "0": { + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000000000000109", + "deviceOverheated": null, + "deviceOverloaded": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000029" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -80, + "rssiPeerValue": -73, + "supportedOptionalFeatures": { + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "currentPowerConsumption": 0.0, + "deviceId": "3014F7110000000000000109", + "energyCounter": 0.0011, + "functionalChannelType": "SWITCH_MEASURING_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000030" + ], + "index": 1, + "label": "", + "on": false, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000011", + "label": "Ausschalter Terrasse Bewegungsmelder", + "lastStatusUpdate": 1570366291250, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 289, + "modelType": "HmIP-FSM", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000109", + "type": "FULL_FLUSH_SWITCH_MEASURING", + "updateState": "UP_TO_DATE" + }, "3014F7110000000000000008": { "availableFirmwareVersion": "0.0.0", "firmwareVersion": "2.6.2", @@ -2236,6 +2374,57 @@ "type": "PLUGABLE_SWITCH_MEASURING", "updateState": "UP_TO_DATE" }, + "3014F7110000000000000110": { + "availableFirmwareVersion": "0.0.0", + "firmwareVersion": "2.6.2", + "firmwareVersionInteger": 132610, + "functionalChannels": { + "0": { + "configPending": false, + "deviceId": "3014F7110000000000000110", + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000017" + ], + "index": 0, + "label": "", + "lowBat": null, + "routerModuleEnabled": true, + "routerModuleSupported": true, + "rssiDeviceValue": -47, + "rssiPeerValue": -49, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000000000000110", + "functionalChannelType": "SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000018" + ], + "index": 1, + "label": "", + "on": true, + "profileMode": "AUTOMATIC", + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000000000000110", + "label": "Schrank", + "lastStatusUpdate": 1524513613922, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 262, + "modelType": "HMIP-PS", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000000000000110", + "type": "PLUGABLE_SWITCH", + "updateState": "UP_TO_DATE" + }, "3014F7110000000000000011": { "automaticValveAdaptionNeeded": false, "availableFirmwareVersion": "2.0.2", From 618cf5fa0433dfda65494101f8a7c00be4989340 Mon Sep 17 00:00:00 2001 From: Paolo Tuninetto Date: Fri, 11 Oct 2019 17:52:38 +0200 Subject: [PATCH 157/639] Move Arduino imports (#27438) --- homeassistant/components/arduino/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arduino/__init__.py b/homeassistant/components/arduino/__init__.py index 4dcde93e749..f973ec136e3 100644 --- a/homeassistant/components/arduino/__init__.py +++ b/homeassistant/components/arduino/__init__.py @@ -1,8 +1,11 @@ """Support for Arduino boards running with the Firmata firmware.""" import logging +import serial import voluptuous as vol +from PyMata.pymata import PyMata + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_PORT import homeassistant.helpers.config_validation as cv @@ -20,7 +23,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Arduino component.""" - import serial port = config[DOMAIN][CONF_PORT] @@ -59,7 +61,6 @@ class ArduinoBoard: def __init__(self, port): """Initialize the board.""" - from PyMata.pymata import PyMata self._port = port self._board = PyMata(self._port, verbose=False) From cb30065a4027fa462b83019a5cef5e90085676b3 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 11 Oct 2019 18:29:27 +0200 Subject: [PATCH 158/639] Update upstream (#27440) --- homeassistant/components/rmvtransport/manifest.json | 2 +- homeassistant/components/rmvtransport/sensor.py | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 1f06daf0623..ed33caa1264 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -3,7 +3,7 @@ "name": "Rmvtransport", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", "requirements": [ - "PyRMVtransport==0.1.3" + "PyRMVtransport==0.2.9" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index f66f22dda17..9acafbbf81f 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -230,8 +230,8 @@ class RMVDepartureData: _data = await self.rmv.get_departures( self._station_id, products=self._products, - directionId=self._direction, - maxJourneys=50, + direction_id=self._direction, + max_journeys=50, ) except RMVtransportApiConnectionError: self.departures = [] diff --git a/requirements_all.txt b/requirements_all.txt index 56616498662..bff608964a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -66,7 +66,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.1.3 +PyRMVtransport==0.2.9 # homeassistant.components.switchbot # PySwitchbot==0.6.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84cef21a1fe..0f37ef15992 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -34,7 +34,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.1.3 +PyRMVtransport==0.2.9 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From 8bd847ed3969e75a1c03dcf652df28ab65106d1e Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 11 Oct 2019 18:30:27 +0200 Subject: [PATCH 159/639] Move imports in waterfurnace component (#27449) --- homeassistant/components/waterfurnace/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/waterfurnace/__init__.py b/homeassistant/components/waterfurnace/__init__.py index acc1c22c734..b6eb22c89ae 100644 --- a/homeassistant/components/waterfurnace/__init__.py +++ b/homeassistant/components/waterfurnace/__init__.py @@ -5,6 +5,7 @@ import time import threading import voluptuous as vol +from waterfurnace.waterfurnace import WaterFurnace, WFCredentialError, WFException from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -37,19 +38,18 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, base_config): """Set up waterfurnace platform.""" - import waterfurnace.waterfurnace as wf config = base_config.get(DOMAIN) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) - wfconn = wf.WaterFurnace(username, password) + wfconn = WaterFurnace(username, password) # NOTE(sdague): login will throw an exception if this doesn't # work, which will abort the setup. try: wfconn.login() - except wf.WFCredentialError: + except WFCredentialError: _LOGGER.error("Invalid credentials for waterfurnace login.") return False @@ -83,7 +83,6 @@ class WaterFurnaceData(threading.Thread): def _reconnect(self): """Reconnect on a failure.""" - import waterfurnace.waterfurnace as wf self._fails += 1 if self._fails > MAX_FAILS: @@ -105,7 +104,7 @@ class WaterFurnaceData(threading.Thread): try: self.client.login() self.data = self.client.read() - except wf.WFException: + except WFException: _LOGGER.exception("Failed to reconnect attempt %s", self._fails) else: _LOGGER.debug("Reconnected to furnace") @@ -113,7 +112,6 @@ class WaterFurnaceData(threading.Thread): def run(self): """Thread run loop.""" - import waterfurnace.waterfurnace as wf @callback def register(): @@ -143,7 +141,7 @@ class WaterFurnaceData(threading.Thread): try: self.data = self.client.read() - except wf.WFException: + except WFException: # WFExceptions are things the WF library understands # that pretty much can all be solved by logging in and # back out again. From 78a08d04259b904fd4caf9cd71fd4ace1091cee1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 12 Oct 2019 00:31:47 +0000 Subject: [PATCH 160/639] [ci skip] Translation update --- .../binary_sensor/.translations/de.json | 7 ++ .../binary_sensor/.translations/ru.json | 79 ++++++++++++++++--- .../components/deconz/.translations/ru.json | 21 ++--- .../components/light/.translations/ru.json | 8 +- .../components/neato/.translations/da.json | 3 + .../components/plex/.translations/ca.json | 1 + .../components/plex/.translations/da.json | 1 + .../components/plex/.translations/de.json | 8 ++ .../components/sensor/.translations/ru.json | 15 ++++ .../components/switch/.translations/ru.json | 12 +-- .../components/zha/.translations/de.json | 4 +- .../components/zha/.translations/ru.json | 37 ++++++++- 12 files changed, 165 insertions(+), 31 deletions(-) create mode 100644 homeassistant/components/binary_sensor/.translations/de.json create mode 100644 homeassistant/components/sensor/.translations/ru.json diff --git a/homeassistant/components/binary_sensor/.translations/de.json b/homeassistant/components/binary_sensor/.translations/de.json new file mode 100644 index 00000000000..25e8ea2f86b --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/de.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "trigger_type": { + "plugged_in": "{entity_name} eingesteckt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json index 7d73cb8d4aa..012a5c4fa45 100644 --- a/homeassistant/components/binary_sensor/.translations/ru.json +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -1,15 +1,76 @@ { "device_automation": { "condition_type": { - "is_bat_low": "{entity_name}: \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", - "is_cold": "{entity_name}: \u0445\u043e\u043b\u043e\u0434\u043d\u043e", - "is_connected": "{entity_name}: \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "is_gas": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0433\u0430\u0437", - "is_hot": "{entity_name}: \u0433\u043e\u0440\u044f\u0447\u043e", - "is_light": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d \u0441\u0432\u0435\u0442", - "is_locked": "{entity_name}: \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e", - "is_moist": "{entity_name}: \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0432\u043b\u0430\u0433\u0430", - "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435" + "is_gas": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437", + "is_light": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442", + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_moist": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443", + "is_motion": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_moving": "{entity_name} \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "is_no_gas": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437", + "is_no_light": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442", + "is_no_motion": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "is_no_problem": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", + "is_no_sound": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_no_vibration": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", + "is_not_locked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_not_moist": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443", + "is_not_moving": "{entity_name} \u043d\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", + "is_sound": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_vibration": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" + }, + "trigger_type": { + "bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "cold": "{entity_name} \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0435\u0442\u0441\u044f", + "connected": "{entity_name} \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "gas": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", + "hot": "{entity_name} \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0435\u0442\u0441\u044f", + "light": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442", + "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", + "moist": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443", + "moist\u00a7": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443", + "motion": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "moving": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435", + "no_gas": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", + "no_light": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442", + "no_motion": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", + "no_problem": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "no_smoke": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", + "no_sound": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a", + "no_vibration": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", + "not_bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u044b\u0439 \u0437\u0430\u0440\u044f\u0434", + "not_cold": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0442\u044c\u0441\u044f", + "not_connected": "{entity_name} \u043e\u0442\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "not_hot": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432\u0430\u0442\u044c\u0441\u044f", + "not_locked": "{entity_name} \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", + "not_moist": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443", + "not_moving": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435", + "not_occupied": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "not_plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u044d\u043d\u0435\u0440\u0433\u0438\u0438", + "not_present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "not_unsafe": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", + "occupied": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "powered": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u044d\u043d\u0435\u0440\u0433\u0438\u0438", + "present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "problem": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "smoke": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", + "sound": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0437\u0432\u0443\u043a", + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "unsafe": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", + "vibration": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 558fd9e5897..a7200a0cbb4 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -48,26 +48,27 @@ "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", - "close": "\u0417\u0430\u043a\u0440\u044b\u0442\u043e", - "dim_down": "\u0423\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", - "dim_up": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044f\u0440\u043a\u043e\u0441\u0442\u044c", + "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "dim_down": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0435\u0442\u0441\u044f", + "dim_up": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f", "left": "\u041d\u0430\u043b\u0435\u0432\u043e", - "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", - "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" }, "trigger_type": { - "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430\u0436\u0434\u044b", + "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", - "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u0434\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0451\u0440\u043d\u0443\u0442\u0430", + "remote_button_rotation_stopped": "\u041f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442\u0441\u044f \u0432\u0440\u0430\u0449\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \"{subtype}\"", "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", - "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438\u0436\u0434\u044b", - "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438" + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430", + "remote_gyro_activated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438" } }, "options": { diff --git a/homeassistant/components/light/.translations/ru.json b/homeassistant/components/light/.translations/ru.json index a6a7994b7c3..8ca964606ae 100644 --- a/homeassistant/components/light/.translations/ru.json +++ b/homeassistant/components/light/.translations/ru.json @@ -6,12 +6,12 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, "condition_type": { - "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/da.json b/homeassistant/components/neato/.translations/da.json index 7f0d122f38b..ca180efa005 100644 --- a/homeassistant/components/neato/.translations/da.json +++ b/homeassistant/components/neato/.translations/da.json @@ -4,6 +4,9 @@ "already_configured": "Allerede konfigureret", "invalid_credentials": "Ugyldige legitimationsoplysninger" }, + "create_entry": { + "default": "Se [Neato-dokumentation] ({docs_url})." + }, "error": { "invalid_credentials": "Ugyldige legitimationsoplysninger", "unexpected_error": "Uventet fejl" diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index a3ba5185371..7a8cf7a1424 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -4,6 +4,7 @@ "all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats", "already_configured": "Aquest servidor Plex ja est\u00e0 configurat", "already_in_progress": "S\u2019est\u00e0 configurant Plex", + "discovery_no_file": "No s'ha trobat cap fitxer de configuraci\u00f3 heretat", "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del testimoni.", "unknown": "Ha fallat per motiu desconegut" diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 4ca695e74d8..99d5d4d1685 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -34,6 +34,7 @@ "title": "V\u00e6lg Plex-server" }, "start_website_auth": { + "description": "Forts\u00e6t for at autorisere p\u00e5 plex.tv.", "title": "Tilslut Plex-server" }, "user": { diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json index 95083102273..56715e60a8c 100644 --- a/homeassistant/components/plex/.translations/de.json +++ b/homeassistant/components/plex/.translations/de.json @@ -5,6 +5,11 @@ }, "step": { "manual_setup": { + "data": { + "host": "Host", + "port": "Port", + "ssl": "SSL verwenden" + }, "title": "Plex Server" }, "start_website_auth": { @@ -12,6 +17,9 @@ "title": "Plex Server verbinden" }, "user": { + "data": { + "manual_setup": "Manuelle Einrichtung" + }, "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." } } diff --git a/homeassistant/components/sensor/.translations/ru.json b/homeassistant/components/sensor/.translations/ru.json new file mode 100644 index 00000000000..8c70f41fcb7 --- /dev/null +++ b/homeassistant/components/sensor/.translations/ru.json @@ -0,0 +1,15 @@ +{ + "device_automation": { + "trigger_type": { + "battery_level": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "humidity": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "illuminance": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "power": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "pressure": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "signal_strength": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "temperature": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "timestamp": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435", + "value": "{entity_name} \u0438\u0437\u043c\u0435\u043d\u044f\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index cd5cbc0d6a1..74503eea60b 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -6,14 +6,14 @@ "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" }, "condition_type": { - "is_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "is_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", - "turn_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e" + "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "turn_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "turn_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435" + "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 9ffd5211a1f..969c78e7b13 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -22,7 +22,9 @@ "close": "Schlie\u00dfen", "left": "Links", "open": "Offen", - "right": "Rechts" + "right": "Rechts", + "turn_off": "Ausschalten", + "turn_on": "Einschalten" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index 2f6f42311c3..291d760dbc8 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -19,7 +19,42 @@ }, "device_automation": { "action_type": { - "warn": "\u041f\u0440\u0435\u0434\u0443\u043f\u0440\u0435\u0436\u0434\u0435\u043d\u0438\u0435" + "squawk": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0441\u0438\u0440\u0435\u043d\u0443", + "warn": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u043f\u043e\u0432\u0435\u0449\u0435\u043d\u0438\u0435" + }, + "trigger_subtype": { + "both_buttons": "\u041e\u0431\u0435 \u043a\u043d\u043e\u043f\u043a\u0438", + "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_5": "\u041f\u044f\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_6": "\u0428\u0435\u0441\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "close": "\u0417\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "dim_down": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0435\u0442\u0441\u044f", + "dim_up": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f", + "left": "\u041d\u0430\u043b\u0435\u0432\u043e", + "open": "\u041e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "right": "\u041d\u0430\u043f\u0440\u0430\u0432\u043e", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" + }, + "trigger_type": { + "device_dropped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441\u0431\u0440\u043e\u0441\u0438\u043b\u0438", + "device_flipped": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", + "device_knocked": "\u041f\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u043f\u043e\u0441\u0442\u0443\u0447\u0430\u043b\u0438 \"{subtype}\"", + "device_rotated": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \"{subtype}\"", + "device_shaken": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u0441\u0442\u0440\u044f\u0445\u043d\u0443\u043b\u0438", + "device_slid": "\u0421\u0434\u0432\u0438\u0433 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \"{subtype}\"", + "device_tilted": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0430\u043a\u043b\u043e\u043d\u0438\u043b\u0438", + "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", + "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0447\u0435\u0442\u044b\u0440\u0435 \u0440\u0430\u0437\u0430", + "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u043f\u044f\u0442\u044c \u0440\u0430\u0437", + "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430", + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" } } } \ No newline at end of file From d58717d7720769d703662fa3e2f093742bfd18a7 Mon Sep 17 00:00:00 2001 From: John Mihalic <2854333+mezz64@users.noreply.github.com> Date: Sat, 12 Oct 2019 01:18:15 -0400 Subject: [PATCH 161/639] Bump pyhik to 0.2.4 (#27523) --- homeassistant/components/hikvision/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hikvision/manifest.json b/homeassistant/components/hikvision/manifest.json index 78917a5351b..11775ed3ae0 100644 --- a/homeassistant/components/hikvision/manifest.json +++ b/homeassistant/components/hikvision/manifest.json @@ -3,7 +3,7 @@ "name": "Hikvision", "documentation": "https://www.home-assistant.io/integrations/hikvision", "requirements": [ - "pyhik==0.2.3" + "pyhik==0.2.4" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index bff608964a4..0512c343b74 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1220,7 +1220,7 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.hikvision -pyhik==0.2.3 +pyhik==0.2.4 # homeassistant.components.hive pyhiveapi==0.2.19.3 From 712628395e18e088ce70d89b5c19890e1c752e77 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 07:18:47 +0200 Subject: [PATCH 162/639] moved imports to top level (#27511) --- homeassistant/components/browser/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/browser/__init__.py b/homeassistant/components/browser/__init__.py index b163f16a5c4..b7612def701 100644 --- a/homeassistant/components/browser/__init__.py +++ b/homeassistant/components/browser/__init__.py @@ -1,4 +1,5 @@ """Support for launching a web browser on the host machine.""" +import webbrowser import voluptuous as vol ATTR_URL = "url" @@ -18,7 +19,6 @@ SERVICE_BROWSE_URL_SCHEMA = vol.Schema( def setup(hass, config): """Listen for browse_url events.""" - import webbrowser hass.services.register( DOMAIN, From f236e84753d5836a76303ce05d46fc1da2623bd6 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 07:19:53 +0200 Subject: [PATCH 163/639] Move imports in updater component (#27485) --- homeassistant/components/updater/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index dd270a0bb75..22c11d0c38e 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -10,6 +10,7 @@ import uuid import aiohttp import async_timeout +from distro import linux_distribution import voluptuous as vol from homeassistant.const import __version__ as current_version @@ -145,9 +146,7 @@ async def get_newest_version(hass, huuid, include_components): if include_components: info_object["components"] = list(hass.config.components) - import distro - - linux_dist = await hass.async_add_executor_job(distro.linux_distribution, False) + linux_dist = await hass.async_add_executor_job(linux_distribution, False) info_object["distribution"] = linux_dist[0] info_object["os_version"] = linux_dist[1] From 99e78084415b3f87790bd4ceb26684dcef30cf83 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sat, 12 Oct 2019 07:21:53 +0200 Subject: [PATCH 164/639] Move imports in rmvtransport (#27420) --- homeassistant/components/rmvtransport/sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 9acafbbf81f..190274518cd 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -3,6 +3,8 @@ import asyncio import logging from datetime import timedelta +from RMVtransport import RMVtransport +from RMVtransport.rmvtransport import RMVtransportApiConnectionError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -208,8 +210,6 @@ class RMVDepartureData: timeout, ): """Initialize the sensor.""" - from RMVtransport import RMVtransport - self.station = None self._station_id = station_id self._destinations = destinations @@ -224,8 +224,6 @@ class RMVDepartureData: @Throttle(SCAN_INTERVAL) async def async_update(self): """Update the connection data.""" - from RMVtransport.rmvtransport import RMVtransportApiConnectionError - try: _data = await self.rmv.get_departures( self._station_id, From a712c9b9f5ebf761d6b45f6015b20c0987d565d2 Mon Sep 17 00:00:00 2001 From: Jacob Mansfield Date: Sat, 12 Oct 2019 06:23:55 +0100 Subject: [PATCH 165/639] SNMP Switch payloads are not guaranteed to be integers (#27422) Fixes #27171 --- homeassistant/components/snmp/switch.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 95496cb6a45..204e98cca83 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -186,20 +186,15 @@ class SnmpSwitch(SwitchDevice): async def async_turn_on(self, **kwargs): """Turn on the switch.""" - from pyasn1.type.univ import Integer - - await self._set(Integer(self._command_payload_on)) + await self._set(self._command_payload_on) async def async_turn_off(self, **kwargs): """Turn off the switch.""" - from pyasn1.type.univ import Integer - - await self._set(Integer(self._command_payload_off)) + await self._set(self._command_payload_off) async def async_update(self): """Update the state.""" from pysnmp.hlapi.asyncio import getCmd, ObjectType, ObjectIdentity - from pyasn1.type.univ import Integer errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) @@ -215,9 +210,9 @@ class SnmpSwitch(SwitchDevice): ) else: for resrow in restable: - if resrow[-1] == Integer(self._payload_on): + if resrow[-1] == self._payload_on: self._state = True - elif resrow[-1] == Integer(self._payload_off): + elif resrow[-1] == self._payload_off: self._state = False else: self._state = None From d516bc44fa77a3b827972f8446d94a25d64fede9 Mon Sep 17 00:00:00 2001 From: thaohtp Date: Sat, 12 Oct 2019 07:40:44 +0200 Subject: [PATCH 166/639] Move trend imports to top level (#27507) --- homeassistant/components/trend/binary_sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 9154f891cc1..7c4a2dc4067 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -3,6 +3,7 @@ from collections import deque import logging import math +import numpy as np import voluptuous as vol from homeassistant.components.binary_sensor import ( @@ -17,9 +18,9 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_ENTITY_ID, CONF_FRIENDLY_NAME, - STATE_UNKNOWN, - STATE_UNAVAILABLE, CONF_SENSORS, + STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -207,8 +208,6 @@ class SensorTrend(BinarySensorDevice): This need run inside executor. """ - import numpy as np - timestamps = np.array([t for t, _ in self.samples]) values = np.array([s for _, s in self.samples]) coeffs = np.polyfit(timestamps, values, 1) From af4bcf8de6a01d1b9eab1e3a3441c0a685a35e92 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 07:44:22 +0200 Subject: [PATCH 167/639] Move imports in waqi component (#27450) --- homeassistant/components/waqi/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/waqi/sensor.py b/homeassistant/components/waqi/sensor.py index 9f3c3ffc13e..b53723a29b6 100644 --- a/homeassistant/components/waqi/sensor.py +++ b/homeassistant/components/waqi/sensor.py @@ -5,6 +5,7 @@ from datetime import timedelta import aiohttp import voluptuous as vol +from waqiasync import WaqiClient from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv @@ -60,13 +61,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the requested World Air Quality Index locations.""" - import waqiasync token = config.get(CONF_TOKEN) station_filter = config.get(CONF_STATIONS) locations = config.get(CONF_LOCATIONS) - client = waqiasync.WaqiClient(token, async_get_clientsession(hass), timeout=TIMEOUT) + client = WaqiClient(token, async_get_clientsession(hass), timeout=TIMEOUT) dev = [] try: for location_name in locations: From de4482e8d3cc2659d7f4898bdeaa6bf1b010e663 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 08:43:34 +0200 Subject: [PATCH 168/639] Move imports in acer_projector component (#27456) --- homeassistant/components/acer_projector/switch.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/acer_projector/switch.py b/homeassistant/components/acer_projector/switch.py index 558cf84d0e1..39a79636c93 100644 --- a/homeassistant/components/acer_projector/switch.py +++ b/homeassistant/components/acer_projector/switch.py @@ -1,6 +1,7 @@ """Use serial protocol of Acer projector to obtain state of the projector.""" import logging import re +import serial import voluptuous as vol @@ -73,7 +74,6 @@ class AcerSwitch(SwitchDevice): def __init__(self, serial_port, name, timeout, write_timeout, **kwargs): """Init of the Acer projector.""" - import serial self.ser = serial.Serial( port=serial_port, timeout=timeout, write_timeout=write_timeout, **kwargs @@ -90,7 +90,6 @@ class AcerSwitch(SwitchDevice): def _write_read(self, msg): """Write to the projector and read the return.""" - import serial ret = "" # Sometimes the projector won't answer for no reason or the projector From 3d05228ec1fec7ac3f3dcc3bcac6be083e7b143c Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 14:09:39 +0200 Subject: [PATCH 169/639] Move imports in vizio component (#27452) --- homeassistant/components/vizio/media_player.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index b844f94a187..f64fd2ca531 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -1,7 +1,10 @@ """Vizio SmartCast Device support.""" from datetime import timedelta import logging + import voluptuous as vol +from pyvizio import Vizio + from homeassistant import util from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( @@ -122,7 +125,6 @@ class VizioDevice(MediaPlayerDevice): def __init__(self, host, token, name, volume_step, device_type): """Initialize Vizio device.""" - import pyvizio self._name = name self._state = None @@ -132,7 +134,7 @@ class VizioDevice(MediaPlayerDevice): self._available_inputs = None self._device_type = device_type self._supported_commands = SUPPORTED_COMMANDS[device_type] - self._device = pyvizio.Vizio(DEVICE_ID, host, DEFAULT_NAME, token, device_type) + self._device = Vizio(DEVICE_ID, host, DEFAULT_NAME, token, device_type) self._max_volume = float(self._device.get_max_volume()) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) From 9d7a218df5156dc9afff73504f9e8e61becb7c19 Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Sat, 12 Oct 2019 15:08:57 +0200 Subject: [PATCH 170/639] Bump pygatt to 4.0.5 (#27526) --- homeassistant/components/bluetooth_le_tracker/manifest.json | 2 +- homeassistant/components/skybeacon/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/manifest.json b/homeassistant/components/bluetooth_le_tracker/manifest.json index 30ed924a9dc..d9f4cb0a2b5 100644 --- a/homeassistant/components/bluetooth_le_tracker/manifest.json +++ b/homeassistant/components/bluetooth_le_tracker/manifest.json @@ -3,7 +3,7 @@ "name": "Bluetooth le tracker", "documentation": "https://www.home-assistant.io/integrations/bluetooth_le_tracker", "requirements": [ - "pygatt[GATTTOOL]==4.0.1" + "pygatt[GATTTOOL]==4.0.5" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/skybeacon/manifest.json b/homeassistant/components/skybeacon/manifest.json index a3cb97cdc2d..7ab42c5da87 100644 --- a/homeassistant/components/skybeacon/manifest.json +++ b/homeassistant/components/skybeacon/manifest.json @@ -3,7 +3,7 @@ "name": "Skybeacon", "documentation": "https://www.home-assistant.io/integrations/skybeacon", "requirements": [ - "pygatt[GATTTOOL]==4.0.1" + "pygatt[GATTTOOL]==4.0.5" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 0512c343b74..f1050385929 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1202,7 +1202,7 @@ pyfttt==0.3 # homeassistant.components.bluetooth_le_tracker # homeassistant.components.skybeacon -pygatt[GATTTOOL]==4.0.1 +pygatt[GATTTOOL]==4.0.5 # homeassistant.components.gogogate2 pygogogate2==0.1.1 From 22eaff9897b65525269b88e73aebd4cd64e9ea3b Mon Sep 17 00:00:00 2001 From: Florent Thoumie Date: Sat, 12 Oct 2019 06:17:02 -0700 Subject: [PATCH 171/639] iaqualink: set 5s timeout, use cookiejar defaults (#27426) --- homeassistant/components/iaqualink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index dec91186be2..9ce0e04895f 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -3,7 +3,7 @@ import asyncio from functools import wraps import logging -from aiohttp import CookieJar +from aiohttp import ClientTimeout import voluptuous as vol from iaqualink import ( @@ -85,7 +85,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> None sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] switches = hass.data[DOMAIN][SWITCH_DOMAIN] = [] - session = async_create_clientsession(hass, cookie_jar=CookieJar(unsafe=True)) + session = async_create_clientsession(hass, timeout=ClientTimeout(total=5)) aqualink = AqualinkClient(username, password, session) try: await aqualink.login() From dbe366933f6be943887d365ec7128498054a7316 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Oct 2019 15:37:32 +0200 Subject: [PATCH 172/639] Fix typing for device condition scaffold (#27487) --- .../templates/device_condition/integration/device_condition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index e9c7e55e23a..9acb351b197 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -29,7 +29,7 @@ CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( ) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[str]: +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: """List device conditions for NEW_NAME devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] From 21ca936d33ca824614621fbe7a17c20c220b2291 Mon Sep 17 00:00:00 2001 From: thaohtp Date: Sat, 12 Oct 2019 16:30:21 +0200 Subject: [PATCH 173/639] Move imports in upcloud component to top-level (#27514) * Move imports in upcloud component to top-level * Additional isort ordering --- homeassistant/components/upcloud/__init__.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/upcloud/__init__.py b/homeassistant/components/upcloud/__init__.py index 3656ba48e74..c77b0fe3cdd 100644 --- a/homeassistant/components/upcloud/__init__.py +++ b/homeassistant/components/upcloud/__init__.py @@ -1,15 +1,16 @@ """Support for UpCloud.""" -import logging from datetime import timedelta +import logging +import upcloud_api import voluptuous as vol from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL, - STATE_ON, + CONF_USERNAME, STATE_OFF, + STATE_ON, STATE_PROBLEM, ) from homeassistant.core import callback @@ -60,8 +61,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the UpCloud component.""" - import upcloud_api - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) From 86386912b9fd6bc4bf4a0b8729aea74c376c4f83 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sat, 12 Oct 2019 17:53:25 +0200 Subject: [PATCH 174/639] Refactor Tradfri cover (#27413) * Remove unused logging * Refactor cover * Remove method * Fix typo and use consistent wording for gateway * Revert changes --- homeassistant/components/tradfri/cover.py | 106 ++++----------------- homeassistant/components/tradfri/sensor.py | 3 - 2 files changed, 16 insertions(+), 93 deletions(-) diff --git a/homeassistant/components/tradfri/cover.py b/homeassistant/components/tradfri/cover.py index 1a3bf841665..9b831dce0ec 100644 --- a/homeassistant/components/tradfri/cover.py +++ b/homeassistant/components/tradfri/cover.py @@ -1,7 +1,4 @@ """Support for IKEA Tradfri covers.""" -import logging - -from pytradfri.error import PytradfriError from homeassistant.components.cover import ( CoverDevice, @@ -10,10 +7,8 @@ from homeassistant.components.cover import ( SUPPORT_CLOSE, SUPPORT_SET_POSITION, ) -from homeassistant.core import callback -from .const import DOMAIN, KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID - -_LOGGER = logging.getLogger(__name__) +from .base_class import TradfriBaseDevice +from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID async def async_setup_entry(hass, config_entry, async_add_entities): @@ -29,120 +24,51 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(TradfriCover(cover, api, gateway_id) for cover in covers) -class TradfriCover(CoverDevice): +class TradfriCover(TradfriBaseDevice, CoverDevice): """The platform class required by Home Assistant.""" - def __init__(self, cover, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a cover.""" - self._api = api - self._unique_id = f"{gateway_id}-{cover.id}" - self._cover = None - self._cover_control = None - self._cover_data = None - self._name = None - self._available = True - self._gateway_id = gateway_id + super().__init__(device, api, gateway_id) + self._unique_id = f"{gateway_id}-{device.id}" - self._refresh(cover) + self._refresh(device) @property def supported_features(self): """Flag supported features.""" return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - @property - def unique_id(self): - """Return unique ID for cover.""" - return self._unique_id - - @property - def device_info(self): - """Return the device info.""" - info = self._cover.device_info - - return { - "identifiers": {(DOMAIN, self._cover.id)}, - "name": self._name, - "manufacturer": info.manufacturer, - "model": info.model_number, - "sw_version": info.firmware_version, - "via_device": (DOMAIN, self._gateway_id), - } - - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() - - @property - def available(self): - """Return True if entity is available.""" - return self._available - - @property - def should_poll(self): - """No polling needed for tradfri cover.""" - return False - - @property - def name(self): - """Return the display name of this cover.""" - return self._name - @property def current_cover_position(self): """Return current position of cover. None is unknown, 0 is closed, 100 is fully open. """ - return 100 - self._cover_data.current_cover_position + return 100 - self._device_data.current_cover_position async def async_set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - await self._api(self._cover_control.set_state(100 - kwargs[ATTR_POSITION])) + await self._api(self._device_control.set_state(100 - kwargs[ATTR_POSITION])) async def async_open_cover(self, **kwargs): """Open the cover.""" - await self._api(self._cover_control.set_state(0)) + await self._api(self._device_control.set_state(0)) async def async_close_cover(self, **kwargs): """Close cover.""" - await self._api(self._cover_control.set_state(100)) + await self._api(self._device_control.set_state(100)) @property def is_closed(self): """Return if the cover is closed or not.""" return self.current_cover_position == 0 - @callback - def _async_start_observe(self, exc=None): - """Start observation of cover.""" - if exc: - self._available = False - self.async_schedule_update_ha_state() - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - try: - cmd = self._cover.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, cover): + def _refresh(self, device): """Refresh the cover data.""" - self._cover = cover + super()._refresh(device) + self._device = device # Caching of BlindControl and cover object - self._available = cover.reachable - self._cover_control = cover.blind_control - self._cover_data = cover.blind_control.blinds[0] - self._name = cover.name - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this cover.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() + self._device_control = device.blind_control + self._device_data = device.blind_control.blinds[0] diff --git a/homeassistant/components/tradfri/sensor.py b/homeassistant/components/tradfri/sensor.py index 56c1a464580..68a2c10291b 100644 --- a/homeassistant/components/tradfri/sensor.py +++ b/homeassistant/components/tradfri/sensor.py @@ -1,12 +1,9 @@ """Support for IKEA Tradfri sensors.""" -import logging from homeassistant.const import DEVICE_CLASS_BATTERY from .base_class import TradfriBaseDevice from .const import KEY_GATEWAY, KEY_API, CONF_GATEWAY_ID -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Tradfri config entry.""" From 96d35379f286e465ac2383fd0c7d436092ab9427 Mon Sep 17 00:00:00 2001 From: Rolf K Date: Sat, 12 Oct 2019 20:46:09 +0200 Subject: [PATCH 175/639] Add improved scene support to input number integration (#27530) * Added improved scene support to the input_number integration. * Minor fix in test. * Use snake case for variable names in test_reproduce_state. * Remove redundant tests. --- .../input_number/reproduce_state.py | 52 ++++++++++++++++ .../input_number/test_reproduce_state.py | 62 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 homeassistant/components/input_number/reproduce_state.py create mode 100644 tests/components/input_number/test_reproduce_state.py diff --git a/homeassistant/components/input_number/reproduce_state.py b/homeassistant/components/input_number/reproduce_state.py new file mode 100644 index 00000000000..97a4837d371 --- /dev/null +++ b/homeassistant/components/input_number/reproduce_state.py @@ -0,0 +1,52 @@ +"""Reproduce an Input number state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + try: + float(state.state) + except ValueError: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service = SERVICE_SET_VALUE + service_data = {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: state.state} + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input number states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/input_number/test_reproduce_state.py b/tests/components/input_number/test_reproduce_state.py new file mode 100644 index 00000000000..37ab83f3204 --- /dev/null +++ b/tests/components/input_number/test_reproduce_state.py @@ -0,0 +1,62 @@ +"""Test reproduce state for Input number.""" +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +VALID_NUMBER1 = "19.0" +VALID_NUMBER2 = "99.9" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input number states.""" + + assert await async_setup_component( + hass, + "input_number", + { + "input_number": { + "test_number": {"min": "5", "max": "100", "initial": VALID_NUMBER1} + } + }, + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("input_number.test_number", VALID_NUMBER1), + # Should not raise + State("input_number.non_existing", "234"), + ], + blocking=True, + ) + + assert hass.states.get("input_number.test_number").state == VALID_NUMBER1 + + # Test reproducing with different state + await hass.helpers.state.async_reproduce_state( + [ + State("input_number.test_number", VALID_NUMBER2), + # Should not raise + State("input_number.non_existing", "234"), + ], + blocking=True, + ) + + assert hass.states.get("input_number.test_number").state == VALID_NUMBER2 + + # Test setting state to number out of range + await hass.helpers.state.async_reproduce_state( + [State("input_number.test_number", "150")], blocking=True + ) + + # The entity states should be unchanged after trying to set them to out-of-range number + assert hass.states.get("input_number.test_number").state == VALID_NUMBER2 + + await hass.helpers.state.async_reproduce_state( + [ + # Test invalid state + State("input_number.test_number", "invalid_state"), + # Set to state it already is. + State("input_number.test_number", VALID_NUMBER2), + ], + blocking=True, + ) From ee8b72fb71bcfdd4610d1acf77aef1d3604ccf92 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:27:27 +0200 Subject: [PATCH 176/639] Move imports in http component (#27474) --- homeassistant/components/http/cors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 39ff45fd4e4..bd821335542 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,4 +1,5 @@ """Provide CORS support for the HTTP component.""" +import aiohttp_cors from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION @@ -22,8 +23,6 @@ VALID_CORS_TYPES = (Resource, ResourceRoute, StaticResource) @callback def setup_cors(app, origins): """Set up CORS.""" - import aiohttp_cors - cors = aiohttp_cors.setup( app, defaults={ From 42691b783eb5038bed439a3f1bb450258ab73799 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Oct 2019 21:28:47 +0200 Subject: [PATCH 177/639] Handle empty service in script action gracefully (#27467) * Handle empty service in script action gracefully * Add test --- homeassistant/helpers/config_validation.py | 1 + tests/components/automation/test_init.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 8598b50f140..7ca5a7e86f9 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -386,6 +386,7 @@ def remove_falsy(value: List[T]) -> List[T]: def service(value): """Validate service.""" # Services use same format as entities so we can use same helper. + value = string(value).lower() if valid_entity_id(value): return value raise vol.Invalid("Service {} does not match format .".format(value)) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 6acb40cec88..a0573ce7c1b 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -842,6 +842,25 @@ async def test_automation_with_error_in_script(hass, caplog): assert "Service not found" in caplog.text +async def test_automation_with_error_in_script_2(hass, caplog): + """Test automation with an error in script.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "alias": "hello", + "trigger": {"platform": "event", "event_type": "test_event"}, + "action": {"service": None, "entity_id": "hello.world"}, + } + }, + ) + + hass.bus.async_fire("test_event") + await hass.async_block_till_done() + assert "string value is None" in caplog.text + + async def test_automation_restore_last_triggered_with_initial_state(hass): """Ensure last_triggered is restored, even when initial state is set.""" time = dt_util.utcnow() From 3873a1b07020b95d0721b2ecc47ef23539883d42 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:35:39 +0200 Subject: [PATCH 178/639] moved imports to top level (#27494) --- homeassistant/components/auth/login_flow.py | 3 +-- homeassistant/components/auth/mfa_setup_flow.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 0f5da5d7527..4fa0f866124 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -68,6 +68,7 @@ associate with an credential if "type" set to "link_user" in """ from aiohttp import web import voluptuous as vol +import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components.http import KEY_REAL_IP @@ -120,8 +121,6 @@ def _prepare_result_json(result): if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result - import voluptuous_serialize - data = result.copy() schema = data["data_schema"] diff --git a/homeassistant/components/auth/mfa_setup_flow.py b/homeassistant/components/auth/mfa_setup_flow.py index 42dab7ebb5a..271e9ae1634 100644 --- a/homeassistant/components/auth/mfa_setup_flow.py +++ b/homeassistant/components/auth/mfa_setup_flow.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components import websocket_api @@ -134,8 +135,6 @@ def _prepare_result_json(result): if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result - import voluptuous_serialize - data = result.copy() schema = data["data_schema"] From 40e5beb0ed0c6700a349a4be746f27cfc7da611b Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sat, 12 Oct 2019 21:37:59 +0200 Subject: [PATCH 179/639] Move imports in rfxtrx component (#27549) --- homeassistant/components/rfxtrx/__init__.py | 9 ++------- homeassistant/components/rfxtrx/binary_sensor.py | 4 +--- homeassistant/components/rfxtrx/cover.py | 3 +-- homeassistant/components/rfxtrx/light.py | 4 +--- homeassistant/components/rfxtrx/sensor.py | 5 ++--- homeassistant/components/rfxtrx/switch.py | 4 +--- tests/components/rfxtrx/test_cover.py | 4 +--- tests/components/rfxtrx/test_light.py | 4 +--- tests/components/rfxtrx/test_switch.py | 6 +----- 9 files changed, 11 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 79b3054ecf2..73ee07cfb5f 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -1,7 +1,8 @@ """Support for RFXtrx devices.""" from collections import OrderedDict +import binascii import logging - +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.const import ( @@ -113,9 +114,6 @@ def setup(hass, config): for subscriber in RECEIVED_EVT_SUBSCRIBERS: subscriber(event) - # Try to load the RFXtrx module. - import RFXtrx as rfxtrxmod - device = config[DOMAIN][ATTR_DEVICE] debug = config[DOMAIN][ATTR_DEBUG] dummy_connection = config[DOMAIN][ATTR_DUMMY] @@ -144,8 +142,6 @@ def setup(hass, config): def get_rfx_object(packetid): """Return the RFXObject with the packetid.""" - import RFXtrx as rfxtrxmod - try: binarypacket = bytearray.fromhex(packetid) except ValueError: @@ -167,7 +163,6 @@ def get_pt2262_deviceid(device_id, nb_data_bits): """Extract and return the address bits from a Lighting4/PT2262 packet.""" if nb_data_bits is None: return - import binascii try: data = bytearray.fromhex(device_id) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 8f1c7e6fa55..259f914b408 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -1,6 +1,6 @@ """Support for RFXtrx binary sensors.""" import logging - +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -54,8 +54,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Binary Sensor platform to RFXtrx.""" - import RFXtrx as rfxtrxmod - sensors = [] for packet_id, entity in config[CONF_DEVICES].items(): diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index 3d420981685..7aff22bd124 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -1,4 +1,5 @@ """Support for RFXtrx covers.""" +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -34,8 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx cover.""" - import RFXtrx as rfxtrxmod - covers = rfxtrx.get_devices_from_config(config, RfxtrxCover) add_entities(covers) diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index d2d2e842c0a..82b1407c798 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -1,6 +1,6 @@ """Support for RFXtrx lights.""" import logging - +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -45,8 +45,6 @@ SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" - import RFXtrx as rfxtrxmod - lights = rfxtrx.get_devices_from_config(config, RfxtrxLight) add_entities(lights) diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 5941b00764b..5f6b90b600f 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -1,8 +1,9 @@ """Support for RFXtrx sensors.""" import logging - import voluptuous as vol +from RFXtrx import SensorEvent + from homeassistant.components import rfxtrx from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_NAME @@ -43,8 +44,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RFXtrx platform.""" - from RFXtrx import SensorEvent - sensors = [] for packet_id, entity_info in config[CONF_DEVICES].items(): event = rfxtrx.get_rfx_object(packet_id) diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index bb5d5fe6d43..b5c830a298d 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -1,6 +1,6 @@ """Support for RFXtrx switches.""" import logging - +import RFXtrx as rfxtrxmod import voluptuous as vol from homeassistant.components import rfxtrx @@ -38,8 +38,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the RFXtrx platform.""" - import RFXtrx as rfxtrxmod - # Add switch from config file switches = rfxtrx.get_devices_from_config(config, RfxtrxSwitch) add_entities_callback(switches) diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index 9fa71bdab67..d2bfb114804 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -1,7 +1,7 @@ """The tests for the Rfxtrx cover platform.""" import unittest - import pytest +import RFXtrx as rfxtrxmod from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core @@ -142,8 +142,6 @@ class TestCoverRfxtrx(unittest.TestCase): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index f3a6bcab1b1..1254a6d6697 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -1,7 +1,7 @@ """The tests for the Rfxtrx light platform.""" import unittest - import pytest +import RFXtrx as rfxtrxmod from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core @@ -109,8 +109,6 @@ class TestLightRfxtrx(unittest.TestCase): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index dc955a198a7..1e39d4afb75 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -1,7 +1,7 @@ """The tests for the Rfxtrx switch platform.""" import unittest - import pytest +import RFXtrx as rfxtrxmod from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core @@ -166,8 +166,6 @@ class TestSwitchRfxtrx(unittest.TestCase): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) @@ -200,8 +198,6 @@ class TestSwitchRfxtrx(unittest.TestCase): }, ) - import RFXtrx as rfxtrxmod - rfxtrx_core.RFXOBJECT = rfxtrxmod.Core( "", transport_protocol=rfxtrxmod.DummyTransport ) From ddeac071b32d9d25db5e43d75d050f5a36fe6e0a Mon Sep 17 00:00:00 2001 From: Moritz Fey Date: Sat, 12 Oct 2019 21:38:39 +0200 Subject: [PATCH 180/639] fill services.yaml for downloader (#27553) --- homeassistant/components/downloader/services.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/downloader/services.yaml b/homeassistant/components/downloader/services.yaml index e69de29bb2d..d16b2788c70 100644 --- a/homeassistant/components/downloader/services.yaml +++ b/homeassistant/components/downloader/services.yaml @@ -0,0 +1,15 @@ +download_file: + description: Downloads a file to the download location. + fields: + url: + description: The URL of the file to download. + example: 'http://example.org/myfile' + subdir: + description: Download into subdirectory. + example: 'download_dir' + filename: + description: Determine the filename. + example: 'my_file_name' + overwrite: + description: Whether to overwrite the file or not. + example: 'false' \ No newline at end of file From 5030be274a36d5c0f166aa408d9cc933951a1a01 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 21:43:06 +0200 Subject: [PATCH 181/639] Add test to Homematic IP Cloud weather (#27536) --- .../homematicip_cloud/test_weather.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/components/homematicip_cloud/test_weather.py diff --git a/tests/components/homematicip_cloud/test_weather.py b/tests/components/homematicip_cloud/test_weather.py new file mode 100644 index 00000000000..0b5d59215bb --- /dev/null +++ b/tests/components/homematicip_cloud/test_weather.py @@ -0,0 +1,82 @@ +"""Tests for HomematicIP Cloud weather.""" +from homeassistant.components.weather import ( + ATTR_WEATHER_ATTRIBUTION, + ATTR_WEATHER_HUMIDITY, + ATTR_WEATHER_TEMPERATURE, + ATTR_WEATHER_WIND_BEARING, + ATTR_WEATHER_WIND_SPEED, +) + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_weather_sensor(hass, default_mock_hap): + """Test HomematicipWeatherSensor.""" + entity_id = "weather.weather_sensor_plus" + entity_name = "Weather Sensor – plus" + device_model = "HmIP-SWO-PL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "" + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 4.3 + assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 97 + assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 15.0 + assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 12.1) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1 + + +async def test_hmip_weather_sensor_pro(hass, default_mock_hap): + """Test HomematicipWeatherSensorPro.""" + entity_id = "weather.wettersensor_pro" + entity_name = "Wettersensor - pro" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "sunny" + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 15.4 + assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 65 + assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 2.6 + assert ha_state.attributes[ATTR_WEATHER_WIND_BEARING] == 295.0 + assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 12.1) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 12.1 + + +async def test_hmip_home_weather(hass, default_mock_hap): + """Test HomematicipHomeWeather.""" + entity_id = "weather.weather_1010_wien_osterreich" + entity_name = "Weather 1010 Wien, Österreich" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + assert hmip_device + assert ha_state.state == "partlycloudy" + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 16.6 + assert ha_state.attributes[ATTR_WEATHER_HUMIDITY] == 54 + assert ha_state.attributes[ATTR_WEATHER_WIND_SPEED] == 8.6 + assert ha_state.attributes[ATTR_WEATHER_WIND_BEARING] == 294 + assert ha_state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Powered by Homematic IP" + + await async_manipulate_test_data( + hass, + default_mock_hap.home.weather, + "temperature", + 28.3, + fire_device=default_mock_hap.home, + ) + + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WEATHER_TEMPERATURE] == 28.3 From eb77db6569a352824548be8f5381ab294c06f028 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 21:43:46 +0200 Subject: [PATCH 182/639] Add test to Homematic IP Cloud alarm control panel (#27534) --- .../test_alarm_control_panel.py | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 tests/components/homematicip_cloud/test_alarm_control_panel.py diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py new file mode 100644 index 00000000000..0a68ac6d509 --- /dev/null +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -0,0 +1,130 @@ +"""Tests for HomematicIP Cloud alarm control panel.""" +from homematicip.base.enums import WindowState +from homematicip.group import SecurityZoneGroup + +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) + +from .helper import get_and_check_entity_basics + + +def _get_security_zones(groups): # pylint: disable=W0221 + """Get the security zones.""" + for group in groups: + if isinstance(group, SecurityZoneGroup): + if group.label == "EXTERNAL": + external = group + elif group.label == "INTERNAL": + internal = group + return internal, external + + +async def _async_manipulate_security_zones( + hass, home, internal_active, external_active, window_state +): + """Set new values on hmip security zones.""" + internal_zone, external_zone = _get_security_zones(home.groups) + external_zone.active = external_active + external_zone.windowState = window_state + internal_zone.active = internal_active + + # Just one call to a security zone is required to refresh the ACP. + internal_zone.fire_update_event() + + await hass.async_block_till_done() + + +async def test_hmip_alarm_control_panel(hass, default_mock_hap): + """Test HomematicipAlarmControlPanel.""" + entity_id = "alarm_control_panel.hmip_alarm_control_panel" + entity_name = "HmIP Alarm Control Panel" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "disarmed" + assert not hmip_device + + home = default_mock_hap.home + service_call_counter = len(home.mock_calls) + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 1 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (True, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=True, + external_active=True, + window_state=WindowState.CLOSED, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_AWAY + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 3 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (False, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=False, + external_active=True, + window_state=WindowState.CLOSED, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_ARMED_HOME + + await hass.services.async_call( + "alarm_control_panel", "alarm_disarm", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 5 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (False, False) + await _async_manipulate_security_zones( + hass, + home, + internal_active=False, + external_active=False, + window_state=WindowState.CLOSED, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_DISARMED + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_away", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 7 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (True, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=True, + external_active=True, + window_state=WindowState.OPEN, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED + + await hass.services.async_call( + "alarm_control_panel", "alarm_arm_home", {"entity_id": entity_id}, blocking=True + ) + assert len(home.mock_calls) == service_call_counter + 9 + assert home.mock_calls[-1][0] == "set_security_zones_activation" + assert home.mock_calls[-1][1] == (False, True) + await _async_manipulate_security_zones( + hass, + home, + internal_active=False, + external_active=True, + window_state=WindowState.OPEN, + ) + assert hass.states.get(entity_id).state is STATE_ALARM_TRIGGERED From bb1be5327e98cf6fed2137102d815e2b2b677cc4 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 21:44:13 +0200 Subject: [PATCH 183/639] Add test to Homematic IP Cloud cover (#27535) --- .../homematicip_cloud/test_cover.py | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 tests/components/homematicip_cloud/test_cover.py diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py new file mode 100644 index 00000000000..7bfb842a0df --- /dev/null +++ b/tests/components/homematicip_cloud/test_cover.py @@ -0,0 +1,141 @@ +"""Tests for HomematicIP Cloud cover.""" +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, +) +from homeassistant.const import STATE_CLOSED, STATE_OPEN + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_cover_shutter(hass, default_mock_hap): + """Test HomematicipCoverShutte.""" + entity_id = "cover.sofa_links" + entity_name = "Sofa links" + device_model = "HmIP-FBL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "closed" + assert ha_state.attributes["current_position"] == 0 + assert ha_state.attributes["current_tilt_position"] == 0 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_shutter_level" + assert hmip_device.mock_calls[-1][1] == (0,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": entity_id, "position": "50"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "set_shutter_level" + assert hmip_device.mock_calls[-1][1] == (0.5,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 50 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 5 + assert hmip_device.mock_calls[-1][0] == "set_shutter_level" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 7 + assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_CLOSED + + +async def test_hmip_cover_slats(hass, default_mock_hap): + """Test HomematicipCoverSlats.""" + entity_id = "cover.sofa_links" + entity_name = "Sofa links" + device_model = "HmIP-FBL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_CLOSED + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 0 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "cover", "open_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (0,) + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", 0) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 100 + + await hass.services.async_call( + "cover", + "set_cover_tilt_position", + {"entity_id": entity_id, "tilt_position": "50"}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 4 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (0.5,) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 0.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 50 + + await hass.services.async_call( + "cover", "close_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 6 + assert hmip_device.mock_calls[-1][0] == "set_slats_level" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "slatsLevel", 1) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN + assert ha_state.attributes[ATTR_CURRENT_POSITION] == 100 + assert ha_state.attributes[ATTR_CURRENT_TILT_POSITION] == 0 + + await hass.services.async_call( + "cover", "stop_cover_tilt", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 8 + assert hmip_device.mock_calls[-1][0] == "set_shutter_stop" + assert hmip_device.mock_calls[-1][1] == () + + await async_manipulate_test_data(hass, hmip_device, "shutterLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OPEN From 28e3cf29b3d8256a7daa5a919c503f7f1a45faee Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 21:44:19 +0200 Subject: [PATCH 184/639] Add test to Homematic IP Cloud sensor (#27533) --- .../homematicip_cloud/test_sensor.py | 242 ++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 tests/components/homematicip_cloud/test_sensor.py diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py new file mode 100644 index 00000000000..d4307477975 --- /dev/null +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -0,0 +1,242 @@ +"""Tests for HomematicIP Cloud sensor.""" +from homeassistant.components.homematicip_cloud.sensor import ( + ATTR_LEFT_COUNTER, + ATTR_RIGHT_COUNTER, + ATTR_TEMPERATURE_OFFSET, + ATTR_WIND_DIRECTION, + ATTR_WIND_DIRECTION_VARIATION, +) +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_accesspoint_status(hass, default_mock_hap): + """Test HomematicipSwitch.""" + entity_id = "sensor.access_point" + entity_name = "Access Point" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + assert hmip_device + assert ha_state.state == "8.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + + await async_manipulate_test_data(hass, hmip_device, "dutyCycle", 17.3) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == "17.3" + + +async def test_hmip_heating_thermostat(hass, default_mock_hap): + """Test HomematicipHeatingThermostat.""" + entity_id = "sensor.heizkorperthermostat_heating" + entity_name = "Heizkörperthermostat Heating" + device_model = "HMIP-eTRV" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "%" + await async_manipulate_test_data(hass, hmip_device, "valvePosition", 0.37) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "37" + + await async_manipulate_test_data(hass, hmip_device, "valveState", "nn") + ha_state = hass.states.get(entity_id) + assert ha_state.state == "nn" + + +async def test_hmip_humidity_sensor(hass, default_mock_hap): + """Test HomematicipHumiditySensor.""" + entity_id = "sensor.bwth_1_humidity" + entity_name = "BWTH 1 Humidity" + device_model = "HmIP-BWTH" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "40" + assert ha_state.attributes["unit_of_measurement"] == "%" + await async_manipulate_test_data(hass, hmip_device, "humidity", 45) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "45" + + +async def test_hmip_temperature_sensor1(hass, default_mock_hap): + """Test HomematicipTemperatureSensor.""" + entity_id = "sensor.bwth_1_temperature" + entity_name = "BWTH 1 Temperature" + device_model = "HmIP-BWTH" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "21.0" + assert ha_state.attributes["unit_of_measurement"] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + assert not ha_state.attributes.get("temperature_offset") + await async_manipulate_test_data(hass, hmip_device, "temperatureOffset", 10) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10 + + +async def test_hmip_temperature_sensor2(hass, default_mock_hap): + """Test HomematicipTemperatureSensor.""" + entity_id = "sensor.heizkorperthermostat_temperature" + entity_name = "Heizkörperthermostat Temperature" + device_model = "HMIP-eTRV" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "20.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS + await async_manipulate_test_data(hass, hmip_device, "valveActualTemperature", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + assert not ha_state.attributes.get(ATTR_TEMPERATURE_OFFSET) + await async_manipulate_test_data(hass, hmip_device, "temperatureOffset", 10) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_TEMPERATURE_OFFSET] == 10 + + +async def test_hmip_power_sensor(hass, default_mock_hap): + """Test HomematicipPowerSensor.""" + entity_id = "sensor.flur_oben_power" + entity_name = "Flur oben Power" + device_model = "HmIP-BSM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "0.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == POWER_WATT + await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 23.5) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "23.5" + + +async def test_hmip_illuminance_sensor1(hass, default_mock_hap): + """Test HomematicipIlluminanceSensor.""" + entity_id = "sensor.wettersensor_illuminance" + entity_name = "Wettersensor Illuminance" + device_model = "HmIP-SWO-B" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "4890.0" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx" + await async_manipulate_test_data(hass, hmip_device, "illumination", 231) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "231" + + +async def test_hmip_illuminance_sensor2(hass, default_mock_hap): + """Test HomematicipIlluminanceSensor.""" + entity_id = "sensor.lichtsensor_nord_illuminance" + entity_name = "Lichtsensor Nord Illuminance" + device_model = "HmIP-SLO" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "807.3" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "lx" + await async_manipulate_test_data(hass, hmip_device, "averageIllumination", 231) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "231" + + +async def test_hmip_windspeed_sensor(hass, default_mock_hap): + """Test HomematicipWindspeedSensor.""" + entity_id = "sensor.wettersensor_pro_windspeed" + entity_name = "Wettersensor - pro Windspeed" + device_model = "HmIP-SWO-PR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "2.6" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "km/h" + await async_manipulate_test_data(hass, hmip_device, "windSpeed", 9.4) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "9.4" + + assert ha_state.attributes[ATTR_WIND_DIRECTION_VARIATION] == 56.25 + assert ha_state.attributes[ATTR_WIND_DIRECTION] == "WNW" + + wind_directions = { + 25: "NNE", + 37.5: "NE", + 70: "ENE", + 92.5: "E", + 115: "ESE", + 137.5: "SE", + 160: "SSE", + 182.5: "S", + 205: "SSW", + 227.5: "SW", + 250: "WSW", + 272.5: "W", + 295: "WNW", + 317.5: "NW", + 340: "NNW", + 0: "N", + } + + for direction, txt in wind_directions.items(): + await async_manipulate_test_data(hass, hmip_device, "windDirection", direction) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_WIND_DIRECTION] == txt + + +async def test_hmip_today_rain_sensor(hass, default_mock_hap): + """Test HomematicipTodayRainSensor.""" + entity_id = "sensor.weather_sensor_plus_today_rain" + entity_name = "Weather Sensor – plus Today Rain" + device_model = "HmIP-SWO-PL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "3.9" + assert ha_state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "mm" + await async_manipulate_test_data(hass, hmip_device, "todayRainCounter", 14.2) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "14.2" + + +async def test_hmip_passage_detector_delta_counter(hass, default_mock_hap): + """Test HomematicipPassageDetectorDeltaCounter.""" + entity_id = "sensor.spdr_1" + entity_name = "SPDR_1" + device_model = "HmIP-SPDR" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == "164" + assert ha_state.attributes[ATTR_LEFT_COUNTER] == 966 + assert ha_state.attributes[ATTR_RIGHT_COUNTER] == 802 + await async_manipulate_test_data(hass, hmip_device, "leftRightCounterDelta", 190) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "190" From 6317ef13245602238d07d570f4fe23bb6f69f82d Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:44:47 +0200 Subject: [PATCH 185/639] moved imports to top level (#27512) --- homeassistant/components/bt_home_hub_5/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bt_home_hub_5/device_tracker.py b/homeassistant/components/bt_home_hub_5/device_tracker.py index 0a068a3981f..20ad909c44e 100644 --- a/homeassistant/components/bt_home_hub_5/device_tracker.py +++ b/homeassistant/components/bt_home_hub_5/device_tracker.py @@ -1,6 +1,8 @@ """Support for BT Home Hub 5.""" import logging +import bthomehub5_devicelist + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -32,7 +34,6 @@ class BTHomeHub5DeviceScanner(DeviceScanner): def __init__(self, config): """Initialise the scanner.""" - import bthomehub5_devicelist _LOGGER.info("Initialising BT Home Hub 5") self.host = config[CONF_HOST] @@ -61,7 +62,6 @@ class BTHomeHub5DeviceScanner(DeviceScanner): def update_info(self): """Ensure the information from the BT Home Hub 5 is up to date.""" - import bthomehub5_devicelist _LOGGER.info("Scanning") From f979eca83ac364e4c5a334dcc41efd2974dbea6d Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 21:45:11 +0200 Subject: [PATCH 186/639] Add test to Homematic IP Cloud climate (#27472) --- .../components/homematicip_cloud/hap.py | 1 + tests/components/homematicip_cloud/helper.py | 21 +- .../homematicip_cloud/test_climate.py | 230 ++++++++++++++++++ .../homematicip_cloud/test_device.py | 111 +++++++++ 4 files changed, 360 insertions(+), 3 deletions(-) create mode 100644 tests/components/homematicip_cloud/test_climate.py create mode 100644 tests/components/homematicip_cloud/test_device.py diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 22ab1fd617c..f6727f91c7e 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -220,6 +220,7 @@ class HomematicipHAP: await self.hass.config_entries.async_forward_entry_unload( self.config_entry, component ) + self.hmip_device_by_entity_id = {} return True async def get_hap( diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index b5e41a6ae86..e5c5c4569d7 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -35,6 +35,7 @@ def get_and_check_entity_basics( assert ha_state.name == entity_name hmip_device = default_mock_hap.hmip_device_by_entity_id.get(entity_id) + if hmip_device: if isinstance(hmip_device, AsyncDevice): assert ha_state.attributes[ATTR_IS_GROUP] is False @@ -85,14 +86,20 @@ class HomeTemplate(Home): super().__init__(connection=connection) self.label = "Access Point" self.model_type = "HmIP-HAP" + self.init_json_state = None def init_home(self, json_path=HOME_JSON): """Init template with json.""" - json_state = json.loads(load_fixture(HOME_JSON), encoding="UTF-8") - self.update_home(json_state=json_state, clearConfig=True) - self._generate_mocks() + self.init_json_state = json.loads(load_fixture(HOME_JSON), encoding="UTF-8") + self.update_home(json_state=self.init_json_state, clearConfig=True) return self + def update_home(self, json_state, clearConfig: bool = False): + """Update home and ensure that mocks are created.""" + result = super().update_home(json_state, clearConfig) + self._generate_mocks() + return result + def _generate_mocks(self): """Generate mocks for groups and devices.""" mock_devices = [] @@ -105,6 +112,10 @@ class HomeTemplate(Home): mock_groups.append(_get_mock(group)) self.groups = mock_groups + def download_configuration(self): + """Return the initial json config.""" + return self.init_json_state + def get_async_home_mock(self): """ Create Mock for Async_Home. based on template to be used for testing. @@ -123,6 +134,10 @@ class HomeTemplate(Home): def _get_mock(instance): """Create a mock and copy instance attributes over mock.""" + if isinstance(instance, Mock): + instance.__dict__.update(instance._mock_wraps.__dict__) # pylint: disable=W0212 + return instance + mock = Mock(spec=instance, wraps=instance) mock.__dict__.update(instance.__dict__) return mock diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py new file mode 100644 index 00000000000..8f8a681fad8 --- /dev/null +++ b/tests/components/homematicip_cloud/test_climate.py @@ -0,0 +1,230 @@ +"""Tests for HomematicIP Cloud climate.""" +import datetime + +from homematicip.base.enums import AbsenceType +from homematicip.functionalHomes import IndoorClimateHome + +from homeassistant.components.climate.const import ( + ATTR_CURRENT_TEMPERATURE, + ATTR_PRESET_MODE, + ATTR_PRESET_MODES, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT, + PRESET_AWAY, + PRESET_BOOST, + PRESET_ECO, + PRESET_NONE, +) + +from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_heating_group(hass, default_mock_hap): + """Test HomematicipHeatingGroup.""" + entity_id = "climate.badezimmer" + entity_name = "Badezimmer" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == HVAC_MODE_AUTO + assert ha_state.attributes["current_temperature"] == 23.8 + assert ha_state.attributes["min_temp"] == 5.0 + assert ha_state.attributes["max_temp"] == 30.0 + assert ha_state.attributes["temperature"] == 5.0 + assert ha_state.attributes["current_humidity"] == 47 + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_NONE, PRESET_BOOST] + + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": entity_id, "temperature": 22.5}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "set_point_temperature" + assert hmip_device.mock_calls[-1][1] == (22.5,) + await async_manipulate_test_data(hass, hmip_device, "actualTemperature", 22.5) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_HEAT}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("MANUAL",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "MANUAL") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_HEAT + + await hass.services.async_call( + "climate", + "set_hvac_mode", + {"entity_id": entity_id, "hvac_mode": HVAC_MODE_AUTO}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 5 + assert hmip_device.mock_calls[-1][0] == "set_control_mode" + assert hmip_device.mock_calls[-1][1] == ("AUTOMATIC",) + await async_manipulate_test_data(hass, hmip_device, "controlMode", "AUTO") + ha_state = hass.states.get(entity_id) + assert ha_state.state == HVAC_MODE_AUTO + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": PRESET_BOOST}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 7 + assert hmip_device.mock_calls[-1][0] == "set_boost" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "boostMode", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_BOOST + + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": PRESET_NONE}, + blocking=True, + ) + assert len(hmip_device.mock_calls) == service_call_counter + 9 + assert hmip_device.mock_calls[-1][0] == "set_boost" + assert hmip_device.mock_calls[-1][1] == (False,) + await async_manipulate_test_data(hass, hmip_device, "boostMode", False) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + # Not required for hmip, but a posiblity to send no temperature. + await hass.services.async_call( + "climate", + "set_temperature", + {"entity_id": entity_id, "target_temp_low": 10, "target_temp_high": 10}, + blocking=True, + ) + # No new service call should be in mock_calls. + assert len(hmip_device.mock_calls) == service_call_counter + 10 + # Only fire event from last async_manipulate_test_data available. + assert hmip_device.mock_calls[-1][0] == "fire_update_event" + + await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") + await async_manipulate_test_data( + hass, + default_mock_hap.home.get_functionalHome(IndoorClimateHome), + "absenceType", + AbsenceType.VACATION, + fire_device=hmip_device, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY + + await async_manipulate_test_data(hass, hmip_device, "controlMode", "ECO") + await async_manipulate_test_data( + hass, + default_mock_hap.home.get_functionalHome(IndoorClimateHome), + "absenceType", + AbsenceType.PERIOD, + fire_device=hmip_device, + ) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO + + +async def test_hmip_climate_services(hass, mock_hap_with_service): + """Test HomematicipHeatingGroup.""" + + home = mock_hap_with_service.home + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_duration", + {"duration": 60, "accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_absence_with_duration" + assert home.mock_calls[-1][1] == (60,) + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_duration", + {"duration": 60}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_absence_with_duration" + assert home.mock_calls[-1][1] == (60,) + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_period", + {"endtime": "2019-02-17 14:00", "accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_absence_with_period" + assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) + + await hass.services.async_call( + "homematicip_cloud", + "activate_eco_mode_with_period", + {"endtime": "2019-02-17 14:00"}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_absence_with_period" + assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) + + await hass.services.async_call( + "homematicip_cloud", + "activate_vacation", + {"endtime": "2019-02-17 14:00", "temperature": 18.5, "accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_vacation" + assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) + + await hass.services.async_call( + "homematicip_cloud", + "activate_vacation", + {"endtime": "2019-02-17 14:00", "temperature": 18.5}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "activate_vacation" + assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) + + await hass.services.async_call( + "homematicip_cloud", + "deactivate_eco_mode", + {"accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "deactivate_absence" + assert home.mock_calls[-1][1] == () + + await hass.services.async_call( + "homematicip_cloud", "deactivate_eco_mode", blocking=True + ) + assert home.mock_calls[-1][0] == "deactivate_absence" + assert home.mock_calls[-1][1] == () + + await hass.services.async_call( + "homematicip_cloud", + "deactivate_vacation", + {"accesspoint_id": HAPID}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "deactivate_vacation" + assert home.mock_calls[-1][1] == () + + await hass.services.async_call( + "homematicip_cloud", "deactivate_vacation", blocking=True + ) + assert home.mock_calls[-1][0] == "deactivate_vacation" + assert home.mock_calls[-1][1] == () diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py new file mode 100644 index 00000000000..81c35f8e2a9 --- /dev/null +++ b/tests/components/homematicip_cloud/test_device.py @@ -0,0 +1,111 @@ +"""Common tests for HomematicIP devices.""" +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_remove_device(hass, default_mock_hap): + """Test Remove of hmip device.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + device_registry = await dr.async_get_registry(hass) + entity_registry = await er.async_get_registry(hass) + + pre_device_count = len(device_registry.devices) + pre_entity_count = len(entity_registry.entities) + pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id) + + hmip_device.fire_remove_event() + + await hass.async_block_till_done() + + assert len(device_registry.devices) == pre_device_count - 1 + assert len(entity_registry.entities) == pre_entity_count - 3 + assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 3 + + +async def test_hmip_remove_group(hass, default_mock_hap): + """Test Remove of hmip group.""" + entity_id = "switch.strom_group" + entity_name = "Strom Group" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + device_registry = await dr.async_get_registry(hass) + entity_registry = await er.async_get_registry(hass) + + pre_device_count = len(device_registry.devices) + pre_entity_count = len(entity_registry.entities) + pre_mapping_count = len(default_mock_hap.hmip_device_by_entity_id) + + hmip_device.fire_remove_event() + + await hass.async_block_till_done() + + assert len(device_registry.devices) == pre_device_count + assert len(entity_registry.entities) == pre_entity_count - 1 + assert len(default_mock_hap.hmip_device_by_entity_id) == pre_mapping_count - 1 + + +async def test_all_devices_unavailable_when_hap_not_connected(hass, default_mock_hap): + """Test make all devices unavaulable when hap is not connected.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + assert default_mock_hap.home.connected + + await async_manipulate_test_data(hass, default_mock_hap.home, "connected", False) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNAVAILABLE + + +async def test_hap_reconnected(hass, default_mock_hap): + """Test reconnect hap.""" + entity_id = "light.treppe" + entity_name = "Treppe" + device_model = "HmIP-BSL" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + assert hmip_device + + assert default_mock_hap.home.connected + + await async_manipulate_test_data(hass, default_mock_hap.home, "connected", False) + + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_UNAVAILABLE + + default_mock_hap._accesspoint_connected = False # pylint: disable=W0212 + await async_manipulate_test_data(hass, default_mock_hap.home, "connected", True) + await hass.async_block_till_done() + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON From ae5cb82908ecad64600daf0b23f9d10180cbad6f Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:45:31 +0200 Subject: [PATCH 187/639] moved imports to top level (#27508) --- homeassistant/components/broadlink/sensor.py | 3 ++- homeassistant/components/broadlink/switch.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/broadlink/sensor.py b/homeassistant/components/broadlink/sensor.py index 98988965ca0..6374f35c503 100644 --- a/homeassistant/components/broadlink/sensor.py +++ b/homeassistant/components/broadlink/sensor.py @@ -3,6 +3,8 @@ import binascii import logging from datetime import timedelta +import broadlink + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -128,7 +130,6 @@ class BroadlinkData: _LOGGER.warning("Failed to connect to device") def _connect(self): - import broadlink self._device = broadlink.a1((self.ip_addr, 80), self.mac_addr, None) self._device.timeout = self.timeout diff --git a/homeassistant/components/broadlink/switch.py b/homeassistant/components/broadlink/switch.py index d60331aaa44..bfb6dc4f42e 100644 --- a/homeassistant/components/broadlink/switch.py +++ b/homeassistant/components/broadlink/switch.py @@ -4,6 +4,8 @@ from datetime import timedelta import logging import socket +import broadlink + import voluptuous as vol from homeassistant.components.switch import ( @@ -91,7 +93,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Broadlink switches.""" - import broadlink devices = config.get(CONF_SWITCHES) slots = config.get("slots", {}) From b9d54de09bb73af3440a7bd83b3f752b9b7c017a Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:45:40 +0200 Subject: [PATCH 188/639] moved imports to top level (#27509) --- homeassistant/components/brottsplatskartan/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/brottsplatskartan/sensor.py b/homeassistant/components/brottsplatskartan/sensor.py index 5a3f72c3ef2..d8592f44fff 100644 --- a/homeassistant/components/brottsplatskartan/sensor.py +++ b/homeassistant/components/brottsplatskartan/sensor.py @@ -4,6 +4,8 @@ from datetime import timedelta import logging import uuid +import brottsplatskartan + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -60,7 +62,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Brottsplatskartan platform.""" - import brottsplatskartan area = config.get(CONF_AREA) latitude = config.get(CONF_LATITUDE, hass.config.latitude) @@ -105,7 +106,6 @@ class BrottsplatskartanSensor(Entity): def update(self): """Update device state.""" - import brottsplatskartan incident_counts = defaultdict(int) incidents = self._brottsplatskartan.get_incidents() From 1dcdc17202045b0bec3071c34efb5de1a64ccf39 Mon Sep 17 00:00:00 2001 From: thaohtp Date: Sat, 12 Oct 2019 21:46:26 +0200 Subject: [PATCH 189/639] Move imports in startca to top-level (#27510) --- homeassistant/components/startca/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/startca/sensor.py b/homeassistant/components/startca/sensor.py index 1b567c58b45..55ae15cede7 100644 --- a/homeassistant/components/startca/sensor.py +++ b/homeassistant/components/startca/sensor.py @@ -1,10 +1,11 @@ """Support for Start.ca Bandwidth Monitor.""" from datetime import timedelta -from xml.parsers.expat import ExpatError import logging -import async_timeout +from xml.parsers.expat import ExpatError +import async_timeout import voluptuous as vol +import xmltodict from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_API_KEY, CONF_MONITORED_VARIABLES, CONF_NAME @@ -138,8 +139,6 @@ class StartcaData: @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the Start.ca bandwidth data from the web service.""" - import xmltodict - _LOGGER.debug("Updating Start.ca usage data") url = "https://www.start.ca/support/usage/api?key=" + self.api_key with async_timeout.timeout(REQUEST_TIMEOUT): From 0331c8453a9a3cd54b1149b32b0531fd54cb4822 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:48:12 +0200 Subject: [PATCH 190/639] moved imports to top level (#27503) --- .../components/bluetooth_le_tracker/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index 29eecdfd077..18edd750639 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -2,6 +2,8 @@ import asyncio import logging +import pygatt # pylint: disable=import-error + from homeassistant.helpers.event import track_point_in_utc_time from homeassistant.components.device_tracker.legacy import ( YAML_DEVICES, @@ -26,8 +28,6 @@ MIN_SEEN_NEW = 5 def setup_scanner(hass, config, see, discovery_info=None): """Set up the Bluetooth LE Scanner.""" - # pylint: disable=import-error - import pygatt new_devices = {} hass.data.setdefault(DATA_BLE, {DATA_BLE_ADAPTER: None}) From b8256316764a6096496bebf6de6326eeaf8a138e Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:48:30 +0200 Subject: [PATCH 191/639] moved imports to top level (#27501) --- homeassistant/components/bh1750/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bh1750/sensor.py b/homeassistant/components/bh1750/sensor.py index 0a305c21adb..cc91fa48bae 100644 --- a/homeassistant/components/bh1750/sensor.py +++ b/homeassistant/components/bh1750/sensor.py @@ -2,6 +2,9 @@ from functools import partial import logging +import smbus # pylint: disable=import-error +from i2csense.bh1750 import BH1750 # pylint: disable=import-error + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -60,8 +63,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BH1750 sensor.""" - import smbus # pylint: disable=import-error - from i2csense.bh1750 import BH1750 # pylint: disable=import-error name = config.get(CONF_NAME) bus_number = config.get(CONF_I2C_BUS) From 4cded9782dc7932c8b88fadad7e333540f4025e3 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:50:30 +0200 Subject: [PATCH 192/639] moved imports to top level (#27498) --- homeassistant/components/axis/device.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 3b91f7e1474..e42a758f3c4 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -3,6 +3,9 @@ import asyncio import async_timeout +import axis +from axis.streammanager import SIGNAL_PLAYING + from homeassistant.const import ( CONF_DEVICE, CONF_HOST, @@ -140,7 +143,6 @@ class AxisNetworkDevice: This is called on every RTSP keep-alive message. Only signal state change if state change is true. """ - from axis.streammanager import SIGNAL_PLAYING if self.available != (status == SIGNAL_PLAYING): self.available = not self.available @@ -198,7 +200,6 @@ class AxisNetworkDevice: async def get_device(hass, config): """Create a Axis device.""" - import axis device = axis.AxisDevice( loop=hass.loop, From 3f9f8eb379c064bc3073e6ecab85242a25ceed07 Mon Sep 17 00:00:00 2001 From: Kevin Fronczak Date: Sat, 12 Oct 2019 15:51:10 -0400 Subject: [PATCH 193/639] Update blink version to 0.14.2 (#27555) * Update blink version to 0.14.2 * Ren gen_requirements_all script --- homeassistant/components/blink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blink/manifest.json b/homeassistant/components/blink/manifest.json index a38ba0bd613..47cded00cc0 100644 --- a/homeassistant/components/blink/manifest.json +++ b/homeassistant/components/blink/manifest.json @@ -3,7 +3,7 @@ "name": "Blink", "documentation": "https://www.home-assistant.io/integrations/blink", "requirements": [ - "blinkpy==0.14.1" + "blinkpy==0.14.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index f1050385929..df024f34cda 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -282,7 +282,7 @@ bimmer_connected==0.6.0 bizkaibus==0.1.1 # homeassistant.components.blink -blinkpy==0.14.1 +blinkpy==0.14.2 # homeassistant.components.blinksticklight blinkstick==1.1.8 From 3ca74373d3f86e6da1252d906829a937a55c0539 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:51:32 +0200 Subject: [PATCH 194/639] moved imports to top level (#27500) --- homeassistant/components/bbox/device_tracker.py | 4 ++-- homeassistant/components/bbox/sensor.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bbox/device_tracker.py b/homeassistant/components/bbox/device_tracker.py index 89449aeab45..122016ecf96 100644 --- a/homeassistant/components/bbox/device_tracker.py +++ b/homeassistant/components/bbox/device_tracker.py @@ -4,6 +4,8 @@ from datetime import timedelta import logging from typing import List +import pybbox + import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -75,8 +77,6 @@ class BboxDeviceScanner(DeviceScanner): """ _LOGGER.info("Scanning...") - import pybbox - box = pybbox.Bbox(ip=self.host) result = box.get_all_connected_devices() diff --git a/homeassistant/components/bbox/sensor.py b/homeassistant/components/bbox/sensor.py index ba38f8d2607..ad6bcc39796 100644 --- a/homeassistant/components/bbox/sensor.py +++ b/homeassistant/components/bbox/sensor.py @@ -3,6 +3,8 @@ import logging from datetime import timedelta import requests +import pybbox + import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -136,7 +138,6 @@ class BboxData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the Bbox.""" - import pybbox try: box = pybbox.Bbox() From 468e6c30b3ee37b54f6135d3890a1f4d66e95a9e Mon Sep 17 00:00:00 2001 From: thaohtp Date: Sat, 12 Oct 2019 21:52:04 +0200 Subject: [PATCH 195/639] Move imports in aruba component to top-level (#27497) Issue: https://github.com/home-assistant/home-assistant/issues/27284 --- homeassistant/components/aruba/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/aruba/device_tracker.py b/homeassistant/components/aruba/device_tracker.py index f93533b6beb..485c731ff6a 100644 --- a/homeassistant/components/aruba/device_tracker.py +++ b/homeassistant/components/aruba/device_tracker.py @@ -2,15 +2,16 @@ import logging import re +import pexpect import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -82,7 +83,6 @@ class ArubaDeviceScanner(DeviceScanner): def get_aruba_data(self): """Retrieve data from Aruba Access Point and return parsed result.""" - import pexpect connect = "ssh {}@{}" ssh = pexpect.spawn(connect.format(self.username, self.host)) From 2c8e24eb14eaccfacb8f3504a36113966e8d6cf7 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:52:19 +0200 Subject: [PATCH 196/639] moved imports to top level (#27496) --- homeassistant/components/aws/__init__.py | 3 ++- homeassistant/components/aws/notify.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/aws/__init__.py b/homeassistant/components/aws/__init__.py index 1959cc05e80..780a65b2d47 100644 --- a/homeassistant/components/aws/__init__.py +++ b/homeassistant/components/aws/__init__.py @@ -3,6 +3,8 @@ import asyncio import logging from collections import OrderedDict +import aiobotocore + import voluptuous as vol from homeassistant import config_entries @@ -151,7 +153,6 @@ async def async_setup_entry(hass, entry): async def _validate_aws_credentials(hass, credential): """Validate AWS credential config.""" - import aiobotocore aws_config = credential.copy() del aws_config[CONF_NAME] diff --git a/homeassistant/components/aws/notify.py b/homeassistant/components/aws/notify.py index fa1cf3fa363..2afa9a3a402 100644 --- a/homeassistant/components/aws/notify.py +++ b/homeassistant/components/aws/notify.py @@ -4,6 +4,8 @@ import base64 import json import logging +import aiobotocore + from homeassistant.components.notify import ( ATTR_TARGET, ATTR_TITLE, @@ -26,7 +28,6 @@ _LOGGER = logging.getLogger(__name__) async def get_available_regions(hass, service): """Get available regions for a service.""" - import aiobotocore session = aiobotocore.get_session() # get_available_regions is not a coroutine since it does not perform @@ -41,8 +42,6 @@ async def async_get_service(hass, config, discovery_info=None): _LOGGER.error("Please config aws notify platform in aws component") return None - import aiobotocore - session = None conf = discovery_info From 8436acbffa581d7d7e5759813fd2f5736b0b919b Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:52:34 +0200 Subject: [PATCH 197/639] moved imports to top level (#27495) --- homeassistant/components/automatic/device_tracker.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/automatic/device_tracker.py b/homeassistant/components/automatic/device_tracker.py index 09cf3f67114..fbb823dd329 100644 --- a/homeassistant/components/automatic/device_tracker.py +++ b/homeassistant/components/automatic/device_tracker.py @@ -6,6 +6,8 @@ import logging import os from aiohttp import web +import aioautomatic + import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -82,7 +84,6 @@ def _write_refresh_token_to_file(hass, filename, refresh_token): @asyncio.coroutine def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return an Automatic scanner.""" - import aioautomatic hass.http.register_view(AutomaticAuthCallbackView()) @@ -215,7 +216,6 @@ class AutomaticData: @asyncio.coroutine def handle_event(self, name, event): """Coroutine to update state for a real time event.""" - import aioautomatic self.hass.bus.async_fire(EVENT_AUTOMATIC_UPDATE, event.data) @@ -261,7 +261,6 @@ class AutomaticData: @asyncio.coroutine def ws_connect(self, now=None): """Open the websocket connection.""" - import aioautomatic self.ws_close_requested = False @@ -321,7 +320,6 @@ class AutomaticData: @asyncio.coroutine def get_vehicle_info(self, vehicle): """Fetch the latest vehicle info from automatic.""" - import aioautomatic name = vehicle.display_name if name is None: From 15820c6751d53df6d1e2c775cf670addfa11dba2 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 12 Oct 2019 21:53:15 +0200 Subject: [PATCH 198/639] Add device condition support to the lock integration (#27488) --- .../components/lock/device_condition.py | 79 +++++++++++ homeassistant/components/lock/strings.json | 8 ++ .../components/lock/test_device_condition.py | 126 ++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 homeassistant/components/lock/device_condition.py create mode 100644 homeassistant/components/lock/strings.json create mode 100644 tests/components/lock/test_device_condition.py diff --git a/homeassistant/components/lock/device_condition.py b/homeassistant/components/lock/device_condition.py new file mode 100644 index 00000000000..328da6ad450 --- /dev/null +++ b/homeassistant/components/lock/device_condition.py @@ -0,0 +1,79 @@ +"""Provides device automations for Lock.""" +from typing import List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_LOCKED, + STATE_UNLOCKED, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +CONDITION_TYPES = {"is_locked", "is_unlocked"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions for Lock devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add conditions for each entity that belongs to this integration + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_locked", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_unlocked", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_locked": + state = STATE_LOCKED + else: + state = STATE_UNLOCKED + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json new file mode 100644 index 00000000000..baa9bc1604f --- /dev/null +++ b/homeassistant/components/lock/strings.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} is locked", + "is_unlocked": "{entity_name} is unlocked" + } + } +} diff --git a/tests/components/lock/test_device_condition.py b/tests/components/lock/test_device_condition.py new file mode 100644 index 00000000000..675f402e770 --- /dev/null +++ b/tests/components/lock/test_device_condition.py @@ -0,0 +1,126 @@ +"""The tests for Lock device conditions.""" +import pytest + +from homeassistant.components.lock import DOMAIN +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a lock.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_locked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_unlocked", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("lock.entity", STATE_LOCKED) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "is_locked", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_locked - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "lock.entity", + "type": "is_unlocked", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_unlocked - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_locked - event - test_event1" + + hass.states.async_set("lock.entity", STATE_UNLOCKED) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_unlocked - event - test_event2" From 5198f522c7137c6d7361935edbfbc6d07c191c6b Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 21:53:43 +0200 Subject: [PATCH 199/639] moved imports to top level (#27483) --- homeassistant/components/aquostv/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aquostv/media_player.py b/homeassistant/components/aquostv/media_player.py index 016db478fc9..d8770592c9f 100644 --- a/homeassistant/components/aquostv/media_player.py +++ b/homeassistant/components/aquostv/media_player.py @@ -1,6 +1,8 @@ """Support for interface with an Aquos TV.""" import logging +import sharp_aquos_rc + import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -77,7 +79,6 @@ SOURCES = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Sharp Aquos TV platform.""" - import sharp_aquos_rc name = config.get(CONF_NAME) port = config.get(CONF_PORT) From 54d63c63c30488a2340c204bd2ae18d9be06faf5 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:54:16 +0200 Subject: [PATCH 200/639] Move imports in uscis component (#27481) --- homeassistant/components/uscis/sensor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/uscis/sensor.py b/homeassistant/components/uscis/sensor.py index bda6ad9041b..3f5175ad09d 100644 --- a/homeassistant/components/uscis/sensor.py +++ b/homeassistant/components/uscis/sensor.py @@ -1,7 +1,8 @@ """Support for USCIS Case Status.""" - import logging from datetime import timedelta + +import uscisstatus import voluptuous as vol from homeassistant.helpers.entity import Entity @@ -67,8 +68,6 @@ class UscisSensor(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Fetch data from the USCIS website and update state attributes.""" - import uscisstatus - try: status = uscisstatus.get_case_status(self._case_id) self._attributes = {self.CURRENT_STATUS: status["status"]} From 0a2ec30ce37227e111cf78817cca9b4b680f71c5 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:54:41 +0200 Subject: [PATCH 201/639] Move imports in vasttrafik component (#27480) --- homeassistant/components/vasttrafik/sensor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/vasttrafik/sensor.py b/homeassistant/components/vasttrafik/sensor.py index 0da730165fe..d13383a0832 100644 --- a/homeassistant/components/vasttrafik/sensor.py +++ b/homeassistant/components/vasttrafik/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import vasttrafik import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -54,7 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the departure sensor.""" - import vasttrafik planner = vasttrafik.JournyPlanner(config.get(CONF_KEY), config.get(CONF_SECRET)) sensors = [] @@ -62,7 +62,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for departure in config.get(CONF_DEPARTURES): sensors.append( VasttrafikDepartureSensor( - vasttrafik, planner, departure.get(CONF_NAME), departure.get(CONF_FROM), @@ -77,9 +76,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class VasttrafikDepartureSensor(Entity): """Implementation of a Vasttrafik Departure Sensor.""" - def __init__(self, vasttrafik, planner, name, departure, heading, lines, delay): + def __init__(self, planner, name, departure, heading, lines, delay): """Initialize the sensor.""" - self._vasttrafik = vasttrafik self._planner = planner self._name = name or departure self._departure = planner.location_name(departure)[0] @@ -119,7 +117,7 @@ class VasttrafikDepartureSensor(Entity): direction=self._heading["id"] if self._heading else None, date=now() + self._delay, ) - except self._vasttrafik.Error: + except vasttrafik.Error: _LOGGER.debug("Unable to read departure board, updating token") self._planner.update_token() From c5b12d6006fbb10220a636b8e5d1826e60598071 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:54:55 +0200 Subject: [PATCH 202/639] Move imports in venstar component (#27478) --- homeassistant/components/venstar/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index 7be31d56c08..81afef97541 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -1,6 +1,7 @@ """Support for Venstar WiFi Thermostats.""" import logging +from venstarcolortouch import VenstarColorTouch import voluptuous as vol from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA @@ -71,7 +72,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Venstar thermostat.""" - import venstarcolortouch username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -84,7 +84,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): else: proto = "http" - client = venstarcolortouch.VenstarColorTouch( + client = VenstarColorTouch( addr=host, timeout=timeout, user=username, password=password, proto=proto ) From eaf855286bfb0079d04b9c9ff5dc72f4786c0c78 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:55:33 +0200 Subject: [PATCH 203/639] Move imports in verisure component (#27476) --- homeassistant/components/verisure/__init__.py | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index df95ed07ca5..f4313c7c1ac 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -3,6 +3,8 @@ import logging import threading from datetime import timedelta +from jsonpath import jsonpath +import verisure import voluptuous as vol from homeassistant.const import ( @@ -71,10 +73,8 @@ CAPTURE_IMAGE_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_SERIAL): cv.string}) def setup(hass, config): """Set up the Verisure component.""" - import verisure - global HUB - HUB = VerisureHub(config[DOMAIN], verisure) + HUB = VerisureHub(config[DOMAIN]) HUB.update_overview = Throttle(config[DOMAIN][CONF_SCAN_INTERVAL])( HUB.update_overview ) @@ -109,13 +109,12 @@ def setup(hass, config): class VerisureHub: """A Verisure hub wrapper class.""" - def __init__(self, domain_config, verisure): + def __init__(self, domain_config): """Initialize the Verisure hub.""" self.overview = {} self.imageseries = {} self.config = domain_config - self._verisure = verisure self._lock = threading.Lock() @@ -125,15 +124,11 @@ class VerisureHub: self.giid = domain_config.get(CONF_GIID) - import jsonpath - - self.jsonpath = jsonpath.jsonpath - def login(self): """Login to Verisure.""" try: self.session.login() - except self._verisure.Error as ex: + except verisure.Error as ex: _LOGGER.error("Could not log in to verisure, %s", ex) return False if self.giid: @@ -144,7 +139,7 @@ class VerisureHub: """Logout from Verisure.""" try: self.session.logout() - except self._verisure.Error as ex: + except verisure.Error as ex: _LOGGER.error("Could not log out from verisure, %s", ex) return False return True @@ -153,7 +148,7 @@ class VerisureHub: """Set installation GIID.""" try: self.session.set_giid(self.giid) - except self._verisure.Error as ex: + except verisure.Error as ex: _LOGGER.error("Could not set installation GIID, %s", ex) return False return True @@ -162,7 +157,7 @@ class VerisureHub: """Update the overview.""" try: self.overview = self.session.get_overview() - except self._verisure.ResponseError as ex: + except verisure.ResponseError as ex: _LOGGER.error("Could not read overview, %s", ex) if ex.status_code == 503: # Service unavailable _LOGGER.info("Trying to log in again") @@ -182,7 +177,7 @@ class VerisureHub: def get(self, jpath, *args): """Get values from the overview that matches the jsonpath.""" - res = self.jsonpath(self.overview, jpath % args) + res = jsonpath(self.overview, jpath % args) return res if res else [] def get_first(self, jpath, *args): @@ -192,5 +187,5 @@ class VerisureHub: def get_image_info(self, jpath, *args): """Get values from the imageseries that matches the jsonpath.""" - res = self.jsonpath(self.imageseries, jpath % args) + res = jsonpath(self.imageseries, jpath % args) return res if res else [] From bac337889f993ea3d5bb75a52c3982060b434b7b Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 21:55:40 +0200 Subject: [PATCH 204/639] Move imports in vera component (#27477) --- homeassistant/components/vera/__init__.py | 3 +-- homeassistant/components/vera/sensor.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 5d9dd80061c..8fcc8a4a2fe 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -2,6 +2,7 @@ import logging from collections import defaultdict +import pyvera as veraApi import voluptuous as vol from requests.exceptions import RequestException @@ -65,7 +66,6 @@ VERA_COMPONENTS = [ def setup(hass, base_config): """Set up for Vera devices.""" - import pyvera as veraApi def stop_subscription(event): """Shutdown Vera subscriptions and subscription thread on exit.""" @@ -118,7 +118,6 @@ def setup(hass, base_config): def map_vera_device(vera_device, remap): """Map vera classes to Home Assistant types.""" - import pyvera as veraApi if isinstance(vera_device, veraApi.VeraDimmer): return "light" diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index c33187cd904..e409a123887 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import pyvera as veraApi + from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.entity import Entity @@ -44,7 +46,6 @@ class VeraSensor(VeraDevice, Entity): @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" - import pyvera as veraApi if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: return self._temperature_units @@ -59,7 +60,6 @@ class VeraSensor(VeraDevice, Entity): def update(self): """Update the state.""" - import pyvera as veraApi if self.vera_device.category == veraApi.CATEGORY_TEMPERATURE_SENSOR: self.current_value = self.vera_device.temperature From 64f9ecbac9ce74f95c3bbf79b3754a7f925d80e5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Oct 2019 12:56:10 -0700 Subject: [PATCH 205/639] Remove incorrect translation folder --- .../components/.translations/airly.ca.json | 22 ------------------- .../components/.translations/airly.da.json | 22 ------------------- .../components/.translations/airly.de.json | 18 --------------- .../components/.translations/airly.en.json | 22 ------------------- .../components/.translations/airly.es.json | 22 ------------------- .../components/.translations/airly.fr.json | 21 ------------------ .../components/.translations/airly.it.json | 22 ------------------- .../components/.translations/airly.lb.json | 22 ------------------- .../components/.translations/airly.nn.json | 10 --------- .../components/.translations/airly.no.json | 22 ------------------- .../components/.translations/airly.pl.json | 22 ------------------- .../components/.translations/airly.ru.json | 22 ------------------- .../components/.translations/airly.sl.json | 22 ------------------- .../.translations/airly.zh-Hant.json | 22 ------------------- 14 files changed, 291 deletions(-) delete mode 100644 homeassistant/components/.translations/airly.ca.json delete mode 100644 homeassistant/components/.translations/airly.da.json delete mode 100644 homeassistant/components/.translations/airly.de.json delete mode 100644 homeassistant/components/.translations/airly.en.json delete mode 100644 homeassistant/components/.translations/airly.es.json delete mode 100644 homeassistant/components/.translations/airly.fr.json delete mode 100644 homeassistant/components/.translations/airly.it.json delete mode 100644 homeassistant/components/.translations/airly.lb.json delete mode 100644 homeassistant/components/.translations/airly.nn.json delete mode 100644 homeassistant/components/.translations/airly.no.json delete mode 100644 homeassistant/components/.translations/airly.pl.json delete mode 100644 homeassistant/components/.translations/airly.ru.json delete mode 100644 homeassistant/components/.translations/airly.sl.json delete mode 100644 homeassistant/components/.translations/airly.zh-Hant.json diff --git a/homeassistant/components/.translations/airly.ca.json b/homeassistant/components/.translations/airly.ca.json deleted file mode 100644 index bf50b4f23e5..00000000000 --- a/homeassistant/components/.translations/airly.ca.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "La clau API no \u00e9s correcta.", - "name_exists": "El nom ja existeix.", - "wrong_location": "No hi ha estacions de mesura Airly en aquesta zona." - }, - "step": { - "user": { - "data": { - "api_key": "Clau API d'Airly", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nom de la integraci\u00f3" - }, - "description": "Configura una integraci\u00f3 de qualitat d\u2019aire Airly. Per generar la clau API, v\u00e9s a https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.da.json b/homeassistant/components/.translations/airly.da.json deleted file mode 100644 index 652cc46a7b3..00000000000 --- a/homeassistant/components/.translations/airly.da.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "API-n\u00f8glen er ikke korrekt.", - "name_exists": "Navnet findes allerede.", - "wrong_location": "Ingen Airly m\u00e5lestationer i dette omr\u00e5de." - }, - "step": { - "user": { - "data": { - "api_key": "Airly API-n\u00f8gle", - "latitude": "Breddegrad", - "longitude": "L\u00e6ngdegrad", - "name": "Integrationens navn" - }, - "description": "Konfigurer Airly luftkvalitet integration. For at generere API-n\u00f8gle, g\u00e5 til https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.de.json b/homeassistant/components/.translations/airly.de.json deleted file mode 100644 index cb290dc46c0..00000000000 --- a/homeassistant/components/.translations/airly.de.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "config": { - "error": { - "name_exists": "Name existiert bereits" - }, - "step": { - "user": { - "data": { - "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad", - "name": "Name der Integration" - }, - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.en.json b/homeassistant/components/.translations/airly.en.json deleted file mode 100644 index 83284aaeb7b..00000000000 --- a/homeassistant/components/.translations/airly.en.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "API key is not correct.", - "name_exists": "Name already exists.", - "wrong_location": "No Airly measuring stations in this area." - }, - "step": { - "user": { - "data": { - "api_key": "Airly API key", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Name of the integration" - }, - "description": "Set up Airly air quality integration. To generate API key go to https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.es.json b/homeassistant/components/.translations/airly.es.json deleted file mode 100644 index 0c29ad0bc66..00000000000 --- a/homeassistant/components/.translations/airly.es.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "La clave de la API no es correcta.", - "name_exists": "El nombre ya existe.", - "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona." - }, - "step": { - "user": { - "data": { - "api_key": "Clave API de Airly", - "latitude": "Latitud", - "longitude": "Longitud", - "name": "Nombre de la integraci\u00f3n" - }, - "description": "Establecer la integraci\u00f3n de la calidad del aire de Airly. Para generar la clave de la API vaya a https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.fr.json b/homeassistant/components/.translations/airly.fr.json deleted file mode 100644 index cf756a9f492..00000000000 --- a/homeassistant/components/.translations/airly.fr.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "error": { - "auth": "La cl\u00e9 API n'est pas correcte.", - "name_exists": "Le nom existe d\u00e9j\u00e0.", - "wrong_location": "Aucune station de mesure Airly dans cette zone." - }, - "step": { - "user": { - "data": { - "api_key": "Cl\u00e9 API Airly", - "latitude": "Latitude", - "longitude": "Longitude", - "name": "Nom de l'int\u00e9gration" - }, - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.it.json b/homeassistant/components/.translations/airly.it.json deleted file mode 100644 index e50f618575b..00000000000 --- a/homeassistant/components/.translations/airly.it.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "La chiave API non \u00e8 corretta.", - "name_exists": "Il nome \u00e8 gi\u00e0 esistente", - "wrong_location": "Nessuna stazione di misurazione Airly in quest'area." - }, - "step": { - "user": { - "data": { - "api_key": "Chiave API Airly", - "latitude": "Latitudine", - "longitude": "Logitudine", - "name": "Nome dell'integrazione" - }, - "description": "Configurazione dell'integrazione della qualit\u00e0 dell'aria Airly. Per generare la chiave API andare su https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.lb.json b/homeassistant/components/.translations/airly.lb.json deleted file mode 100644 index 08aac57d162..00000000000 --- a/homeassistant/components/.translations/airly.lb.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "Api Schl\u00ebssel ass net korrekt.", - "name_exists": "Numm g\u00ebtt et schonn", - "wrong_location": "Keng Airly Moos Statioun an d\u00ebsem Ber\u00e4ich" - }, - "step": { - "user": { - "data": { - "api_key": "Airly API Schl\u00ebssel", - "latitude": "Breedegrad", - "longitude": "L\u00e4ngegrad", - "name": "Numm vun der Installatioun" - }, - "description": "Airly Loft Qualit\u00e9it Integratioun ariichten. Fir een API Schl\u00ebssel z'erstelle gitt op https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.nn.json b/homeassistant/components/.translations/airly.nn.json deleted file mode 100644 index 7e2f4f1ff6b..00000000000 --- a/homeassistant/components/.translations/airly.nn.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "config": { - "step": { - "user": { - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.no.json b/homeassistant/components/.translations/airly.no.json deleted file mode 100644 index 70924bb7bf4..00000000000 --- a/homeassistant/components/.translations/airly.no.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "API-n\u00f8kkelen er ikke korrekt.", - "name_exists": "Navnet finnes allerede.", - "wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det." - }, - "step": { - "user": { - "data": { - "api_key": "Airly API-n\u00f8kkel", - "latitude": "Breddegrad", - "longitude": "Lengdegrad", - "name": "Navn p\u00e5 integrasjonen" - }, - "description": "Sett opp Airly luftkvalitet integrering. For \u00e5 generere API-n\u00f8kkel g\u00e5 til https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.pl.json b/homeassistant/components/.translations/airly.pl.json deleted file mode 100644 index 5d601b37591..00000000000 --- a/homeassistant/components/.translations/airly.pl.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "Klucz API jest nieprawid\u0142owy.", - "name_exists": "Nazwa ju\u017c istnieje.", - "wrong_location": "Brak stacji pomiarowych Airly w tym rejonie." - }, - "step": { - "user": { - "data": { - "api_key": "Klucz API Airly", - "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna", - "name": "Nazwa integracji" - }, - "description": "Konfiguracja integracji Airly. By wygenerowa\u0107 klucz API, przejd\u017a na stron\u0119 https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.ru.json b/homeassistant/components/.translations/airly.ru.json deleted file mode 100644 index 36080c9f372..00000000000 --- a/homeassistant/components/.translations/airly.ru.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", - "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", - "wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly." - }, - "step": { - "user": { - "data": { - "api_key": "\u041a\u043b\u044e\u0447 API", - "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" - }, - "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 Airly. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://developer.airly.eu/register.", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.sl.json b/homeassistant/components/.translations/airly.sl.json deleted file mode 100644 index 08f57d88bcb..00000000000 --- a/homeassistant/components/.translations/airly.sl.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "Klju\u010d API ni pravilen.", - "name_exists": "Ime \u017ee obstaja", - "wrong_location": "Na tem obmo\u010dju ni merilnih postaj Airly." - }, - "step": { - "user": { - "data": { - "api_key": "Airly API klju\u010d", - "latitude": "Zemljepisna \u0161irina", - "longitude": "Zemljepisna dol\u017eina", - "name": "Ime integracije" - }, - "description": "Nastavite Airly integracijo za kakovost zraka. \u010ce \u017eelite ustvariti API klju\u010d pojdite na https://developer.airly.eu/register", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file diff --git a/homeassistant/components/.translations/airly.zh-Hant.json b/homeassistant/components/.translations/airly.zh-Hant.json deleted file mode 100644 index bb38d2b9b8c..00000000000 --- a/homeassistant/components/.translations/airly.zh-Hant.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "error": { - "auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002", - "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", - "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" - }, - "step": { - "user": { - "data": { - "api_key": "Airly API \u5bc6\u9470", - "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6", - "name": "\u6574\u5408\u540d\u7a31" - }, - "description": "\u6b32\u8a2d\u5b9a Airly \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://developer.airly.eu/register \u7522\u751f API \u5bc6\u9470", - "title": "Airly" - } - }, - "title": "Airly" - } -} \ No newline at end of file From 652bf540442700626d2ee24af2a46b7e3184f9a9 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 12 Oct 2019 21:57:01 +0200 Subject: [PATCH 206/639] Fix update after network error (#27444) --- homeassistant/components/airly/air_quality.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index a8ec82ab304..f8500869509 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -148,6 +148,9 @@ class AirlyAirQuality(AirQualityEntity): """Get the data from Airly.""" await self.airly.async_update() + if self.airly.data: + self.data = self.airly.data + self._pm_10 = self.data[ATTR_API_PM10] self._pm_2_5 = self.data[ATTR_API_PM25] self._aqi = self.data[ATTR_API_CAQI] From 17b1ba2e9fccc8d8c0e0bfdcdef06588803089db Mon Sep 17 00:00:00 2001 From: Paolo Tuninetto Date: Sat, 12 Oct 2019 21:57:18 +0200 Subject: [PATCH 207/639] Move AmazonPolly imports (#27443) --- homeassistant/components/amazon_polly/tts.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/amazon_polly/tts.py b/homeassistant/components/amazon_polly/tts.py index 64b8b71457c..3acfd472320 100644 --- a/homeassistant/components/amazon_polly/tts.py +++ b/homeassistant/components/amazon_polly/tts.py @@ -1,5 +1,6 @@ """Support for the Amazon Polly text to speech service.""" import logging +import boto3 import voluptuous as vol @@ -156,8 +157,6 @@ def get_engine(hass, config): config[CONF_SAMPLE_RATE] = sample_rate - import boto3 - profile = config.get(CONF_PROFILE_NAME) if profile is not None: From 22e7cb11f4f423553ef9a7deea8e79de6dbe7d53 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Sat, 12 Oct 2019 21:58:52 +0200 Subject: [PATCH 208/639] Change persistent notification about dev-info panel (#27441) * there is no dev-info panel anymore * Update __init__.py * Update __init__.py --- homeassistant/components/hassio/__init__.py | 2 +- homeassistant/components/homeassistant/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 6603728e037..e0c0a57375a 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -269,7 +269,7 @@ async def async_setup(hass, config): if errors: _LOGGER.error(errors) hass.components.persistent_notification.async_create( - "Config error. See dev-info panel for details.", + "Config error. See [the logs](/developer-tools/logs) for details.", "Config validating", f"{HASS_DOMAIN}.check_config", ) diff --git a/homeassistant/components/homeassistant/__init__.py b/homeassistant/components/homeassistant/__init__.py index 02e53d1de10..d2d6abdadb5 100644 --- a/homeassistant/components/homeassistant/__init__.py +++ b/homeassistant/components/homeassistant/__init__.py @@ -108,7 +108,7 @@ async def async_setup(hass: ha.HomeAssistant, config: dict) -> Awaitable[bool]: if errors: _LOGGER.error(errors) hass.components.persistent_notification.async_create( - "Config error. See dev-info panel for details.", + "Config error. See [the logs](/developer-tools/logs) for details.", "Config validating", f"{ha.DOMAIN}.check_config", ) From 15f0fabe9d76489a54a7fd47ec719985517e04f4 Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Sat, 12 Oct 2019 21:59:36 +0200 Subject: [PATCH 209/639] Bump pysyncthru to 0.5.0 (#27439) --- homeassistant/components/syncthru/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/syncthru/manifest.json b/homeassistant/components/syncthru/manifest.json index 79e6f8e5571..41ac1024a85 100644 --- a/homeassistant/components/syncthru/manifest.json +++ b/homeassistant/components/syncthru/manifest.json @@ -3,7 +3,7 @@ "name": "Syncthru", "documentation": "https://www.home-assistant.io/integrations/syncthru", "requirements": [ - "pysyncthru==0.4.3" + "pysyncthru==0.5.0" ], "dependencies": [], "codeowners": ["@nielstron"] diff --git a/requirements_all.txt b/requirements_all.txt index df024f34cda..923c27fa241 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1465,7 +1465,7 @@ pysuez==0.1.17 pysupla==0.0.3 # homeassistant.components.syncthru -pysyncthru==0.4.3 +pysyncthru==0.5.0 # homeassistant.components.tautulli pytautulli==0.5.0 From 3b9ee9c9015c5a622b63787ef9fc378c1a1a875c Mon Sep 17 00:00:00 2001 From: quthla Date: Sat, 12 Oct 2019 22:00:48 +0200 Subject: [PATCH 210/639] Bump RtmAPI to 0.7.2 (#27433) * Bump RtmAPI to 0.7.2 * Bump RtmAPI to 0.7.2 * Bump RtmAPI to 0.7.2 --- homeassistant/components/remember_the_milk/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index 4979fe29e0e..6ec9dc6f8f4 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -3,7 +3,7 @@ "name": "Remember the milk", "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", "requirements": [ - "RtmAPI==0.7.0", + "RtmAPI==0.7.2", "httplib2==0.10.3" ], "dependencies": ["configurator"], diff --git a/requirements_all.txt b/requirements_all.txt index 923c27fa241..15022ba161f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -85,7 +85,7 @@ PyXiaomiGateway==0.12.4 # RPi.GPIO==0.6.5 # homeassistant.components.remember_the_milk -RtmAPI==0.7.0 +RtmAPI==0.7.2 # homeassistant.components.travisci TravisPy==0.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f37ef15992..143f219fecf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -40,7 +40,7 @@ PyRMVtransport==0.2.9 PyTransportNSW==0.1.1 # homeassistant.components.remember_the_milk -RtmAPI==0.7.0 +RtmAPI==0.7.2 # homeassistant.components.yessssms YesssSMS==0.4.1 From 701bb666c4977a0949273b5de6ec442ae3ea47c6 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 22:01:35 +0200 Subject: [PATCH 211/639] Move imports in watson_iot component (#27448) --- homeassistant/components/watson_iot/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/watson_iot/__init__.py b/homeassistant/components/watson_iot/__init__.py index aef2cc8ccce..adc05893fde 100644 --- a/homeassistant/components/watson_iot/__init__.py +++ b/homeassistant/components/watson_iot/__init__.py @@ -4,6 +4,8 @@ import queue import threading import time +from ibmiotf import MissingMessageEncoderException +from ibmiotf.gateway import Client import voluptuous as vol from homeassistant.const import ( @@ -67,7 +69,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Watson IoT Platform component.""" - from ibmiotf import gateway conf = config[DOMAIN] @@ -85,7 +86,7 @@ def setup(hass, config): "auth-method": "token", "auth-token": conf[CONF_TOKEN], } - watson_gateway = gateway.Client(client_args) + watson_gateway = Client(client_args) def event_to_json(event): """Add an event to the outgoing list.""" @@ -190,7 +191,6 @@ class WatsonIOTThread(threading.Thread): def write_to_watson(self, events): """Write preprocessed events to watson.""" - import ibmiotf for event in events: for retry in range(MAX_TRIES + 1): @@ -208,7 +208,7 @@ class WatsonIOTThread(threading.Thread): _LOGGER.error("Failed to publish message to Watson IoT") continue break - except (ibmiotf.MissingMessageEncoderException, IOError): + except (MissingMessageEncoderException, IOError): if retry < MAX_TRIES: time.sleep(RETRY_DELAY) else: From 8a1738281aff91aa4f10871f858c27a82b44da7c Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 22:02:12 +0200 Subject: [PATCH 212/639] moved imports to top level (#27454) --- homeassistant/components/abode/__init__.py | 9 ++++----- homeassistant/components/abode/binary_sensor.py | 5 +++-- homeassistant/components/abode/camera.py | 4 ++-- homeassistant/components/abode/cover.py | 3 ++- homeassistant/components/abode/light.py | 3 ++- homeassistant/components/abode/lock.py | 3 ++- homeassistant/components/abode/sensor.py | 3 ++- homeassistant/components/abode/switch.py | 5 +++-- 8 files changed, 20 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index f43cbc50f98..b7f13d49b69 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -2,6 +2,10 @@ import logging from functools import partial from requests.exceptions import HTTPError, ConnectTimeout +import abodepy +import abodepy.helpers.constants as CONST +from abodepy.exceptions import AbodeException +import abodepy.helpers.timeline as TIMELINE import voluptuous as vol @@ -98,7 +102,6 @@ class AbodeSystem: def __init__(self, username, password, cache, name, polling, exclude, lights): """Initialize the system.""" - import abodepy self.abode = abodepy.Abode( username, @@ -124,7 +127,6 @@ class AbodeSystem: def is_light(self, device): """Check if a switch device is configured as a light.""" - import abodepy.helpers.constants as CONST return device.generic_type == CONST.TYPE_LIGHT or ( device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights @@ -133,7 +135,6 @@ class AbodeSystem: def setup(hass, config): """Set up Abode component.""" - from abodepy.exceptions import AbodeException conf = config[DOMAIN] username = conf.get(CONF_USERNAME) @@ -172,7 +173,6 @@ def setup(hass, config): def setup_hass_services(hass): """Home assistant services.""" - from abodepy.exceptions import AbodeException def change_setting(call): """Change an Abode system setting.""" @@ -246,7 +246,6 @@ def setup_hass_events(hass): def setup_abode_events(hass): """Event callbacks.""" - import abodepy.helpers.timeline as TIMELINE def event_callback(event, event_json): """Handle an event callback from Abode.""" diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index e37f6a465a4..3ae7f41d84e 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -1,6 +1,9 @@ """Support for Abode Security System binary sensors.""" import logging +import abodepy.helpers.constants as CONST +import abodepy.helpers.timeline as TIMELINE + from homeassistant.components.binary_sensor import BinarySensorDevice from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice @@ -10,8 +13,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a sensor for an Abode device.""" - import abodepy.helpers.constants as CONST - import abodepy.helpers.timeline as TIMELINE data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index 95755a644e2..f52bbe17475 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -3,6 +3,8 @@ from datetime import timedelta import logging import requests +import abodepy.helpers.constants as CONST +import abodepy.helpers.timeline as TIMELINE from homeassistant.components.camera import Camera from homeassistant.util import Throttle @@ -16,8 +18,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Abode camera devices.""" - import abodepy.helpers.constants as CONST - import abodepy.helpers.timeline as TIMELINE data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 4c868daf4ba..13d46c53f73 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -1,6 +1,8 @@ """Support for Abode Security System covers.""" import logging +import abodepy.helpers.constants as CONST + from homeassistant.components.cover import CoverDevice from . import DOMAIN as ABODE_DOMAIN, AbodeDevice @@ -10,7 +12,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Abode cover devices.""" - import abodepy.helpers.constants as CONST data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 8e6691560e5..6551cba2ef1 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -2,6 +2,8 @@ import logging from math import ceil +import abodepy.helpers.constants as CONST + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -23,7 +25,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Abode light devices.""" - import abodepy.helpers.constants as CONST data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index c1272a3de5f..ff069751605 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -1,6 +1,8 @@ """Support for Abode Security System locks.""" import logging +import abodepy.helpers.constants as CONST + from homeassistant.components.lock import LockDevice from . import DOMAIN as ABODE_DOMAIN, AbodeDevice @@ -10,7 +12,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Abode lock devices.""" - import abodepy.helpers.constants as CONST data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index ba28eab79c7..fca32b8dc43 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -1,6 +1,8 @@ """Support for Abode Security System sensors.""" import logging +import abodepy.helpers.constants as CONST + from homeassistant.const import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -21,7 +23,6 @@ SENSOR_TYPES = { def setup_platform(hass, config, add_entities, discovery_info=None): """Set up a sensor for an Abode device.""" - import abodepy.helpers.constants as CONST data = hass.data[ABODE_DOMAIN] diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 82a550df1a5..4192ebb4485 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -1,6 +1,9 @@ """Support for Abode Security System switches.""" import logging +import abodepy.helpers.constants as CONST +import abodepy.helpers.timeline as TIMELINE + from homeassistant.components.switch import SwitchDevice from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice @@ -10,8 +13,6 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Abode switch devices.""" - import abodepy.helpers.constants as CONST - import abodepy.helpers.timeline as TIMELINE data = hass.data[ABODE_DOMAIN] From e9642a0f65dc45fe8dea9d503822a790cccfcbf9 Mon Sep 17 00:00:00 2001 From: quthla Date: Sat, 12 Oct 2019 22:02:20 +0200 Subject: [PATCH 213/639] Bump PyGithub to 1.43.8 (#27432) * Bump PyGithub to 1.43.8 * Bump PyGithub to 1.43.8 --- homeassistant/components/github/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/github/manifest.json b/homeassistant/components/github/manifest.json index 0b5e3c0df9f..02593bf603d 100644 --- a/homeassistant/components/github/manifest.json +++ b/homeassistant/components/github/manifest.json @@ -3,7 +3,7 @@ "name": "Github", "documentation": "https://www.home-assistant.io/integrations/github", "requirements": [ - "PyGithub==1.43.5" + "PyGithub==1.43.8" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 15022ba161f..f5b4bf38da3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -47,7 +47,7 @@ OPi.GPIO==0.3.6 PyEssent==0.13 # homeassistant.components.github -PyGithub==1.43.5 +PyGithub==1.43.8 # homeassistant.components.isy994 PyISY==1.1.2 From 72b711fde62a80230e1eef09ad7c7523950e9eea Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 12 Oct 2019 22:03:02 +0200 Subject: [PATCH 214/639] Move imports in w800rf32 component (#27451) --- homeassistant/components/w800rf32/__init__.py | 3 +-- homeassistant/components/w800rf32/binary_sensor.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/w800rf32/__init__.py b/homeassistant/components/w800rf32/__init__.py index 9f15e0b2aa1..805cca47023 100644 --- a/homeassistant/components/w800rf32/__init__.py +++ b/homeassistant/components/w800rf32/__init__.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import W800rf32 as w800 from homeassistant.const import ( CONF_DEVICE, @@ -26,8 +27,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the w800rf32 component.""" - # Try to load the W800rf32 module. - import W800rf32 as w800 # Declare the Handle event def handle_receive(event): diff --git a/homeassistant/components/w800rf32/binary_sensor.py b/homeassistant/components/w800rf32/binary_sensor.py index f1f1890f7aa..e08111da8ba 100644 --- a/homeassistant/components/w800rf32/binary_sensor.py +++ b/homeassistant/components/w800rf32/binary_sensor.py @@ -2,6 +2,7 @@ import logging import voluptuous as vol +import W800rf32 as w800 from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, @@ -104,9 +105,8 @@ class W800rf32BinarySensor(BinarySensorDevice): @callback def binary_sensor_update(self, event): """Call for control updates from the w800rf32 gateway.""" - import W800rf32 as w800rf32mod - if not isinstance(event, w800rf32mod.W800rf32Event): + if not isinstance(event, w800.W800rf32Event): return dev_id = event.device From 930b576685c3126b726dd51911e66a6f7f1e0a08 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 22:03:42 +0200 Subject: [PATCH 215/639] moved imports to top level (#27458) --- homeassistant/components/ads/__init__.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ads/__init__.py b/homeassistant/components/ads/__init__.py index 1b4f11c7cc1..ba4762da84a 100644 --- a/homeassistant/components/ads/__init__.py +++ b/homeassistant/components/ads/__init__.py @@ -7,6 +7,8 @@ from collections import namedtuple import asyncio import async_timeout +import pyads + import voluptuous as vol from homeassistant.const import ( @@ -78,7 +80,6 @@ SCHEMA_SERVICE_WRITE_DATA_BY_NAME = vol.Schema( def setup(hass, config): """Set up the ADS component.""" - import pyads conf = config[DOMAIN] @@ -161,7 +162,6 @@ class AdsHub: def shutdown(self, *args, **kwargs): """Shutdown ADS connection.""" - import pyads _LOGGER.debug("Shutting down ADS") for notification_item in self._notification_items.values(): @@ -187,7 +187,6 @@ class AdsHub: def write_by_name(self, name, value, plc_datatype): """Write a value to the device.""" - import pyads with self._lock: try: @@ -197,7 +196,6 @@ class AdsHub: def read_by_name(self, name, plc_datatype): """Read a value from the device.""" - import pyads with self._lock: try: @@ -207,7 +205,6 @@ class AdsHub: def add_device_notification(self, name, plc_datatype, callback): """Add a notification to the ADS devices.""" - import pyads attr = pyads.NotificationAttrib(ctypes.sizeof(plc_datatype)) From 3b4e2572141e616dbe86b1dd70139ce8fa94dc54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sat, 12 Oct 2019 22:03:56 +0200 Subject: [PATCH 216/639] Move imports in dht component (#27459) * Move imports in dht component * remove empty line * Move imports for pushbullet component * revert unwanted changes --- homeassistant/components/dht/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dht/sensor.py b/homeassistant/components/dht/sensor.py index aadb6b2d4cb..648e0e1ed72 100644 --- a/homeassistant/components/dht/sensor.py +++ b/homeassistant/components/dht/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import Adafruit_DHT # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -50,7 +51,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the DHT sensor.""" - import Adafruit_DHT # pylint: disable=import-error SENSOR_TYPES[SENSOR_TEMPERATURE][1] = hass.config.units.temperature_unit available_sensors = { From 08ccaac21fb8b6fc68339d0b43ebf3e4d2f18043 Mon Sep 17 00:00:00 2001 From: Paolo Tuninetto Date: Sat, 12 Oct 2019 22:04:14 +0200 Subject: [PATCH 217/639] Move Epson imports (#27457) --- .../components/epson/media_player.py | 68 ++++++++----------- 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/epson/media_player.py b/homeassistant/components/epson/media_player.py index 435ef582da8..638f012ac7a 100644 --- a/homeassistant/components/epson/media_player.py +++ b/homeassistant/components/epson/media_player.py @@ -3,6 +3,30 @@ import logging import voluptuous as vol +from epson_projector.const import ( + BACK, + BUSY, + CMODE, + CMODE_LIST, + CMODE_LIST_SET, + DEFAULT_SOURCES, + EPSON_CODES, + FAST, + INV_SOURCES, + MUTE, + PAUSE, + PLAY, + POWER, + SOURCE, + SOURCE_LIST, + TURN_ON, + TURN_OFF, + VOLUME, + VOL_DOWN, + VOL_UP, +) +import epson_projector as epson + from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player.const import ( DOMAIN, @@ -61,8 +85,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Epson media player platform.""" - from epson_projector.const import CMODE_LIST_SET - if DATA_EPSON not in hass.data: hass.data[DATA_EPSON] = [] @@ -71,12 +93,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= port = config.get(CONF_PORT) ssl = config.get(CONF_SSL) - epson = EpsonProjector( + epson_proj = EpsonProjector( async_get_clientsession(hass, verify_ssl=False), name, host, port, ssl ) - hass.data[DATA_EPSON].append(epson) - async_add_entities([epson], update_before_add=True) + hass.data[DATA_EPSON].append(epson_proj) + async_add_entities([epson_proj], update_before_add=True) async def async_service_handler(service): """Handle for services.""" @@ -108,9 +130,6 @@ class EpsonProjector(MediaPlayerDevice): def __init__(self, websession, name, host, port, encryption): """Initialize entity to control Epson projector.""" - import epson_projector as epson - from epson_projector.const import DEFAULT_SOURCES - self._name = name self._projector = epson.Projector(host, websession=websession, port=port) self._cmode = None @@ -121,17 +140,6 @@ class EpsonProjector(MediaPlayerDevice): async def async_update(self): """Update state of device.""" - from epson_projector.const import ( - EPSON_CODES, - POWER, - CMODE, - CMODE_LIST, - SOURCE, - VOLUME, - BUSY, - SOURCE_LIST, - ) - is_turned_on = await self._projector.get_property(POWER) _LOGGER.debug("Project turn on/off status: %s", is_turned_on) if is_turned_on and is_turned_on == EPSON_CODES[POWER]: @@ -165,15 +173,11 @@ class EpsonProjector(MediaPlayerDevice): async def async_turn_on(self): """Turn on epson.""" - from epson_projector.const import TURN_ON - if self._state == STATE_OFF: await self._projector.send_command(TURN_ON) async def async_turn_off(self): """Turn off epson.""" - from epson_projector.const import TURN_OFF - if self._state == STATE_ON: await self._projector.send_command(TURN_OFF) @@ -194,57 +198,39 @@ class EpsonProjector(MediaPlayerDevice): async def select_cmode(self, cmode): """Set color mode in Epson.""" - from epson_projector.const import CMODE_LIST_SET - await self._projector.send_command(CMODE_LIST_SET[cmode]) async def async_select_source(self, source): """Select input source.""" - from epson_projector.const import INV_SOURCES - selected_source = INV_SOURCES[source] await self._projector.send_command(selected_source) async def async_mute_volume(self, mute): """Mute (true) or unmute (false) sound.""" - from epson_projector.const import MUTE - await self._projector.send_command(MUTE) async def async_volume_up(self): """Increase volume.""" - from epson_projector.const import VOL_UP - await self._projector.send_command(VOL_UP) async def async_volume_down(self): """Decrease volume.""" - from epson_projector.const import VOL_DOWN - await self._projector.send_command(VOL_DOWN) async def async_media_play(self): """Play media via Epson.""" - from epson_projector.const import PLAY - await self._projector.send_command(PLAY) async def async_media_pause(self): """Pause media via Epson.""" - from epson_projector.const import PAUSE - await self._projector.send_command(PAUSE) async def async_media_next_track(self): """Skip to next.""" - from epson_projector.const import FAST - await self._projector.send_command(FAST) async def async_media_previous_track(self): """Skip to previous.""" - from epson_projector.const import BACK - await self._projector.send_command(BACK) @property From 88e54a4ce6095bf4ca78f64bdc2ce63b1d9bca03 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 22:04:35 +0200 Subject: [PATCH 218/639] moved imports to top level (#27468) --- homeassistant/components/anthemav/media_player.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/anthemav/media_player.py b/homeassistant/components/anthemav/media_player.py index a033470e5c9..d472af6104e 100644 --- a/homeassistant/components/anthemav/media_player.py +++ b/homeassistant/components/anthemav/media_player.py @@ -1,6 +1,8 @@ """Support for Anthem Network Receivers and Processors.""" import logging +import anthemav + import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -46,7 +48,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up our socket to the AVR.""" - import anthemav host = config.get(CONF_HOST) port = config.get(CONF_PORT) From 3096e94343aa287059bcabcf48c4a5e87355706e Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 12 Oct 2019 22:04:42 +0200 Subject: [PATCH 219/639] moved imports to top level (#27469) --- homeassistant/components/aprs/device_tracker.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index 86b0b6f48af..0d23cedb4ee 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -3,6 +3,11 @@ import logging import threading +import geopy.distance +import aprslib +from aprslib import ConnectionError as AprsConnectionError +from aprslib import LoginError + import voluptuous as vol from homeassistant.components.device_tracker import PLATFORM_SCHEMA @@ -59,7 +64,6 @@ def make_filter(callsigns: list) -> str: def gps_accuracy(gps, posambiguity: int) -> int: """Calculate the GPS accuracy based on APRS posambiguity.""" - import geopy.distance pos_a_map = {0: 0, 1: 1 / 600, 2: 1 / 60, 3: 1 / 6, 4: 1} if posambiguity in pos_a_map: @@ -115,8 +119,6 @@ class AprsListenerThread(threading.Thread): """Initialize the class.""" super().__init__() - import aprslib - self.callsign = callsign self.host = host self.start_event = threading.Event() @@ -138,8 +140,6 @@ class AprsListenerThread(threading.Thread): def run(self): """Connect to APRS and listen for data.""" self.ais.set_filter(self.server_filter) - from aprslib import ConnectionError as AprsConnectionError - from aprslib import LoginError try: _LOGGER.info( From 86da3fb334811ec8246da9e8423cdf1e6efe71c8 Mon Sep 17 00:00:00 2001 From: Jordan Speicher Date: Sat, 12 Oct 2019 15:05:01 -0500 Subject: [PATCH 220/639] Add mobile_app dependency on cloud (#27470) --- homeassistant/components/mobile_app/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index 8c95ca4ad41..ab140b4148e 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -7,6 +7,7 @@ "PyNaCl==1.3.0" ], "dependencies": [ + "cloud", "http", "webhook" ], From 6b92dbe20914e9e704aaa084eef55ed18eea6a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20RAMAGE?= Date: Sat, 12 Oct 2019 22:05:36 +0200 Subject: [PATCH 221/639] Move imports for pushbullet component (#27460) * Move imports in dht component * remove empty line * Move imports for pushbullet component * revert unwanted changes * Move imports for pushbullet component * remove dht change from that branch * remove dht changes from this branch --- homeassistant/components/pushbullet/notify.py | 6 +++--- homeassistant/components/pushbullet/sensor.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/pushbullet/notify.py b/homeassistant/components/pushbullet/notify.py index 70738965340..76c1e14e5a5 100644 --- a/homeassistant/components/pushbullet/notify.py +++ b/homeassistant/components/pushbullet/notify.py @@ -2,6 +2,9 @@ import logging import mimetypes +from pushbullet import PushBullet +from pushbullet import InvalidKeyError +from pushbullet import PushError import voluptuous as vol from homeassistant.const import CONF_API_KEY @@ -28,8 +31,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_API_KEY): cv.string} def get_service(hass, config, discovery_info=None): """Get the Pushbullet notification service.""" - from pushbullet import PushBullet - from pushbullet import InvalidKeyError try: pushbullet = PushBullet(config[CONF_API_KEY]) @@ -124,7 +125,6 @@ class PushBulletNotificationService(BaseNotificationService): def _push_data(self, message, title, data, pusher, email=None): """Create the message content.""" - from pushbullet import PushError if data is None: data = {} diff --git a/homeassistant/components/pushbullet/sensor.py b/homeassistant/components/pushbullet/sensor.py index 3ed53fb01f6..600b38b6eaf 100644 --- a/homeassistant/components/pushbullet/sensor.py +++ b/homeassistant/components/pushbullet/sensor.py @@ -1,6 +1,10 @@ """Pushbullet platform for sensor component.""" import logging +import threading +from pushbullet import PushBullet +from pushbullet import InvalidKeyError +from pushbullet import Listener import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_MONITORED_CONDITIONS @@ -35,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Pushbullet Sensor platform.""" - from pushbullet import PushBullet - from pushbullet import InvalidKeyError try: pushbullet = PushBullet(config.get(CONF_API_KEY)) @@ -95,7 +97,6 @@ class PushBulletNotificationProvider: def __init__(self, pb): """Start to retrieve pushes from the given Pushbullet instance.""" - import threading self.pushbullet = pb self._data = None @@ -123,7 +124,6 @@ class PushBulletNotificationProvider: Spawn a new Listener and links it to self.on_push. """ - from pushbullet import Listener self.listener = Listener(account=self.pushbullet, on_push=self.on_push) _LOGGER.debug("Getting pushes") From 1f7bd4235cbae607950e6f4f9641b969b441ab17 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 12 Oct 2019 22:07:54 +0200 Subject: [PATCH 222/639] Add test to Homematic IP Cloud switch (#27532) --- .../homematicip_cloud/test_switch.py | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 tests/components/homematicip_cloud/test_switch.py diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py new file mode 100644 index 00000000000..15eaf6da04c --- /dev/null +++ b/tests/components/homematicip_cloud/test_switch.py @@ -0,0 +1,156 @@ +"""Tests for HomematicIP Cloud switch.""" +from homeassistant.components.homematicip_cloud.device import ( + ATTR_GROUP_MEMBER_UNREACHABLE, +) +from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH +from homeassistant.const import STATE_OFF, STATE_ON + +from .helper import async_manipulate_test_data, get_and_check_entity_basics + + +async def test_hmip_switch(hass, default_mock_hap): + """Test HomematicipSwitch.""" + entity_id = "switch.schrank" + entity_name = "Schrank" + device_model = "HMIP-PS" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + +async def test_hmip_switch_measuring(hass, default_mock_hap): + """Test HomematicipSwitchMeasuring.""" + entity_id = "switch.pc" + entity_name = "Pc" + device_model = "HMIP-PSM" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + await async_manipulate_test_data(hass, hmip_device, "currentPowerConsumption", 50) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + assert ha_state.attributes[ATTR_CURRENT_POWER_W] == 50 + assert ha_state.attributes[ATTR_TODAY_ENERGY_KWH] == 36 + + await async_manipulate_test_data(hass, hmip_device, "energyCounter", None) + ha_state = hass.states.get(entity_id) + assert not ha_state.attributes.get(ATTR_TODAY_ENERGY_KWH) + + +async def test_hmip_group_switch(hass, default_mock_hap): + """Test HomematicipGroupSwitch.""" + entity_id = "switch.strom_group" + entity_name = "Strom Group" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_ON + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == () + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + assert not ha_state.attributes.get(ATTR_GROUP_MEMBER_UNREACHABLE) + await async_manipulate_test_data(hass, hmip_device, "unreach", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes[ATTR_GROUP_MEMBER_UNREACHABLE] is True + + +async def test_hmip_multi_switch(hass, default_mock_hap): + """Test HomematicipMultiSwitch.""" + entity_id = "switch.jalousien_1_kizi_2_schlazi_channel1" + entity_name = "Jalousien - 1 KiZi, 2 SchlaZi Channel1" + device_model = "HmIP-PCBS2" + + ha_state, hmip_device = get_and_check_entity_basics( + hass, default_mock_hap, entity_id, entity_name, device_model + ) + + assert ha_state.state == STATE_OFF + service_call_counter = len(hmip_device.mock_calls) + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 1 + assert hmip_device.mock_calls[-1][0] == "turn_on" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "on", True) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_ON + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": entity_id}, blocking=True + ) + assert len(hmip_device.mock_calls) == service_call_counter + 3 + assert hmip_device.mock_calls[-1][0] == "turn_off" + assert hmip_device.mock_calls[-1][1] == (1,) + await async_manipulate_test_data(hass, hmip_device, "on", False) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF From 96c0ad302f0ec6871b6443b25433c76091d965b4 Mon Sep 17 00:00:00 2001 From: Moritz Fey Date: Sat, 12 Oct 2019 22:08:45 +0200 Subject: [PATCH 223/639] add device conditions for platform cover (#27544) * add device condition support to the cover integration * remove TODO comment * add strings.json --- .../components/cover/device_condition.py | 103 ++++++++++ homeassistant/components/cover/strings.json | 10 + .../components/cover/test_device_condition.py | 190 ++++++++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 homeassistant/components/cover/device_condition.py create mode 100644 homeassistant/components/cover/strings.json create mode 100644 tests/components/cover/test_device_condition.py diff --git a/homeassistant/components/cover/device_condition.py b/homeassistant/components/cover/device_condition.py new file mode 100644 index 00000000000..129462047e4 --- /dev/null +++ b/homeassistant/components/cover/device_condition.py @@ -0,0 +1,103 @@ +"""Provides device automations for Cover.""" +from typing import Any, Dict, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + STATE_OPEN, + STATE_CLOSED, + STATE_OPENING, + STATE_CLOSING, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.typing import ConfigType, TemplateVarsType +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from . import DOMAIN + +CONDITION_TYPES = {"is_open", "is_closed", "is_opening", "is_closing"} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device conditions for Cover devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions: List[Dict[str, Any]] = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add conditions for each entity that belongs to this integration + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_open", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_closed", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_opening", + } + ) + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "is_closing", + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + if config[CONF_TYPE] == "is_open": + state = STATE_OPEN + elif config[CONF_TYPE] == "is_closed": + state = STATE_CLOSED + elif config[CONF_TYPE] == "is_opening": + state = STATE_OPENING + elif config[CONF_TYPE] == "is_closing": + state = STATE_CLOSING + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/cover/strings.json b/homeassistant/components/cover/strings.json new file mode 100644 index 00000000000..db3ccf9119f --- /dev/null +++ b/homeassistant/components/cover/strings.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_open": "{entity_name} is open", + "is_closed": "{entity_name} is closed", + "is_opening": "{entity_name} is opening", + "is_closing": "{entity_name} is closing" + } + } +} \ No newline at end of file diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py new file mode 100644 index 00000000000..494368f76ff --- /dev/null +++ b/tests/components/cover/test_device_condition.py @@ -0,0 +1,190 @@ +"""The tests for Cover device conditions.""" +import pytest + +from homeassistant.components.cover import DOMAIN +from homeassistant.const import STATE_OPEN, STATE_CLOSED, STATE_OPENING, STATE_CLOSING +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a cover.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_open", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_opening", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_closing", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for turn_on and turn_off conditions.""" + hass.states.async_set("cover.entity", STATE_OPEN) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_open", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_open - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_closed", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_closed - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_opening", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_opening - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event4"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "cover.entity", + "type": "is_closing", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_closing - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_open - event - test_event1" + + hass.states.async_set("cover.entity", STATE_CLOSED) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_closed - event - test_event2" + + hass.states.async_set("cover.entity", STATE_OPENING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event3") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "is_opening - event - test_event3" + + hass.states.async_set("cover.entity", STATE_CLOSING) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event4") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "is_closing - event - test_event4" From 6c947f58b8f50a7decb0230946434fe0a3b5d42e Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Sat, 12 Oct 2019 16:11:30 -0400 Subject: [PATCH 224/639] Fix for unknown sensor state (#27542) --- homeassistant/components/ecobee/binary_sensor.py | 8 ++++++-- homeassistant/components/ecobee/sensor.py | 10 +++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ecobee/binary_sensor.py b/homeassistant/components/ecobee/binary_sensor.py index 3367f33a66f..06289572aea 100644 --- a/homeassistant/components/ecobee/binary_sensor.py +++ b/homeassistant/components/ecobee/binary_sensor.py @@ -105,6 +105,10 @@ class EcobeeBinarySensor(BinarySensorDevice): """Get the latest state of the sensor.""" await self.data.update() for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue for item in sensor["capability"]: - if item["type"] == "occupancy" and self.sensor_name == sensor["name"]: - self._state = item["value"] + if item["type"] != "occupancy": + continue + self._state = item["value"] + break diff --git a/homeassistant/components/ecobee/sensor.py b/homeassistant/components/ecobee/sensor.py index 1c47bc9b26d..76945080bfa 100644 --- a/homeassistant/components/ecobee/sensor.py +++ b/homeassistant/components/ecobee/sensor.py @@ -112,7 +112,7 @@ class EcobeeSensor(Entity): @property def state(self): """Return the state of the sensor.""" - if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN]: + if self._state in [ECOBEE_STATE_CALIBRATING, ECOBEE_STATE_UNKNOWN, "unknown"]: return None if self.type == "temperature": @@ -129,6 +129,10 @@ class EcobeeSensor(Entity): """Get the latest state of the sensor.""" await self.data.update() for sensor in self.data.ecobee.get_remote_sensors(self.index): + if sensor["name"] != self.sensor_name: + continue for item in sensor["capability"]: - if item["type"] == self.type and self.sensor_name == sensor["name"]: - self._state = item["value"] + if item["type"] != self.type: + continue + self._state = item["value"] + break From 9e121b785a38b1b72888ef8b6014bdefc4c77d2a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Oct 2019 14:07:01 -0700 Subject: [PATCH 225/639] Google: catch query not supported (#27559) --- .../components/google_assistant/report_state.py | 12 +++++++++++- .../google_assistant/test_report_state.py | 17 +++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 869bc61d7a3..b842a552714 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -1,15 +1,21 @@ """Google Report State implementation.""" +import logging + from homeassistant.core import HomeAssistant, callback from homeassistant.const import MATCH_ALL from homeassistant.helpers.event import async_call_later from .helpers import AbstractConfig, GoogleEntity, async_get_entities +from .error import SmartHomeError # Time to wait until the homegraph updates # https://github.com/actions-on-google/smart-home-nodejs/issues/196#issuecomment-439156639 INITIAL_REPORT_DELAY = 60 +_LOGGER = logging.getLogger(__name__) + + @callback def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig): """Enable state reporting.""" @@ -26,7 +32,11 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig if not entity.is_supported(): return - entity_data = entity.query_serialize() + try: + entity_data = entity.query_serialize() + except SmartHomeError as err: + _LOGGER.debug("Not reporting state for %s: %s", changed_entity, err.code) + return if old_state: old_entity = GoogleEntity(hass, google_config, old_state) diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index 734d9ec7fc8..6ab88286a69 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,7 +1,7 @@ """Test Google report state.""" from unittest.mock import patch -from homeassistant.components.google_assistant import report_state +from homeassistant.components.google_assistant import report_state, error from homeassistant.util.dt import utcnow from . import BASIC_CONFIG @@ -10,7 +10,7 @@ from . import BASIC_CONFIG from tests.common import mock_coro, async_fire_time_changed -async def test_report_state(hass): +async def test_report_state(hass, caplog): """Test report state works.""" hass.states.async_set("light.ceiling", "off") hass.states.async_set("switch.ac", "on") @@ -57,6 +57,19 @@ async def test_report_state(hass): assert len(mock_report.mock_calls) == 0 + # Test that entities that we can't query don't report a state + with patch.object( + BASIC_CONFIG, "async_report_state", side_effect=mock_coro + ) as mock_report, patch( + "homeassistant.components.google_assistant.report_state.GoogleEntity.query_serialize", + side_effect=error.SmartHomeError("mock-error", "mock-msg"), + ): + hass.states.async_set("light.kitchen", "off") + await hass.async_block_till_done() + + assert "Not reporting state for light.kitchen: mock-error" + assert len(mock_report.mock_calls) == 0 + unsub() with patch.object( From a82ff4f7a94fc6f2c635f9ecc260211654fcc42d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 12 Oct 2019 14:09:06 -0700 Subject: [PATCH 226/639] Add strings for device automations to scaffold (#27556) --- script/scaffold/docs.py | 3 +++ script/scaffold/generate.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index ab87799d6b2..bb119c0e42e 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -38,6 +38,7 @@ that can occur in the state will cause the right service to be called. f""" Device trigger base has been added to the {info.domain} integration: - {info.integration_dir / "device_trigger.py"} + - {info.integration_dir / "strings.json"} (translations) - {info.tests_dir / "test_device_trigger.py"} You will now need to update the code to make sure that relevant triggers @@ -50,6 +51,7 @@ are exposed. f""" Device condition base has been added to the {info.domain} integration: - {info.integration_dir / "device_condition.py"} + - {info.integration_dir / "strings.json"} (translations) - {info.tests_dir / "test_device_condition.py"} You will now need to update the code to make sure that relevant condtions @@ -62,6 +64,7 @@ are exposed. f""" Device action base has been added to the {info.domain} integration: - {info.integration_dir / "device_action.py"} + - {info.integration_dir / "strings.json"} (translations) - {info.tests_dir / "test_device_action.py"} You will now need to update the code to make sure that relevant services diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 6bccf6529fe..e16316fd76b 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -68,6 +68,39 @@ def _custom_tasks(template, info) -> None: info.update_manifest(**changes) + if template == "device_trigger": + info.update_strings( + device_automation={ + **info.strings().get("device_automation", {}), + "trigger_type": { + "turned_on": "{entity_name} turned on", + "turned_off": "{entity_name} turned off", + }, + } + ) + + if template == "device_condition": + info.update_strings( + device_automation={ + **info.strings().get("device_automation", {}), + "condtion_type": { + "is_on": "{entity_name} is on", + "is_off": "{entity_name} is off", + }, + } + ) + + if template == "device_action": + info.update_strings( + device_automation={ + **info.strings().get("device_automation", {}), + "action_type": { + "turn_on": "Turn on {entity_name}", + "turn_off": "Turn off {entity_name}", + }, + } + ) + if template == "config_flow": info.update_manifest(config_flow=True) info.update_strings( From 62b886a5d58f8ce2e7b0fc53e197b70f25328cb1 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 13 Oct 2019 00:31:39 +0000 Subject: [PATCH 227/639] [ci skip] Translation update --- .../binary_sensor/.translations/pl.json | 36 +++++++++--------- .../binary_sensor/.translations/ru.json | 18 +++++++++ .../binary_sensor/.translations/zh-Hant.json | 2 + .../components/cover/.translations/en.json | 10 +++++ .../components/cover/.translations/no.json | 10 +++++ .../components/deconz/.translations/pl.json | 32 ++++++++-------- .../components/light/.translations/pl.json | 4 +- .../components/lock/.translations/en.json | 8 ++++ .../components/lock/.translations/no.json | 8 ++++ .../components/switch/.translations/pl.json | 4 +- .../components/zha/.translations/pl.json | 38 +++++++++---------- 11 files changed, 113 insertions(+), 57 deletions(-) create mode 100644 homeassistant/components/cover/.translations/en.json create mode 100644 homeassistant/components/cover/.translations/no.json create mode 100644 homeassistant/components/lock/.translations/en.json create mode 100644 homeassistant/components/lock/.translations/no.json diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index a1ab03770f9..bc474e3d514 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -45,15 +45,15 @@ "is_vibration": "sensor {entity_name} wykrywa wibracje" }, "trigger_type": { - "bat_low": "bateria {entity_name} stanie si\u0119 roz\u0142adowana", - "closed": "zamkni\u0119cie {entity_name}", + "bat_low": "nast\u0105pi roz\u0142adowanie baterii {entity_name}", + "closed": "nast\u0105pi zamkni\u0119cie {entity_name}", "cold": "sensor {entity_name} wykryje zimno", - "connected": "pod\u0142\u0105czenie {entity_name}", + "connected": "nast\u0105pi pod\u0142\u0105czenie {entity_name}", "gas": "sensor {entity_name} wykryje gaz", "hot": "sensor {entity_name} wykryje gor\u0105co", "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", - "locked": "zamkni\u0119cie {entity_name}", - "moist": "sensor {entity_name} wykry\u0142 wilgo\u0107", + "locked": "nast\u0105pi zamkni\u0119cie {entity_name}", + "moist": "nast\u0105pi wykrycie wilgoci {entity_name}", "moist\u00a7": "sensor {entity_name} wykryje wilgo\u0107", "motion": "sensor {entity_name} wykryje ruch", "moving": "sensor {entity_name} zacznie porusza\u0107 si\u0119", @@ -64,29 +64,29 @@ "no_smoke": "sensor {entity_name} przestanie wykrywa\u0107 dym", "no_sound": "sensor {entity_name} przestanie wykrywa\u0107 d\u017awi\u0119k", "no_vibration": "sensor {entity_name} przestanie wykrywa\u0107 wibracje", - "not_bat_low": "bateria {entity_name} staje si\u0119 na\u0142adowana", + "not_bat_low": "nast\u0105pi na\u0142adowanie baterii {entity_name}", "not_cold": "sensor {entity_name} przestanie wykrywa\u0107 zimno", - "not_connected": "roz\u0142\u0105czenie {entity_name}", + "not_connected": "nast\u0105pi roz\u0142\u0105czenie {entity_name}", "not_hot": "sensor {entity_name} przestanie wykrywa\u0107 gor\u0105co", - "not_locked": "otwarcie {entity_name}", + "not_locked": "nast\u0105pi otwarcie {entity_name}", "not_moist": "sensor {entity_name} przestanie wykrywa\u0107 wilgo\u0107", "not_moving": "sensor {entity_name} przestanie porusza\u0107 si\u0119", - "not_occupied": "sensor {entity_name} przesta\u0142 by\u0107 zaj\u0119ty", - "not_opened": "sensor {entity_name} zosta\u0142 zamkni\u0119ty", - "not_plugged_in": "od\u0142\u0105czenie {entity_name}", - "not_powered": "od\u0142\u0105czenie zasilania {entity_name}", + "not_occupied": "sensor {entity_name} przestanie by\u0107 zaj\u0119ty", + "not_opened": "nast\u0105pi zamkni\u0119cie {entity_name}", + "not_plugged_in": "nast\u0105pi od\u0142\u0105czenie {entity_name}", + "not_powered": "nast\u0105pi od\u0142\u0105czenie zasilania {entity_name}", "not_present": "sensor {entity_name} przestanie wykrywa\u0107 obecno\u015b\u0107", "not_unsafe": "sensor {entity_name} przestanie wykrywa\u0107 niebezpiecze\u0144stwo", - "occupied": "sensor {entity_name} sta\u0142 si\u0119 zaj\u0119ty", - "opened": "otwarcie {entity_name}", - "plugged_in": "pod\u0142\u0105czenie {entity_name}", - "powered": "pod\u0142\u0105czenie zasilenia {entity_name}", + "occupied": "sensor {entity_name} stanie si\u0119 zaj\u0119ty", + "opened": "nast\u0105pi otwarcie {entity_name}", + "plugged_in": "nast\u0105pi pod\u0142\u0105czenie {entity_name}", + "powered": "nast\u0105pi pod\u0142\u0105czenie zasilenia {entity_name}", "present": "sensor {entity_name} wykryje obecno\u015b\u0107", "problem": "sensor {entity_name} wykryje problem", "smoke": "sensor {entity_name} wykryje dym", "sound": "sensor {entity_name} wykryje d\u017awi\u0119k", - "turned_off": "wy\u0142\u0105czenie {entity_name}", - "turned_on": "w\u0142\u0105czenie {entity_name}", + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}", "unsafe": "sensor {entity_name} wykryje niebezpiecze\u0144stwo", "vibration": "sensor {entity_name} wykryje wibracje" } diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json index 012a5c4fa45..a7ecced3f11 100644 --- a/homeassistant/components/binary_sensor/.translations/ru.json +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -1,7 +1,11 @@ { "device_automation": { "condition_type": { + "is_bat_low": "{entity_name} \u0432 \u0440\u0430\u0437\u0440\u044f\u0436\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_cold": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", + "is_connected": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", "is_gas": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0433\u0430\u0437", + "is_hot": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432", "is_light": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u0435\u0442", "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_moist": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443", @@ -14,16 +18,30 @@ "is_no_smoke": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", "is_no_sound": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", "is_no_vibration": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e", + "is_not_bat_low": "{entity_name} \u0432 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_not_cold": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043e\u0445\u043b\u0430\u0436\u0434\u0435\u043d\u0438\u0435", + "is_not_connected": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_not_hot": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043d\u0430\u0433\u0440\u0435\u0432", "is_not_locked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_not_moist": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u043b\u0430\u0433\u0443", "is_not_moving": "{entity_name} \u043d\u0435 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0430\u0435\u0442\u0441\u044f", + "is_not_occupied": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_not_plugged_in": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_not_powered": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", + "is_not_present": "{entity_name} \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", + "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_occupied": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_plugged_in": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", + "is_powered": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u044d\u043d\u0435\u0440\u0433\u0438\u044e", + "is_present": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "is_problem": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", "is_smoke": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0434\u044b\u043c", "is_sound": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0437\u0432\u0443\u043a", + "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_vibration": "{entity_name} \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0435\u0442 \u0432\u0438\u0431\u0440\u0430\u0446\u0438\u044e" }, "trigger_type": { diff --git a/homeassistant/components/binary_sensor/.translations/zh-Hant.json b/homeassistant/components/binary_sensor/.translations/zh-Hant.json index 36c72dcb9e6..046b999cb8c 100644 --- a/homeassistant/components/binary_sensor/.translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/.translations/zh-Hant.json @@ -53,6 +53,7 @@ "hot": "{entity_name} \u5df2\u8b8a\u71b1", "light": "{entity_name} \u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", "locked": "{entity_name} \u5df2\u4e0a\u9396", + "moist": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5", "moist\u00a7": "{entity_name} \u5df2\u8b8a\u6f6e\u6fd5", "motion": "{entity_name} \u5df2\u5075\u6e2c\u5230\u52d5\u4f5c", "moving": "{entity_name} \u958b\u59cb\u79fb\u52d5", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} \u5df2\u8b8a\u4e7e", "not_moving": "{entity_name} \u505c\u6b62\u79fb\u52d5", "not_occupied": "{entity_name} \u672a\u6709\u4eba", + "not_opened": "{entity_name} \u5df2\u95dc\u9589", "not_plugged_in": "{entity_name} \u672a\u63d2\u5165", "not_powered": "{entity_name} \u672a\u901a\u96fb", "not_present": "{entity_name} \u672a\u51fa\u73fe", diff --git a/homeassistant/components/cover/.translations/en.json b/homeassistant/components/cover/.translations/en.json new file mode 100644 index 00000000000..f9f47be3104 --- /dev/null +++ b/homeassistant/components/cover/.translations/en.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} is closed", + "is_closing": "{entity_name} is closing", + "is_open": "{entity_name} is open", + "is_opening": "{entity_name} is opening" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/no.json b/homeassistant/components/cover/.translations/no.json new file mode 100644 index 00000000000..ff37aa27d58 --- /dev/null +++ b/homeassistant/components/cover/.translations/no.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} er lukket", + "is_closing": "{entity_name} lukker", + "is_open": "{entity_name} er \u00e5pen", + "is_opening": "{entity_name} \u00e5pnes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 498c8dd18d6..6d36d6ab39d 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -48,27 +48,27 @@ "button_2": "drugi przycisk", "button_3": "trzeci przycisk", "button_4": "czwarty przycisk", - "close": "zamkni\u0119cie", - "dim_down": "zmniejszenie jasno\u015bci", - "dim_up": "zwi\u0119kszenie jasno\u015bci", + "close": "nast\u0105pi zamkni\u0119cie", + "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci", + "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci", "left": "w lewo", "open": "otwarcie", "right": "w prawo", - "turn_off": "wy\u0142\u0105czenie", - "turn_on": "wy\u0142\u0105czenie" + "turn_off": "nast\u0105pi wy\u0142\u0105czenie", + "turn_on": "nast\u0105pi w\u0142\u0105czenie" }, "trigger_type": { - "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_rotated": "przycisk obr\u00f3cony \"{subtype}\"", - "remote_button_rotation_stopped": "obr\u00f3t przycisku \"{subtype}\" zatrzymany", - "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", - "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty", - "remote_gyro_activated": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" + "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_rotated": "przycisk zostanie obr\u00f3cony \"{subtype}\"", + "remote_button_rotation_stopped": "nast\u0105pi zatrzymanie obrotu przycisku \"{subtype}\"", + "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty", + "remote_gyro_activated": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem" } }, "options": { diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 33a38fc930e..05589210dba 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -10,8 +10,8 @@ "is_on": "\u015bwiat\u0142o {entity_name} jest w\u0142\u0105czone" }, "trigger_type": { - "turned_off": "wy\u0142\u0105czenie {entity_name}", - "turned_on": "w\u0142\u0105czenie {entity_name}" + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/en.json b/homeassistant/components/lock/.translations/en.json new file mode 100644 index 00000000000..a4b69197a91 --- /dev/null +++ b/homeassistant/components/lock/.translations/en.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} is locked", + "is_unlocked": "{entity_name} is unlocked" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/no.json b/homeassistant/components/lock/.translations/no.json new file mode 100644 index 00000000000..c60fc52ce53 --- /dev/null +++ b/homeassistant/components/lock/.translations/no.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} er l\u00e5st", + "is_unlocked": "{entity_name} er l\u00e5st opp" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 09b43f4100d..3d352aa2b58 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -12,8 +12,8 @@ "turn_on": "prze\u0142\u0105cznik {entity_name} w\u0142\u0105czony" }, "trigger_type": { - "turned_off": "wy\u0142\u0105czenie {entity_name}", - "turned_on": "w\u0142\u0105czenie {entity_name}" + "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", + "turned_on": "nast\u0105pi w\u0142\u0105czenie {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json index 0e1b7028dbb..4189ea6d9be 100644 --- a/homeassistant/components/zha/.translations/pl.json +++ b/homeassistant/components/zha/.translations/pl.json @@ -30,9 +30,9 @@ "button_4": "czwarty przycisk", "button_5": "pi\u0105ty przycisk", "button_6": "sz\u00f3sty przycisk", - "close": "zamkni\u0119cie", - "dim_down": "zmniejszenie jasno\u015bci", - "dim_up": "zwi\u0119kszenie jasno\u015bci", + "close": "nast\u0105pi zamkni\u0119cie", + "dim_down": "nast\u0105pi zmniejszenie jasno\u015bci", + "dim_up": "nast\u0105pi zwi\u0119kszenie jasno\u015bci", "face_1": "z aktywowan\u0105 twarz\u0105 1", "face_2": "z aktywowan\u0105 twarz\u0105 2", "face_3": "z aktywowan\u0105 twarz\u0105 3", @@ -43,25 +43,25 @@ "left": "w lewo", "open": "otwarcie", "right": "w prawo", - "turn_off": "wy\u0142\u0105czenie", - "turn_on": "w\u0142\u0105czenie" + "turn_off": "nast\u0105pi wy\u0142\u0105czenie", + "turn_on": "nast\u0105pi w\u0142\u0105czenie" }, "trigger_type": { - "device_dropped": "upadek urz\u0105dzenia", - "device_flipped": "odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", - "device_knocked": "pukni\u0119cie urz\u0105dzenia \"{subtype}\"", - "device_rotated": "obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", - "device_shaken": "potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", - "device_slid": "przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", - "device_tilted": "przechylenie urz\u0105dzenia", - "remote_button_double_press": "przycisk \"{subtype}\" podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "przycisk \"{subtype}\" zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "device_dropped": "nast\u0105pi upadek urz\u0105dzenia", + "device_flipped": "nast\u0105pi odwr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_knocked": "nast\u0105pi pukni\u0119cie w urz\u0105dzenie \"{subtype}\"", + "device_rotated": "nast\u0105pi obr\u00f3cenie urz\u0105dzenia \"{subtype}\"", + "device_shaken": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", + "device_slid": "nast\u0105pi przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", + "device_tilted": "nast\u0105pi przechylenie urz\u0105dzenia", + "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "przycisk \"{subtype}\" pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_short_press": "przycisk \"{subtype}\" naci\u015bni\u0119ty", - "remote_button_short_release": "przycisk \"{subtype}\" zwolniony", - "remote_button_triple_press": "przycisk \"{subtype}\" trzykrotnie naci\u015bni\u0119ty" + "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } } } \ No newline at end of file From 1fa6d9887e5b62b7861e0bfbbe0cc1c15176257e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 04:59:31 +0200 Subject: [PATCH 228/639] Move imports in tts component (#27565) * move imports in tts components * fix: order of imports --- homeassistant/components/tts/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 3e7900502d6..2ce0e18bee5 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -11,17 +11,18 @@ import re from typing import Optional from aiohttp import web +import mutagen import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as DOMAIN_MP, MEDIA_TYPE_MUSIC, SERVICE_PLAY_MEDIA, ) -from homeassistant.components.media_player.const import DOMAIN as DOMAIN_MP -from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, CONF_PLATFORM +from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, ENTITY_MATCH_ALL from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_per_platform @@ -29,7 +30,6 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_prepare_setup_platform - # mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -433,7 +433,6 @@ class SpeechManager: Async friendly. """ - import mutagen data_bytes = io.BytesIO(data) data_bytes.name = filename From a8f43843bffc3b5a41803d0bf3c601352f41199d Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Oct 2019 05:00:48 +0200 Subject: [PATCH 229/639] Filled services.yaml for browser integration (#27563) * Filled services.yaml for browser integration * Update services.yaml --- homeassistant/components/browser/services.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/components/browser/services.yaml b/homeassistant/components/browser/services.yaml index e69de29bb2d..460def22dc1 100644 --- a/homeassistant/components/browser/services.yaml +++ b/homeassistant/components/browser/services.yaml @@ -0,0 +1,6 @@ +browse_url: + description: Open a URL in the default browser on the host machine of Home Assistant. + fields: + url: + description: The URL to open. + example: "https://www.home-assistant.io" From 25bec13335889846844a6ebd3dadfb05bf7a3bf6 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Oct 2019 05:01:12 +0200 Subject: [PATCH 230/639] Filled services.yaml for logbook integration (#27560) --- homeassistant/components/logbook/services.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/logbook/services.yaml b/homeassistant/components/logbook/services.yaml index e69de29bb2d..08c463feed2 100644 --- a/homeassistant/components/logbook/services.yaml +++ b/homeassistant/components/logbook/services.yaml @@ -0,0 +1,15 @@ +log: + description: Create a custom entry in your logbook. + fields: + name: + description: Custom name for an entity, can be referenced with entity_id + example: "Kitchen" + message: + description: Message of the custom logbook entry + example: "is being used" + entity_id: + description: Entity to reference in custom logbook entry [Optional] + example: "light.kitchen" + domain: + description: Icon of domain to display in custom logbook entry [Optional] + example: "light" \ No newline at end of file From df646f5db100333534a45a7ab66e596171721f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 05:02:29 +0200 Subject: [PATCH 231/639] Move imports in tikteck component (#27568) * move imports in tikteck component * fix: order of imports --- homeassistant/components/tikteck/light.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tikteck/light.py b/homeassistant/components/tikteck/light.py index 013e5276f49..6c623f29f18 100644 --- a/homeassistant/components/tikteck/light.py +++ b/homeassistant/components/tikteck/light.py @@ -1,17 +1,18 @@ """Support for Tikteck lights.""" import logging +import tikteck import voluptuous as vol -from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PASSWORD import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -48,7 +49,6 @@ class TikteckLight(Light): def __init__(self, device): """Initialize the light.""" - import tikteck self._name = device["name"] self._address = device["address"] From 6cbc9d6abb3db99ca1db2521d0c8e725c54139bb Mon Sep 17 00:00:00 2001 From: chriscla Date: Sat, 12 Oct 2019 20:18:30 -0700 Subject: [PATCH 232/639] Fixing nzbget units display (#27521) --- homeassistant/components/nzbget/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nzbget/sensor.py b/homeassistant/components/nzbget/sensor.py index ce1fda0839e..20b49a492f3 100644 --- a/homeassistant/components/nzbget/sensor.py +++ b/homeassistant/components/nzbget/sensor.py @@ -34,9 +34,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = discovery_info["client_name"] devices = [] - for sensor_type, sensor_config in SENSOR_TYPES.items(): + for sensor_config in SENSOR_TYPES.values(): new_sensor = NZBGetSensor( - nzbget_data, sensor_type, name, sensor_config[0], sensor_config[1] + nzbget_data, sensor_config[0], name, sensor_config[1], sensor_config[2] ) devices.append(new_sensor) @@ -50,8 +50,8 @@ class NZBGetSensor(Entity): self, nzbget_data, sensor_type, client_name, sensor_name, unit_of_measurement ): """Initialize a new NZBGet sensor.""" - self._name = f"{client_name} {sensor_type}" - self.type = sensor_name + self._name = f"{client_name} {sensor_name}" + self.type = sensor_type self.client_name = client_name self.nzbget_data = nzbget_data self._state = None From 7aae1065256848677ff7f9a54a99a484d9b7594c Mon Sep 17 00:00:00 2001 From: foxy82 Date: Sun, 13 Oct 2019 09:59:35 +0100 Subject: [PATCH 233/639] Fix pioneer volume when using onkyo component (#27218) * Fix Onkyo when using pioneer AV receiver so it can use max volume of 164 * Update media_player.py Change to make receiver max volume configurable * Update manifest.json Update to latest onkyo-eiscp with a fix required for Pionner AVR * Fix Onkyo when using pioneer AV receiver so it can use max volume of 164 * Fix Onkyo when using pioneer AV receiver so it can use max volume of 164 * Format * Requirements all * Fix CI errors * Black --- homeassistant/components/onkyo/manifest.json | 2 +- .../components/onkyo/media_player.py | 71 +++++++++++++++---- requirements_all.txt | 2 +- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/onkyo/manifest.json b/homeassistant/components/onkyo/manifest.json index 5bb116dece8..ef28c14a8b0 100644 --- a/homeassistant/components/onkyo/manifest.json +++ b/homeassistant/components/onkyo/manifest.json @@ -3,7 +3,7 @@ "name": "Onkyo", "documentation": "https://www.home-assistant.io/integrations/onkyo", "requirements": [ - "onkyo-eiscp==1.2.4" + "onkyo-eiscp==1.2.7" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index d6117283da7..86f0f418c3f 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -31,9 +31,11 @@ _LOGGER = logging.getLogger(__name__) CONF_SOURCES = "sources" CONF_MAX_VOLUME = "max_volume" +CONF_RECEIVER_MAX_VOLUME = "receiver_max_volume" DEFAULT_NAME = "Onkyo Receiver" -SUPPORTED_MAX_VOLUME = 80 +SUPPORTED_MAX_VOLUME = 100 +DEFAULT_RECEIVER_MAX_VOLUME = 80 SUPPORT_ONKYO = ( SUPPORT_VOLUME_SET @@ -77,8 +79,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_HOST): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_MAX_VOLUME, default=SUPPORTED_MAX_VOLUME): vol.All( - vol.Coerce(int), vol.Range(min=1, max=SUPPORTED_MAX_VOLUME) + vol.Coerce(int), vol.Range(min=1, max=100) ), + vol.Optional( + CONF_RECEIVER_MAX_VOLUME, default=DEFAULT_RECEIVER_MAX_VOLUME + ): vol.All(vol.Coerce(int), vol.Range(min=0)), vol.Optional(CONF_SOURCES, default=DEFAULT_SOURCES): {cv.string: cv.string}, } ) @@ -163,6 +168,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): config.get(CONF_SOURCES), name=config.get(CONF_NAME), max_volume=config.get(CONF_MAX_VOLUME), + receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), ) ) KNOWN_HOSTS.append(host) @@ -178,6 +184,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): receiver, config.get(CONF_SOURCES), name=f"{config[CONF_NAME]} Zone 2", + max_volume=config.get(CONF_MAX_VOLUME), + receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), ) ) # Add Zone3 if available @@ -189,6 +197,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): receiver, config.get(CONF_SOURCES), name=f"{config[CONF_NAME]} Zone 3", + max_volume=config.get(CONF_MAX_VOLUME), + receiver_max_volume=config.get(CONF_RECEIVER_MAX_VOLUME), ) ) except OSError: @@ -204,7 +214,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class OnkyoDevice(MediaPlayerDevice): """Representation of an Onkyo device.""" - def __init__(self, receiver, sources, name=None, max_volume=SUPPORTED_MAX_VOLUME): + def __init__( + self, + receiver, + sources, + name=None, + max_volume=SUPPORTED_MAX_VOLUME, + receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME, + ): """Initialize the Onkyo Receiver.""" self._receiver = receiver self._muted = False @@ -214,6 +231,7 @@ class OnkyoDevice(MediaPlayerDevice): name or f"{receiver.info['model_name']}_{receiver.info['identifier']}" ) self._max_volume = max_volume + self._receiver_max_volume = receiver_max_volume self._current_source = None self._source_list = list(sources.values()) self._source_mapping = sources @@ -270,7 +288,10 @@ class OnkyoDevice(MediaPlayerDevice): del self._attributes[ATTR_PRESET] self._muted = bool(mute_raw[1] == "on") - self._volume = volume_raw[1] / self._max_volume + # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100) + self._volume = ( + volume_raw[1] / self._receiver_max_volume * (self._max_volume / 100) + ) if not hdmi_out_raw: return @@ -324,10 +345,15 @@ class OnkyoDevice(MediaPlayerDevice): """ Set volume level, input is range 0..1. - Onkyo ranges from 1-80 however 80 is usually far too loud - so allow the user to specify the upper range with CONF_MAX_VOLUME + However full volume on the amp is usually far too loud so allow the user to specify the upper range + with CONF_MAX_VOLUME. we change as per max_volume set by user. This means that if max volume is 80 then full + volume in HA will give 80% volume on the receiver. Then we convert + that to the correct scale for the receiver. """ - self.command(f"volume {int(volume * self._max_volume)}") + # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL + self.command( + f"volume {int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" + ) def volume_up(self): """Increase volume by 1 step.""" @@ -368,11 +394,19 @@ class OnkyoDevice(MediaPlayerDevice): class OnkyoDeviceZone(OnkyoDevice): """Representation of an Onkyo device's extra zone.""" - def __init__(self, zone, receiver, sources, name=None): + def __init__( + self, + zone, + receiver, + sources, + name=None, + max_volume=SUPPORTED_MAX_VOLUME, + receiver_max_volume=DEFAULT_RECEIVER_MAX_VOLUME, + ): """Initialize the Zone with the zone identifier.""" self._zone = zone self._supports_volume = True - super().__init__(receiver, sources, name) + super().__init__(receiver, sources, name, max_volume, receiver_max_volume) def update(self): """Get the latest state from the device.""" @@ -419,7 +453,10 @@ class OnkyoDeviceZone(OnkyoDevice): elif ATTR_PRESET in self._attributes: del self._attributes[ATTR_PRESET] if self._supports_volume: - self._volume = volume_raw[1] / 80.0 + # AMP_VOL/MAX_RECEIVER_VOL*(MAX_VOL/100) + self._volume = ( + volume_raw[1] / self._receiver_max_volume * (self._max_volume / 100) + ) @property def supported_features(self): @@ -433,8 +470,18 @@ class OnkyoDeviceZone(OnkyoDevice): self.command(f"zone{self._zone}.power=standby") def set_volume_level(self, volume): - """Set volume level, input is range 0..1. Onkyo ranges from 1-80.""" - self.command(f"zone{self._zone}.volume={int(volume * 80)}") + """ + Set volume level, input is range 0..1. + + However full volume on the amp is usually far too loud so allow the user to specify the upper range + with CONF_MAX_VOLUME. we change as per max_volume set by user. This means that if max volume is 80 then full + volume in HA will give 80% volume on the receiver. Then we convert + that to the correct scale for the receiver. + """ + # HA_VOL * (MAX VOL / 100) * MAX_RECEIVER_VOL + self.command( + f"zone{self._zone}.volume={int(volume * (self._max_volume / 100) * self._receiver_max_volume)}" + ) def volume_up(self): """Increase volume by 1 step.""" diff --git a/requirements_all.txt b/requirements_all.txt index f5b4bf38da3..dfdf84bf6ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -890,7 +890,7 @@ oauth2client==4.0.0 oemthermostat==1.1 # homeassistant.components.onkyo -onkyo-eiscp==1.2.4 +onkyo-eiscp==1.2.7 # homeassistant.components.onvif onvif-zeep-async==0.2.0 From b570be47ca253f06e357eea49bbc609012bb87e7 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 Oct 2019 12:46:43 +0200 Subject: [PATCH 234/639] Upgrade alpha_vantage to 2.1.1 (#27580) * Upgrade alpha_vantage to 2.1.1 * Move imports --- homeassistant/components/alpha_vantage/manifest.json | 2 +- homeassistant/components/alpha_vantage/sensor.py | 7 +++---- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 9ac8d1ea1e0..1213bb12e74 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -3,7 +3,7 @@ "name": "Alpha vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "requirements": [ - "alpha_vantage==2.1.0" + "alpha_vantage==2.1.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/alpha_vantage/sensor.py b/homeassistant/components/alpha_vantage/sensor.py index 188567e4cf4..da29e4e25e1 100644 --- a/homeassistant/components/alpha_vantage/sensor.py +++ b/homeassistant/components/alpha_vantage/sensor.py @@ -3,6 +3,8 @@ from datetime import timedelta import logging import voluptuous as vol +from alpha_vantage.timeseries import TimeSeries +from alpha_vantage.foreignexchange import ForeignExchange from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_CURRENCY, CONF_NAME @@ -62,15 +64,12 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Alpha Vantage sensor.""" - from alpha_vantage.timeseries import TimeSeries - from alpha_vantage.foreignexchange import ForeignExchange - api_key = config.get(CONF_API_KEY) symbols = config.get(CONF_SYMBOLS, []) conversions = config.get(CONF_FOREIGN_EXCHANGE, []) if not symbols and not conversions: - msg = "Warning: No symbols or currencies configured." + msg = "No symbols or currencies configured." hass.components.persistent_notification.create(msg, "Sensor alpha_vantage") _LOGGER.warning(msg) return diff --git a/requirements_all.txt b/requirements_all.txt index dfdf84bf6ed..b5ad094401f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -194,7 +194,7 @@ aladdin_connect==0.3 alarmdecoder==1.13.2 # homeassistant.components.alpha_vantage -alpha_vantage==2.1.0 +alpha_vantage==2.1.1 # homeassistant.components.ambiclimate ambiclimate==0.2.1 From bb2a1cd439493934ef1925a7a129a891c23879a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 14:42:29 +0200 Subject: [PATCH 235/639] Move imports in thermoworks_smoke component (#27586) --- .../components/thermoworks_smoke/sensor.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/thermoworks_smoke/sensor.py b/homeassistant/components/thermoworks_smoke/sensor.py index 70a16287fcc..d5af021108a 100644 --- a/homeassistant/components/thermoworks_smoke/sensor.py +++ b/homeassistant/components/thermoworks_smoke/sensor.py @@ -9,18 +9,21 @@ https://home-assistant.io/components/sensor.thermoworks_smoke/ import logging from requests import RequestException +from requests.exceptions import HTTPError +from stringcase import camelcase, snakecase +import thermoworks_smoke import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_FAHRENHEIT, - CONF_EMAIL, - CONF_PASSWORD, - CONF_MONITORED_CONDITIONS, - CONF_EXCLUDE, ATTR_BATTERY_LEVEL, + CONF_EMAIL, + CONF_EXCLUDE, + CONF_MONITORED_CONDITIONS, + CONF_PASSWORD, + TEMP_FAHRENHEIT, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -65,8 +68,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the thermoworks sensor.""" - import thermoworks_smoke - from requests.exceptions import HTTPError email = config[CONF_EMAIL] password = config[CONF_PASSWORD] @@ -144,7 +145,6 @@ class ThermoworksSmokeSensor(Entity): def update(self): """Get the monitored data from firebase.""" - from stringcase import camelcase, snakecase try: values = self.mgr.data(self.serial) From bbafeb5da21179f38aaeec5ed36dcd9be0b73a33 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 Oct 2019 14:46:12 +0200 Subject: [PATCH 236/639] Upgrade pillow to 6.2.0 (#27581) --- .../components/image_processing/manifest.json | 2 +- homeassistant/components/proxy/camera.py | 14 ++++--------- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/__init__.py | 2 +- .../components/qrcode/image_processing.py | 21 ++++++++++--------- homeassistant/components/qrcode/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 8 files changed, 21 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/image_processing/manifest.json b/homeassistant/components/image_processing/manifest.json index 4a96e9828cb..6a88a358f1d 100644 --- a/homeassistant/components/image_processing/manifest.json +++ b/homeassistant/components/image_processing/manifest.json @@ -3,7 +3,7 @@ "name": "Image processing", "documentation": "https://www.home-assistant.io/integrations/image_processing", "requirements": [ - "pillow==6.1.0" + "pillow==6.2.0" ], "dependencies": [ "camera" diff --git a/homeassistant/components/proxy/camera.py b/homeassistant/components/proxy/camera.py index b1ce8ad7ac0..90487120ffe 100644 --- a/homeassistant/components/proxy/camera.py +++ b/homeassistant/components/proxy/camera.py @@ -1,12 +1,14 @@ """Proxy camera platform that enables image processing of camera data.""" import asyncio +from datetime import timedelta +import io import logging -from datetime import timedelta +from PIL import Image import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.const import CONF_ENTITY_ID, CONF_NAME, CONF_MODE +from homeassistant.const import CONF_ENTITY_ID, CONF_MODE, CONF_NAME from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv import homeassistant.util.dt as dt_util @@ -58,9 +60,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _precheck_image(image, opts): """Perform some pre-checks on the given image.""" - from PIL import Image - import io - if not opts: raise ValueError() try: @@ -77,9 +76,6 @@ def _precheck_image(image, opts): def _resize_image(image, opts): """Resize image.""" - from PIL import Image - import io - try: img = _precheck_image(image, opts) except ValueError: @@ -125,8 +121,6 @@ def _resize_image(image, opts): def _crop_image(image, opts): """Crop image.""" - import io - try: img = _precheck_image(image, opts) except ValueError: diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index c67fd4afc09..e3f62514801 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,7 +3,7 @@ "name": "Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", "requirements": [ - "pillow==6.1.0" + "pillow==6.2.0" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/qrcode/__init__.py b/homeassistant/components/qrcode/__init__.py index bcc1985a2dc..55b1a2a9d6b 100644 --- a/homeassistant/components/qrcode/__init__.py +++ b/homeassistant/components/qrcode/__init__.py @@ -1 +1 @@ -"""The qrcode component.""" +"""The QR code component.""" diff --git a/homeassistant/components/qrcode/image_processing.py b/homeassistant/components/qrcode/image_processing.py index 5e1b7c11b25..018f074a6d2 100644 --- a/homeassistant/components/qrcode/image_processing.py +++ b/homeassistant/components/qrcode/image_processing.py @@ -1,15 +1,20 @@ -"""Support for the QR image processing.""" -from homeassistant.core import split_entity_id +"""Support for the QR code image processing.""" +import io + +from PIL import Image +from pyzbar import pyzbar + from homeassistant.components.image_processing import ( - ImageProcessingEntity, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + ImageProcessingEntity, ) +from homeassistant.core import split_entity_id def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the demo image processing platform.""" + """Set up the QR code image processing platform.""" # pylint: disable=unused-argument entities = [] for camera in config[CONF_SOURCE]: @@ -19,7 +24,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class QrEntity(ImageProcessingEntity): - """QR image processing entity.""" + """A QR image processing entity.""" def __init__(self, camera_entity, name): """Initialize QR image processing entity.""" @@ -49,10 +54,6 @@ class QrEntity(ImageProcessingEntity): def process_image(self, image): """Process image.""" - import io - from pyzbar import pyzbar - from PIL import Image - stream = io.BytesIO(image) img = Image.open(stream) diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 87e16f62987..a3130070cc3 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -3,7 +3,7 @@ "name": "Qrcode", "documentation": "https://www.home-assistant.io/integrations/qrcode", "requirements": [ - "pillow==6.1.0", + "pillow==6.2.0", "pyzbar==0.1.7" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index b5ad094401f..a4623fe8bfb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -953,7 +953,7 @@ pilight==0.1.1 # homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -pillow==6.1.0 +pillow==6.2.0 # homeassistant.components.dominos pizzapi==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 143f219fecf..5c9f80e408c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -328,7 +328,7 @@ pilight==0.1.1 # homeassistant.components.image_processing # homeassistant.components.proxy # homeassistant.components.qrcode -pillow==6.1.0 +pillow==6.2.0 # homeassistant.components.plex plexapi==3.0.6 From 930182a7cbcfa3b240a84fd73137b8b694ea6a6f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Sun, 13 Oct 2019 14:54:38 +0200 Subject: [PATCH 237/639] Move import in deutsche_bahn integration (#27579) * Moved import in deutsche_bahn integration * Moved import schiene before PLATFORM_SCHEMA --- homeassistant/components/deutsche_bahn/sensor.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/deutsche_bahn/sensor.py b/homeassistant/components/deutsche_bahn/sensor.py index fbe0efa15ac..ad7b40f78db 100644 --- a/homeassistant/components/deutsche_bahn/sensor.py +++ b/homeassistant/components/deutsche_bahn/sensor.py @@ -4,6 +4,8 @@ import logging import voluptuous as vol +import schiene + from homeassistant.components.sensor import PLATFORM_SCHEMA import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -89,7 +91,6 @@ class SchieneData: def __init__(self, start, goal, offset, only_direct): """Initialize the sensor.""" - import schiene self.start = start self.goal = goal From 627ca3182a927af91af871b5fd4e31e7ca4d31e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 14:56:02 +0200 Subject: [PATCH 238/639] Move imports in thingspeak component (#27585) --- homeassistant/components/thingspeak/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/thingspeak/__init__.py b/homeassistant/components/thingspeak/__init__.py index 0893b3311bb..1870a317752 100644 --- a/homeassistant/components/thingspeak/__init__.py +++ b/homeassistant/components/thingspeak/__init__.py @@ -2,6 +2,7 @@ import logging from requests.exceptions import RequestException +import thingspeak import voluptuous as vol from homeassistant.const import ( @@ -36,7 +37,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Thingspeak environment.""" - import thingspeak conf = config[DOMAIN] api_key = conf.get(CONF_API_KEY) From 989c3581ac80e4fb5b92f3d8b0bbbe63a567cbe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 15:01:13 +0200 Subject: [PATCH 239/639] Move imports in tplink_lte component (#27583) --- homeassistant/components/tplink_lte/__init__.py | 3 +-- homeassistant/components/tplink_lte/notify.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tplink_lte/__init__.py b/homeassistant/components/tplink_lte/__init__.py index 215dd5c94b2..e495a14a38c 100644 --- a/homeassistant/components/tplink_lte/__init__.py +++ b/homeassistant/components/tplink_lte/__init__.py @@ -4,6 +4,7 @@ import logging import aiohttp import attr +import tp_connected import voluptuous as vol from homeassistant.const import ( @@ -106,7 +107,6 @@ async def async_setup(hass, config): async def _setup_lte(hass, lte_config, delay=0): """Set up a TP-Link LTE modem.""" - import tp_connected host = lte_config[CONF_HOST] password = lte_config[CONF_PASSWORD] @@ -145,7 +145,6 @@ async def _login(hass, modem_data, password): async def _retry_login(hass, modem_data, password): """Sleep and retry setup.""" - import tp_connected _LOGGER.warning("Could not connect to %s. Will keep trying.", modem_data.host) diff --git a/homeassistant/components/tplink_lte/notify.py b/homeassistant/components/tplink_lte/notify.py index e677b42a511..478b3e998c0 100644 --- a/homeassistant/components/tplink_lte/notify.py +++ b/homeassistant/components/tplink_lte/notify.py @@ -2,6 +2,7 @@ import logging import attr +import tp_connected from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService from homeassistant.const import CONF_RECIPIENT @@ -27,7 +28,6 @@ class TplinkNotifyService(BaseNotificationService): async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" - import tp_connected modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config) if not modem_data: From 5e4b33c740478819de44b3446d596d6b108806fb Mon Sep 17 00:00:00 2001 From: bouni Date: Sun, 13 Oct 2019 15:09:44 +0200 Subject: [PATCH 240/639] Move imports in bme280 component (#27505) --- homeassistant/components/bme280/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bme280/sensor.py b/homeassistant/components/bme280/sensor.py index ee4e1731156..b9bc18e6abf 100644 --- a/homeassistant/components/bme280/sensor.py +++ b/homeassistant/components/bme280/sensor.py @@ -3,6 +3,9 @@ from datetime import timedelta from functools import partial import logging +import smbus # pylint: disable=import-error +from i2csense.bme280 import BME280 # pylint: disable=import-error + import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -76,8 +79,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the BME280 sensor.""" - import smbus # pylint: disable=import-error - from i2csense.bme280 import BME280 # pylint: disable=import-error SENSOR_TYPES[SENSOR_TEMP][1] = hass.config.units.temperature_unit name = config.get(CONF_NAME) From d96cd4c4ea1999f87db5b660ec225ad26cb3d471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 17:05:04 +0200 Subject: [PATCH 241/639] Move imports in tplink component (#27567) * move imports in tplink component * fix: order of imports * fix: tplink tests * fix: import order in tests * fix: tests formatting --- homeassistant/components/tplink/__init__.py | 6 +- homeassistant/components/tplink/common.py | 5 +- .../components/tplink/config_flow.py | 6 +- .../components/tplink/device_tracker.py | 18 +++--- tests/components/tplink/test_init.py | 56 +++++++++++++------ 5 files changed, 55 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 075bffb9f26..85258b5e94e 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -3,20 +3,20 @@ import logging import voluptuous as vol -from homeassistant.const import CONF_HOST from homeassistant import config_entries +from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .common import ( - async_discover_devices, - get_static_devices, ATTR_CONFIG, CONF_DIMMER, CONF_DISCOVERY, CONF_LIGHT, CONF_SWITCH, SmartDevices, + async_discover_devices, + get_static_devices, ) _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 90895104170..75636c8dc28 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -1,10 +1,10 @@ """Common code for tplink.""" import asyncio -import logging from datetime import timedelta +import logging from typing import Any, Callable, List -from pyHS100 import SmartBulb, SmartDevice, SmartPlug, SmartDeviceException +from pyHS100 import Discover, SmartBulb, SmartDevice, SmartDeviceException, SmartPlug from homeassistant.helpers.typing import HomeAssistantType @@ -49,7 +49,6 @@ class SmartDevices: async def async_get_discoverable_devices(hass): """Return if there are devices that can be discovered.""" - from pyHS100 import Discover def discover(): devs = Discover.discover() diff --git a/homeassistant/components/tplink/config_flow.py b/homeassistant/components/tplink/config_flow.py index c4888ecee96..40583294bfd 100644 --- a/homeassistant/components/tplink/config_flow.py +++ b/homeassistant/components/tplink/config_flow.py @@ -1,9 +1,9 @@ """Config flow for TP-Link.""" -from homeassistant.helpers import config_entry_flow from homeassistant import config_entries -from .const import DOMAIN -from .common import async_get_discoverable_devices +from homeassistant.helpers import config_entry_flow +from .common import async_get_discoverable_devices +from .const import DOMAIN config_entry_flow.register_discovery_flow( DOMAIN, diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py index f6921efed91..ce17f6e465f 100644 --- a/homeassistant/components/tplink/device_tracker.py +++ b/homeassistant/components/tplink/device_tracker.py @@ -7,18 +7,19 @@ import re from aiohttp.hdrs import ( ACCEPT, - COOKIE, - PRAGMA, - REFERER, - CONNECTION, - KEEP_ALIVE, - USER_AGENT, - CONTENT_TYPE, - CACHE_CONTROL, ACCEPT_ENCODING, ACCEPT_LANGUAGE, + CACHE_CONTROL, + CONNECTION, + CONTENT_TYPE, + COOKIE, + KEEP_ALIVE, + PRAGMA, + REFERER, + USER_AGENT, ) import requests +from tplink.tplink import TpLinkClient import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -88,7 +89,6 @@ class TplinkDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from tplink.tplink import TpLinkClient host = config[CONF_HOST] password = config[CONF_PASSWORD] diff --git a/tests/components/tplink/test_init.py b/tests/components/tplink/test_init.py index df9bf2c2ca2..9428bf05483 100644 --- a/tests/components/tplink/test_init.py +++ b/tests/components/tplink/test_init.py @@ -1,21 +1,22 @@ """Tests for the TP-Link component.""" -from typing import Dict, Any +from typing import Any, Dict from unittest.mock import MagicMock, patch +from pyHS100 import SmartBulb, SmartDevice, SmartDeviceException, SmartPlug import pytest -from pyHS100 import SmartPlug, SmartBulb, SmartDevice, SmartDeviceException from homeassistant import config_entries, data_entry_flow from homeassistant.components import tplink from homeassistant.components.tplink.common import ( - CONF_DISCOVERY, CONF_DIMMER, + CONF_DISCOVERY, CONF_LIGHT, CONF_SWITCH, ) from homeassistant.const import CONF_HOST from homeassistant.setup import async_setup_component -from tests.common import MockDependency, MockConfigEntry, mock_coro + +from tests.common import MockConfigEntry, MockDependency, mock_coro MOCK_PYHS100 = MockDependency("pyHS100") @@ -25,7 +26,10 @@ async def test_creating_entry_tries_discover(hass): with MOCK_PYHS100, patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), - ) as mock_setup, patch("pyHS100.Discover.discover", return_value={"host": 1234}): + ) as mock_setup, patch( + "homeassistant.components.tplink.common.Discover.discover", + return_value={"host": 1234}, + ): result = await hass.config_entries.flow.async_init( tplink.DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -43,7 +47,9 @@ async def test_creating_entry_tries_discover(hass): async def test_configuring_tplink_causes_discovery(hass): """Test that specifying empty config does discovery.""" - with MOCK_PYHS100, patch("pyHS100.Discover.discover") as discover: + with MOCK_PYHS100, patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover: discover.return_value = {"host": 1234} await async_setup_component(hass, tplink.DOMAIN, {tplink.DOMAIN: {}}) await hass.async_block_till_done() @@ -61,8 +67,10 @@ async def test_configuring_tplink_causes_discovery(hass): @pytest.mark.parametrize("count", [1, 2, 3]) async def test_configuring_device_types(hass, name, cls, platform, count): """Test that light or switch platform list is filled correctly.""" - with patch("pyHS100.Discover.discover") as discover, patch( - "pyHS100.SmartDevice._query_helper" + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" ): discovery_data = { "123.123.123.{}".format(c): cls("123.123.123.123") for c in range(count) @@ -104,8 +112,10 @@ class UnknownSmartDevice(SmartDevice): async def test_configuring_devices_from_multiple_sources(hass): """Test static and discover devices are not duplicated.""" - with patch("pyHS100.Discover.discover") as discover, patch( - "pyHS100.SmartDevice._query_helper" + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" ): discover_device_fail = SmartPlug("123.123.123.123") discover_device_fail.get_sysinfo = MagicMock(side_effect=SmartDeviceException()) @@ -139,11 +149,15 @@ async def test_configuring_devices_from_multiple_sources(hass): async def test_is_dimmable(hass): """Test that is_dimmable switches are correctly added as lights.""" - with patch("pyHS100.Discover.discover") as discover, patch( + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), - ) as setup, patch("pyHS100.SmartDevice._query_helper"), patch( - "pyHS100.SmartPlug.is_dimmable", True + ) as setup, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", True ): dimmable_switch = SmartPlug("123.123.123.123") discover.return_value = {"host": dimmable_switch} @@ -162,7 +176,9 @@ async def test_configuring_discovery_disabled(hass): with MOCK_PYHS100, patch( "homeassistant.components.tplink.async_setup_entry", return_value=mock_coro(True), - ) as mock_setup, patch("pyHS100.Discover.discover", return_value=[]) as discover: + ) as mock_setup, patch( + "homeassistant.components.tplink.common.Discover.discover", return_value=[] + ) as discover: await async_setup_component( hass, tplink.DOMAIN, {tplink.DOMAIN: {tplink.CONF_DISCOVERY: False}} ) @@ -182,8 +198,10 @@ async def test_platforms_are_initialized(hass): } } - with patch("pyHS100.Discover.discover") as discover, patch( - "pyHS100.SmartDevice._query_helper" + with patch( + "homeassistant.components.tplink.common.Discover.discover" + ) as discover, patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" ), patch( "homeassistant.components.tplink.light.async_setup_entry", return_value=mock_coro(True), @@ -191,7 +209,7 @@ async def test_platforms_are_initialized(hass): "homeassistant.components.tplink.switch.async_setup_entry", return_value=mock_coro(True), ) as switch_setup, patch( - "pyHS100.SmartPlug.is_dimmable", False + "homeassistant.components.tplink.common.SmartPlug.is_dimmable", False ): # patching is_dimmable is necessray to avoid misdetection as light. await async_setup_component(hass, tplink.DOMAIN, config) @@ -221,7 +239,9 @@ async def test_unload(hass, platform): entry = MockConfigEntry(domain=tplink.DOMAIN) entry.add_to_hass(hass) - with patch("pyHS100.SmartDevice._query_helper"), patch( + with patch( + "homeassistant.components.tplink.common.SmartDevice._query_helper" + ), patch( "homeassistant.components.tplink.{}" ".async_setup_entry".format(platform), return_value=mock_coro(True), ) as light_setup: From 1774a1427ba48994fab875b82aff8dea49995796 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Sun, 13 Oct 2019 11:01:04 -0700 Subject: [PATCH 242/639] Add abode config entries and device registry (#26699) * Adds support for config entries and device registry * Fixing string formatting for logger * Add unit test for abode config flow * Fix for lights, only allow one config, add ability to unload entry * Fix for subscribing to hass_events on adding abode component * Several fixes from code review * Several fixes from second code review * Several fixes from third code review * Update documentation url to fix branch conflict * Fixes config flow and removes unused constants * Fix for switches, polling entry option, improved tests * Update .coveragerc, disable pylint W0611, remove polling from UI * Multiple fixes and edits to adhere to style guidelines * Removed unique_id * Minor correction for formatting error in rebase * Resolves issue causing CI to fail * Bump abodepy version * Add remove device callback and minor clean up * Fix incorrect method name * Docstring edits * Fix duplicate import issues from rebase * Add logout_listener attribute to AbodeSystem * Add additional test for complete coverage --- .coveragerc | 10 +- CODEOWNERS | 1 + homeassistant/components/abode/__init__.py | 190 +++++++++--------- .../components/abode/alarm_control_panel.py | 30 ++- .../components/abode/binary_sensor.py | 24 +-- homeassistant/components/abode/camera.py | 23 ++- homeassistant/components/abode/config_flow.py | 79 ++++++++ homeassistant/components/abode/const.py | 3 + homeassistant/components/abode/cover.py | 19 +- homeassistant/components/abode/light.py | 25 +-- homeassistant/components/abode/lock.py | 21 +- homeassistant/components/abode/manifest.json | 9 +- homeassistant/components/abode/sensor.py | 19 +- homeassistant/components/abode/strings.json | 22 ++ homeassistant/components/abode/switch.py | 25 +-- homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/abode/__init__.py | 1 + tests/components/abode/test_config_flow.py | 120 +++++++++++ 20 files changed, 433 insertions(+), 194 deletions(-) create mode 100644 homeassistant/components/abode/config_flow.py create mode 100644 homeassistant/components/abode/const.py create mode 100644 homeassistant/components/abode/strings.json create mode 100644 tests/components/abode/__init__.py create mode 100644 tests/components/abode/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index d241260fdf0..145350b6b19 100644 --- a/.coveragerc +++ b/.coveragerc @@ -10,7 +10,15 @@ omit = homeassistant/util/async.py # omit pieces of code that rely on external devices being present - homeassistant/components/abode/* + homeassistant/components/abode/__init__.py + homeassistant/components/abode/alarm_control_panel.py + homeassistant/components/abode/binary_sensor.py + homeassistant/components/abode/camera.py + homeassistant/components/abode/cover.py + homeassistant/components/abode/light.py + homeassistant/components/abode/lock.py + homeassistant/components/abode/sensor.py + homeassistant/components/abode/switch.py homeassistant/components/acer_projector/switch.py homeassistant/components/actiontec/device_tracker.py homeassistant/components/adguard/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index 070151d01e0..ea50d24095c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -13,6 +13,7 @@ homeassistant/util/* @home-assistant/core homeassistant/scripts/check_config.py @kellerza # Integrations +homeassistant/components/abode/* @shred86 homeassistant/components/adguard/* @frenck homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index b7f13d49b69..aeba437aceb 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -1,49 +1,36 @@ -"""Support for Abode Home Security system.""" -import logging +"""Support for the Abode Security System.""" +from asyncio import gather +from copy import deepcopy from functools import partial -from requests.exceptions import HTTPError, ConnectTimeout -import abodepy -import abodepy.helpers.constants as CONST +import logging + +from abodepy import Abode from abodepy.exceptions import AbodeException import abodepy.helpers.timeline as TIMELINE - +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DATE, - ATTR_TIME, ATTR_ENTITY_ID, - CONF_USERNAME, + ATTR_TIME, CONF_PASSWORD, - CONF_EXCLUDE, - CONF_NAME, - CONF_LIGHTS, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, - EVENT_HOMEASSISTANT_START, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery from homeassistant.helpers.entity import Entity +from .const import ATTRIBUTION, DOMAIN + _LOGGER = logging.getLogger(__name__) -ATTRIBUTION = "Data provided by goabode.com" - CONF_POLLING = "polling" -DOMAIN = "abode" DEFAULT_CACHEDB = "./abodepy_cache.pickle" -NOTIFICATION_ID = "abode_notification" -NOTIFICATION_TITLE = "Abode Security Setup" - -EVENT_ABODE_ALARM = "abode_alarm" -EVENT_ABODE_ALARM_END = "abode_alarm_end" -EVENT_ABODE_AUTOMATION = "abode_automation" -EVENT_ABODE_FAULT = "abode_panel_fault" -EVENT_ABODE_RESTORE = "abode_panel_restore" - SERVICE_SETTINGS = "change_setting" SERVICE_CAPTURE_IMAGE = "capture_image" SERVICE_TRIGGER = "trigger_quick_action" @@ -67,10 +54,7 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_POLLING, default=False): cv.boolean, - vol.Optional(CONF_EXCLUDE, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, - vol.Optional(CONF_LIGHTS, default=[]): ABODE_DEVICE_ID_LIST_SCHEMA, } ) }, @@ -100,73 +84,80 @@ ABODE_PLATFORMS = [ class AbodeSystem: """Abode System class.""" - def __init__(self, username, password, cache, name, polling, exclude, lights): + def __init__(self, abode, polling): """Initialize the system.""" - self.abode = abodepy.Abode( - username, - password, - auto_login=True, - get_devices=True, - get_automations=True, - cache_path=cache, - ) - self.name = name + self.abode = abode self.polling = polling - self.exclude = exclude - self.lights = lights self.devices = [] - - def is_excluded(self, device): - """Check if a device is configured to be excluded.""" - return device.device_id in self.exclude - - def is_automation_excluded(self, automation): - """Check if an automation is configured to be excluded.""" - return automation.automation_id in self.exclude - - def is_light(self, device): - """Check if a switch device is configured as a light.""" - - return device.generic_type == CONST.TYPE_LIGHT or ( - device.generic_type == CONST.TYPE_SWITCH and device.device_id in self.lights - ) + self.logout_listener = None -def setup(hass, config): - """Set up Abode component.""" +async def async_setup(hass, config): + """Set up Abode integration.""" + if DOMAIN not in config: + return True conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - name = conf.get(CONF_NAME) - polling = conf.get(CONF_POLLING) - exclude = conf.get(CONF_EXCLUDE) - lights = conf.get(CONF_LIGHTS) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=deepcopy(conf) + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Abode integration from a config entry.""" + username = config_entry.data.get(CONF_USERNAME) + password = config_entry.data.get(CONF_PASSWORD) + polling = config_entry.data.get(CONF_POLLING) try: cache = hass.config.path(DEFAULT_CACHEDB) - hass.data[DOMAIN] = AbodeSystem( - username, password, cache, name, polling, exclude, lights + abode = await hass.async_add_executor_job( + Abode, username, password, True, True, True, cache ) + hass.data[DOMAIN] = AbodeSystem(abode, polling) + except (AbodeException, ConnectTimeout, HTTPError) as ex: _LOGGER.error("Unable to connect to Abode: %s", str(ex)) - - hass.components.persistent_notification.create( - "Error: {}
" - "You will need to restart hass after fixing." - "".format(ex), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID, - ) return False - setup_hass_services(hass) - setup_hass_events(hass) - setup_abode_events(hass) + for platform in ABODE_PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, platform) + ) + + await setup_hass_events(hass) + await hass.async_add_executor_job(setup_hass_services, hass) + await hass.async_add_executor_job(setup_abode_events, hass) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + hass.services.async_remove(DOMAIN, SERVICE_SETTINGS) + hass.services.async_remove(DOMAIN, SERVICE_CAPTURE_IMAGE) + hass.services.async_remove(DOMAIN, SERVICE_TRIGGER) + + tasks = [] for platform in ABODE_PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) + tasks.append( + hass.config_entries.async_forward_entry_unload(config_entry, platform) + ) + + await gather(*tasks) + + await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.stop) + await hass.async_add_executor_job(hass.data[DOMAIN].abode.logout) + + hass.data[DOMAIN].logout_listener() + hass.data.pop(DOMAIN) return True @@ -223,13 +214,9 @@ def setup_hass_services(hass): ) -def setup_hass_events(hass): +async def setup_hass_events(hass): """Home Assistant start and stop callbacks.""" - def startup(event): - """Listen for push events.""" - hass.data[DOMAIN].abode.events.start() - def logout(event): """Logout of Abode.""" if not hass.data[DOMAIN].polling: @@ -239,9 +226,11 @@ def setup_hass_events(hass): _LOGGER.info("Logged out of Abode") if not hass.data[DOMAIN].polling: - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, startup) + await hass.async_add_executor_job(hass.data[DOMAIN].abode.events.start) - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout) + hass.data[DOMAIN].logout_listener = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, logout + ) def setup_abode_events(hass): @@ -282,30 +271,36 @@ class AbodeDevice(Entity): """Representation of an Abode device.""" def __init__(self, data, device): - """Initialize a sensor for Abode device.""" + """Initialize Abode device.""" self._data = data self._device = device async def async_added_to_hass(self): - """Subscribe Abode events.""" + """Subscribe to device events.""" self.hass.async_add_job( self._data.abode.events.add_device_callback, self._device.device_id, self._update_callback, ) + async def async_will_remove_from_hass(self): + """Unsubscribe from device events.""" + self.hass.async_add_job( + self._data.abode.events.remove_all_device_callbacks, self._device.device_id + ) + @property def should_poll(self): """Return the polling state.""" return self._data.polling def update(self): - """Update automation state.""" + """Update device and automation states.""" self._device.refresh() @property def name(self): - """Return the name of the sensor.""" + """Return the name of the device.""" return self._device.name @property @@ -319,6 +314,21 @@ class AbodeDevice(Entity): "device_type": self._device.type, } + @property + def unique_id(self): + """Return a unique ID to use for this device.""" + return self._device.device_uuid + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self._device.device_id)}, + "manufacturer": "Abode", + "name": self._device.name, + "device_type": self._device.type, + } + def _update_callback(self, device): """Update the device state.""" self.schedule_update_ha_state() @@ -353,7 +363,7 @@ class AbodeAutomation(Entity): @property def name(self): - """Return the name of the sensor.""" + """Return the name of the automation.""" return self._automation.name @property @@ -367,6 +377,6 @@ class AbodeAutomation(Entity): } def _update_callback(self, device): - """Update the device state.""" + """Update the automation state.""" self._automation.refresh() self.schedule_update_ha_state() diff --git a/homeassistant/components/abode/alarm_control_panel.py b/homeassistant/components/abode/alarm_control_panel.py index c5c10e65302..f774e773cb5 100644 --- a/homeassistant/components/abode/alarm_control_panel.py +++ b/homeassistant/components/abode/alarm_control_panel.py @@ -9,32 +9,31 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, ) -from . import ATTRIBUTION, DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import ATTRIBUTION, DOMAIN _LOGGER = logging.getLogger(__name__) ICON = "mdi:security" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up an alarm control panel for an Abode device.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] - alarm_devices = [AbodeAlarm(data, data.abode.get_alarm(), data.name)] - - data.devices.extend(alarm_devices) - - add_entities(alarm_devices) + async_add_entities( + [AbodeAlarm(data, await hass.async_add_executor_job(data.abode.get_alarm))] + ) class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): """An alarm_control_panel implementation for Abode.""" - def __init__(self, data, device, name): - """Initialize the alarm control panel.""" - super().__init__(data, device) - self._name = name - @property def icon(self): """Return the icon.""" @@ -65,11 +64,6 @@ class AbodeAlarm(AbodeDevice, alarm.AlarmControlPanel): """Send arm away command.""" self._device.set_away() - @property - def name(self): - """Return the name of the alarm.""" - return self._name or super().name - @property def device_state_attributes(self): """Return the state attributes.""" diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index 3ae7f41d84e..31f74448496 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -6,15 +6,20 @@ import abodepy.helpers.timeline as TIMELINE from homeassistant.components.binary_sensor import BinarySensorDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice +from . import AbodeAutomation, AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up a sensor for an Abode device.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a sensor for an Abode device.""" + data = hass.data[DOMAIN] device_types = [ CONST.TYPE_CONNECTIVITY, @@ -25,25 +30,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] devices = [] - for device in data.abode.get_devices(generic_type=device_types): - if data.is_excluded(device): - continue + for device in data.abode.get_devices(generic_type=device_types): devices.append(AbodeBinarySensor(data, device)) for automation in data.abode.get_automations(generic_type=CONST.TYPE_QUICK_ACTION): - if data.is_automation_excluded(automation): - continue - devices.append( AbodeQuickActionBinarySensor( data, automation, TIMELINE.AUTOMATION_EDIT_GROUP ) ) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): diff --git a/homeassistant/components/abode/camera.py b/homeassistant/components/abode/camera.py index f52bbe17475..e98a59a985c 100644 --- a/homeassistant/components/abode/camera.py +++ b/homeassistant/components/abode/camera.py @@ -2,35 +2,36 @@ from datetime import timedelta import logging -import requests import abodepy.helpers.constants as CONST import abodepy.helpers.timeline as TIMELINE +import requests from homeassistant.components.camera import Camera from homeassistant.util import Throttle -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=90) _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode camera devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up a camera for an Abode device.""" + + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_CAMERA): - if data.is_excluded(device): - continue - devices.append(AbodeCamera(data, device, TIMELINE.CAPTURE_IMAGE)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeCamera(AbodeDevice, Camera): diff --git a/homeassistant/components/abode/config_flow.py b/homeassistant/components/abode/config_flow.py new file mode 100644 index 00000000000..d8d914f7998 --- /dev/null +++ b/homeassistant/components/abode/config_flow.py @@ -0,0 +1,79 @@ +"""Config flow for the Abode Security System component.""" +import logging + +from abodepy import Abode +from abodepy.exceptions import AbodeException +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback + +from .const import DOMAIN # pylint: disable=W0611 + +CONF_POLLING = "polling" + +_LOGGER = logging.getLogger(__name__) + + +class AbodeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Config flow for Abode.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize.""" + self.data_schema = { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + if not user_input: + return self._show_form() + + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + polling = user_input.get(CONF_POLLING, False) + + try: + await self.hass.async_add_executor_job(Abode, username, password, True) + + except (AbodeException, ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Abode: %s", str(ex)) + if ex.errcode == 400: + return self._show_form({"base": "invalid_credentials"}) + return self._show_form({"base": "connection_error"}) + + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_POLLING: polling, + }, + ) + + @callback + def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema(self.data_schema), + errors=errors if errors else {}, + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + if self._async_current_entries(): + _LOGGER.warning("Only one configuration of abode is allowed.") + return self.async_abort(reason="single_instance_allowed") + + return await self.async_step_user(import_config) diff --git a/homeassistant/components/abode/const.py b/homeassistant/components/abode/const.py new file mode 100644 index 00000000000..35e74e154cf --- /dev/null +++ b/homeassistant/components/abode/const.py @@ -0,0 +1,3 @@ +"""Constants for the Abode Security System component.""" +DOMAIN = "abode" +ATTRIBUTION = "Data provided by goabode.com" diff --git a/homeassistant/components/abode/cover.py b/homeassistant/components/abode/cover.py index 13d46c53f73..ebe59ee45c7 100644 --- a/homeassistant/components/abode/cover.py +++ b/homeassistant/components/abode/cover.py @@ -5,26 +5,27 @@ import abodepy.helpers.constants as CONST from homeassistant.components.cover import CoverDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode cover devices.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_COVER): - if data.is_excluded(device): - continue - devices.append(AbodeCover(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeCover(AbodeDevice, CoverDevice): diff --git a/homeassistant/components/abode/light.py b/homeassistant/components/abode/light.py index 6551cba2ef1..163982d040e 100644 --- a/homeassistant/components/abode/light.py +++ b/homeassistant/components/abode/light.py @@ -18,30 +18,27 @@ from homeassistant.util.color import ( color_temperature_mired_to_kelvin, ) -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode light devices.""" - - data = hass.data[ABODE_DOMAIN] - - device_types = [CONST.TYPE_LIGHT, CONST.TYPE_SWITCH] + data = hass.data[DOMAIN] devices = [] - # Get all regular lights that are not excluded or switches marked as lights - for device in data.abode.get_devices(generic_type=device_types): - if data.is_excluded(device) or not data.is_light(device): - continue - + for device in data.abode.get_devices(generic_type=CONST.TYPE_LIGHT): devices.append(AbodeLight(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeLight(AbodeDevice, Light): diff --git a/homeassistant/components/abode/lock.py b/homeassistant/components/abode/lock.py index ff069751605..11f792f88fd 100644 --- a/homeassistant/components/abode/lock.py +++ b/homeassistant/components/abode/lock.py @@ -1,30 +1,31 @@ -"""Support for Abode Security System locks.""" +"""Support for the Abode Security System locks.""" import logging import abodepy.helpers.constants as CONST from homeassistant.components.lock import LockDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Abode lock devices.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_LOCK): - if data.is_excluded(device): - continue - devices.append(AbodeLock(data, device)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeLock(AbodeDevice, LockDevice): diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 793c19cc466..8316691f701 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -1,10 +1,13 @@ { "domain": "abode", "name": "Abode", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", "requirements": [ - "abodepy==0.15.0" + "abodepy==0.16.5" ], "dependencies": [], - "codeowners": [] -} + "codeowners": [ + "@shred86" + ] +} \ No newline at end of file diff --git a/homeassistant/components/abode/sensor.py b/homeassistant/components/abode/sensor.py index fca32b8dc43..e25921f295f 100644 --- a/homeassistant/components/abode/sensor.py +++ b/homeassistant/components/abode/sensor.py @@ -9,7 +9,8 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ) -from . import DOMAIN as ABODE_DOMAIN, AbodeDevice +from . import AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,22 +22,22 @@ SENSOR_TYPES = { } -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a sensor for an Abode device.""" - data = hass.data[ABODE_DOMAIN] + data = hass.data[DOMAIN] devices = [] for device in data.abode.get_devices(generic_type=CONST.TYPE_SENSOR): - if data.is_excluded(device): - continue - for sensor_type in SENSOR_TYPES: devices.append(AbodeSensor(data, device, sensor_type)) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeSensor(AbodeDevice): diff --git a/homeassistant/components/abode/strings.json b/homeassistant/components/abode/strings.json new file mode 100644 index 00000000000..bf7e768f6e3 --- /dev/null +++ b/homeassistant/components/abode/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "Abode", + "step": { + "user": { + "title": "Fill in your Abode login information", + "data": { + "username": "Email Address", + "password": "Password" + } + } + }, + "error": { + "identifier_exists": "Account already registered.", + "invalid_credentials": "Invalid credentials.", + "connection_error": "Unable to connect to Abode." + }, + "abort": { + "single_instance_allowed": "Only a single configuration of Abode is allowed." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index 4192ebb4485..7bd7f394d30 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -6,37 +6,32 @@ import abodepy.helpers.timeline as TIMELINE from homeassistant.components.switch import SwitchDevice -from . import DOMAIN as ABODE_DOMAIN, AbodeAutomation, AbodeDevice +from . import AbodeAutomation, AbodeDevice +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Abode switch devices.""" +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Platform uses config entry setup.""" + pass - data = hass.data[ABODE_DOMAIN] + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Abode switch devices.""" + data = hass.data[DOMAIN] devices = [] - # Get all regular switches that are not excluded or marked as lights for device in data.abode.get_devices(generic_type=CONST.TYPE_SWITCH): - if data.is_excluded(device) or data.is_light(device): - continue - devices.append(AbodeSwitch(data, device)) - # Get all Abode automations that can be enabled/disabled for automation in data.abode.get_automations(generic_type=CONST.TYPE_AUTOMATION): - if data.is_automation_excluded(automation): - continue - devices.append( AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP) ) - data.devices.extend(devices) - - add_entities(devices) + async_add_entities(devices) class AbodeSwitch(AbodeDevice, SwitchDevice): diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 4a4effc36ce..664e83fba33 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -6,6 +6,7 @@ To update, run python3 -m script.hassfest # fmt: off FLOWS = [ + "abode", "adguard", "airly", "ambiclimate", diff --git a/requirements_all.txt b/requirements_all.txt index a4623fe8bfb..9a9540c8747 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -103,7 +103,7 @@ WazeRouteCalculator==0.10 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.15.0 +abodepy==0.16.5 # homeassistant.components.mcp23017 adafruit-blinka==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5c9f80e408c..4c3a6e7721b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,6 +45,9 @@ RtmAPI==0.7.2 # homeassistant.components.yessssms YesssSMS==0.4.1 +# homeassistant.components.abode +abodepy==0.16.5 + # homeassistant.components.androidtv adb-shell==0.0.4 diff --git a/tests/components/abode/__init__.py b/tests/components/abode/__init__.py new file mode 100644 index 00000000000..a34320c21de --- /dev/null +++ b/tests/components/abode/__init__.py @@ -0,0 +1 @@ +"""Tests for the Abode component.""" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py new file mode 100644 index 00000000000..c3f5d170767 --- /dev/null +++ b/tests/components/abode/test_config_flow.py @@ -0,0 +1,120 @@ +"""Tests for the Abode config flow.""" +from unittest.mock import patch + +from abodepy.exceptions import AbodeAuthenticationException + +from homeassistant import data_entry_flow +from homeassistant.components.abode import config_flow +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.common import MockConfigEntry + +CONF_POLLING = "polling" + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_one_config_allowed(hass): + """Test that only one Abode configuration is allowed.""" + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + MockConfigEntry( + domain="abode", + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ).add_to_hass(hass) + + step_user_result = await flow.async_step_user() + + assert step_user_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert step_user_result["reason"] == "single_instance_allowed" + + conf = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + import_config_result = await flow.async_step_import(conf) + + assert import_config_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert import_config_result["reason"] == "single_instance_allowed" + + +async def test_invalid_credentials(hass): + """Test that invalid credentials throws an error.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch( + "homeassistant.components.abode.config_flow.Abode", + side_effect=AbodeAuthenticationException((400, "auth error")), + ): + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "invalid_credentials"} + + +async def test_connection_error(hass): + """Test other than invalid credentials throws an error.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch( + "homeassistant.components.abode.config_flow.Abode", + side_effect=AbodeAuthenticationException((500, "connection error")), + ): + result = await flow.async_step_user(user_input=conf) + assert result["errors"] == {"base": "connection_error"} + + +async def test_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch("homeassistant.components.abode.config_flow.Abode"): + result = await flow.async_step_import(import_config=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + result = await flow.async_step_user(user_input=result["data"]) + assert result["title"] == "user@email.com" + assert result["data"] == { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + flow = config_flow.AbodeFlowHandler() + flow.hass = hass + + with patch("homeassistant.components.abode.config_flow.Abode"): + result = await flow.async_step_user(user_input=conf) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "user@email.com" + assert result["data"] == { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_POLLING: False, + } From 2acd3f9e98763605ad0abe68e5532c79253fc11f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 13 Oct 2019 20:29:14 +0200 Subject: [PATCH 243/639] Allow MQTT json light floating point transition (#27253) * MQTT json light: allow floating point transition Allow to use floating point values for the transition time of the MQTT json light. In this way transitions shorter than 1s can be used (0.5 seconds for instance) if the MQTT light supports it. * Always sent a float --- homeassistant/components/mqtt/light/schema_json.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 1a46cd5e535..1e8114a48e6 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -463,7 +463,7 @@ class MqttLightJson( message["flash"] = self._flash_times[CONF_FLASH_TIME_SHORT] if ATTR_TRANSITION in kwargs: - message["transition"] = int(kwargs[ATTR_TRANSITION]) + message["transition"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs and self._brightness is not None: message["brightness"] = int( @@ -521,7 +521,7 @@ class MqttLightJson( message = {"state": "OFF"} if ATTR_TRANSITION in kwargs: - message["transition"] = int(kwargs[ATTR_TRANSITION]) + message["transition"] = kwargs[ATTR_TRANSITION] mqtt.async_publish( self.hass, From dd8fc4174779ee3010aaec8799595621ea5189bf Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sun, 13 Oct 2019 22:19:11 +0200 Subject: [PATCH 244/639] Move imports in rflink component (#27367) * Move imports in rflink component * import order * import order * Update __init__.py * Update __init__.py I don't understand why tests are failing... * Fix RFLink imports * Fix monkeypatch for 'create_rflink_connection' * isort for rflink classes --- homeassistant/components/rflink/__init__.py | 15 +++++++-------- homeassistant/components/rflink/sensor.py | 3 +-- tests/components/rflink/test_binary_sensor.py | 5 ++--- tests/components/rflink/test_cover.py | 10 +++++----- tests/components/rflink/test_init.py | 12 +++++++----- tests/components/rflink/test_light.py | 4 ++-- tests/components/rflink/test_sensor.py | 3 ++- tests/components/rflink/test_switch.py | 4 ++-- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index c218bc271ce..b3e1d2b16b7 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -2,8 +2,10 @@ import asyncio from collections import defaultdict import logging -import async_timeout +import async_timeout +from rflink.protocol import create_rflink_connection +from serial import SerialException import voluptuous as vol from homeassistant.const import ( @@ -11,18 +13,18 @@ from homeassistant.const import ( CONF_COMMAND, CONF_HOST, CONF_PORT, - STATE_ON, EVENT_HOMEASSISTANT_STOP, + STATE_ON, ) from homeassistant.core import CoreState, callback from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.deprecation import get_deprecated -from homeassistant.helpers.entity import Entity from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) +from homeassistant.helpers.entity import Entity from homeassistant.helpers.restore_state import RestoreEntity _LOGGER = logging.getLogger(__name__) @@ -118,9 +120,6 @@ def identify_event_type(event): async def async_setup(hass, config): """Set up the Rflink component.""" - from rflink.protocol import create_rflink_connection - import serial - # Allow entities to register themselves by device_id to be looked up when # new rflink events arrive to be handled hass.data[DATA_ENTITY_LOOKUP] = { @@ -239,7 +238,7 @@ async def async_setup(hass, config): transport, protocol = await connection except ( - serial.serialutil.SerialException, + SerialException, ConnectionRefusedError, TimeoutError, OSError, diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index 48484621c4d..aa0ef4f9c62 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -1,6 +1,7 @@ """Support for Rflink sensors.""" import logging +from rflink.parser import PACKET_FIELDS, UNITS import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -66,8 +67,6 @@ def lookup_unit_for_sensor_type(sensor_type): Async friendly. """ - from rflink.parser import UNITS, PACKET_FIELDS - field_abbrev = {v: k for k, v in PACKET_FIELDS.items()} return UNITS.get(field_abbrev.get(sensor_type)) diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index 442ebebdffe..d1fdec579c9 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -8,14 +8,13 @@ from datetime import timedelta from unittest.mock import patch from homeassistant.components.rflink import CONF_RECONNECT_INTERVAL - -import homeassistant.core as ha from homeassistant.const import ( EVENT_STATE_CHANGED, - STATE_ON, STATE_OFF, + STATE_ON, STATE_UNAVAILABLE, ) +import homeassistant.core as ha import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/rflink/test_cover.py b/tests/components/rflink/test_cover.py index 858258e7efd..dc286502068 100644 --- a/tests/components/rflink/test_cover.py +++ b/tests/components/rflink/test_cover.py @@ -9,13 +9,13 @@ import logging from homeassistant.components.rflink import EVENT_BUTTON_PRESSED from homeassistant.const import ( - SERVICE_OPEN_COVER, - SERVICE_CLOSE_COVER, - STATE_OPEN, - STATE_CLOSED, ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + STATE_CLOSED, + STATE_OPEN, ) -from homeassistant.core import callback, State, CoreState +from homeassistant.core import CoreState, State, callback from tests.common import mock_restore_cache from tests.components.rflink.test_init import mock_rflink diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 5e821fbdeb2..df96b0e87ae 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -5,14 +5,14 @@ from unittest.mock import Mock from homeassistant.bootstrap import async_setup_component from homeassistant.components.rflink import ( CONF_RECONNECT_INTERVAL, - SERVICE_SEND_COMMAND, - RflinkCommand, - TMP_ENTITY, DATA_ENTITY_LOOKUP, EVENT_KEY_COMMAND, EVENT_KEY_SENSOR, + SERVICE_SEND_COMMAND, + TMP_ENTITY, + RflinkCommand, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_STOP_COVER +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_STOP_COVER, SERVICE_TURN_OFF async def mock_rflink( @@ -46,7 +46,9 @@ async def mock_rflink( return transport, protocol mock_create = Mock(wraps=create_rflink_connection) - monkeypatch.setattr("rflink.protocol.create_rflink_connection", mock_create) + monkeypatch.setattr( + "homeassistant.components.rflink.create_rflink_connection", mock_create + ) await async_setup_component(hass, "rflink", config) await async_setup_component(hass, domain, config) diff --git a/tests/components/rflink/test_light.py b/tests/components/rflink/test_light.py index a22e7680ac8..b22730a3310 100644 --- a/tests/components/rflink/test_light.py +++ b/tests/components/rflink/test_light.py @@ -11,10 +11,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_ON, STATE_OFF, + STATE_ON, ) -from homeassistant.core import callback, State, CoreState +from homeassistant.core import CoreState, State, callback from tests.common import mock_restore_cache from tests.components.rflink.test_init import mock_rflink diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index bf6c9e03fbc..3fea3ef6ef4 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -7,12 +7,13 @@ automatic sensor creation. from homeassistant.components.rflink import ( CONF_RECONNECT_INTERVAL, - TMP_ENTITY, DATA_ENTITY_LOOKUP, EVENT_KEY_COMMAND, EVENT_KEY_SENSOR, + TMP_ENTITY, ) from homeassistant.const import STATE_UNKNOWN + from tests.components.rflink.test_init import mock_rflink DOMAIN = "sensor" diff --git a/tests/components/rflink/test_switch.py b/tests/components/rflink/test_switch.py index 4503f1a232f..d1fced33208 100644 --- a/tests/components/rflink/test_switch.py +++ b/tests/components/rflink/test_switch.py @@ -10,10 +10,10 @@ from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_ON, STATE_OFF, + STATE_ON, ) -from homeassistant.core import callback, State, CoreState +from homeassistant.core import CoreState, State, callback from tests.common import mock_restore_cache from tests.components.rflink.test_init import mock_rflink From 45694de2ee34c58ec86a14cf16c4f69449fe1a4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 22:25:54 +0200 Subject: [PATCH 245/639] move imports in tibber component (#27584) --- homeassistant/components/tibber/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 0622b70f127..df56989714f 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -3,12 +3,13 @@ import asyncio import logging import aiohttp +import tibber import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_ACCESS_TOKEN, CONF_NAME +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util DOMAIN = "tibber" @@ -25,8 +26,6 @@ async def async_setup(hass, config): """Set up the Tibber component.""" conf = config.get(DOMAIN) - import tibber - tibber_connection = tibber.Tibber( conf[CONF_ACCESS_TOKEN], websession=async_get_clientsession(hass), From 5e79408708adda5e4108e66588a8418ed824f156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sun, 13 Oct 2019 23:27:42 +0300 Subject: [PATCH 246/639] Upgrade to flake8-docstrings 1.5.0, pytest 5.2.1, and pytest-cov 2.8.1 (#27588) https://gitlab.com/pycqa/flake8-docstrings/blob/1.5.0/HISTORY.rst https://docs.pytest.org/en/latest/changelog.html#pytest-5-2-1-2019-10-06 https://pytest-cov.readthedocs.io/en/latest/changelog.html#id1 --- requirements_test.txt | 6 +++--- requirements_test_all.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index d0b7880d78d..b6d5bdd7ee9 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -flake8-docstrings==1.3.1 +flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 mypy==0.730 @@ -15,8 +15,8 @@ pydocstyle==4.0.1 pylint==2.4.2 astroid==2.3.1 pytest-aiohttp==0.3.0 -pytest-cov==2.7.1 +pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.0 +pytest==5.2.1 requests_mock==1.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4c3a6e7721b..c3fa3024041 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -7,7 +7,7 @@ asynctest==0.13.0 black==19.3b0 codecov==2.0.15 -flake8-docstrings==1.3.1 +flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 mypy==0.730 @@ -16,10 +16,10 @@ pydocstyle==4.0.1 pylint==2.4.2 astroid==2.3.1 pytest-aiohttp==0.3.0 -pytest-cov==2.7.1 +pytest-cov==2.8.1 pytest-sugar==0.9.2 pytest-timeout==1.3.3 -pytest==5.2.0 +pytest==5.2.1 requests_mock==1.7.0 From 585214f3a4d055ba397d856e95ca8a93c3ee2cc2 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 Oct 2019 22:29:48 +0200 Subject: [PATCH 247/639] Upgrade Mastodon.py to 1.5.0 (#27598) --- homeassistant/components/mastodon/manifest.json | 2 +- homeassistant/components/mastodon/notify.py | 10 +++------- requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/mastodon/manifest.json b/homeassistant/components/mastodon/manifest.json index e041ba2f669..cacdf9dd502 100644 --- a/homeassistant/components/mastodon/manifest.json +++ b/homeassistant/components/mastodon/manifest.json @@ -3,7 +3,7 @@ "name": "Mastodon", "documentation": "https://www.home-assistant.io/integrations/mastodon", "requirements": [ - "Mastodon.py==1.4.6" + "Mastodon.py==1.5.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/mastodon/notify.py b/homeassistant/components/mastodon/notify.py index e7f7de5917f..88716de5773 100644 --- a/homeassistant/components/mastodon/notify.py +++ b/homeassistant/components/mastodon/notify.py @@ -1,13 +1,14 @@ """Mastodon platform for notify component.""" import logging +from mastodon import Mastodon +from mastodon.Mastodon import MastodonAPIError, MastodonUnauthorizedError import voluptuous as vol +from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ACCESS_TOKEN import homeassistant.helpers.config_validation as cv -from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService - _LOGGER = logging.getLogger(__name__) CONF_BASE_URL = "base_url" @@ -28,9 +29,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the Mastodon notification service.""" - from mastodon import Mastodon - from mastodon.Mastodon import MastodonUnauthorizedError - client_id = config.get(CONF_CLIENT_ID) client_secret = config.get(CONF_CLIENT_SECRET) access_token = config.get(CONF_ACCESS_TOKEN) @@ -60,8 +58,6 @@ class MastodonNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from mastodon.Mastodon import MastodonAPIError - try: self._api.toot(message) except MastodonAPIError: diff --git a/requirements_all.txt b/requirements_all.txt index 9a9540c8747..bf36c898b1e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -38,7 +38,7 @@ Adafruit-SHT31==1.0.2 HAP-python==2.6.0 # homeassistant.components.mastodon -Mastodon.py==1.4.6 +Mastodon.py==1.5.0 # homeassistant.components.orangepi_gpio OPi.GPIO==0.3.6 From 1d2b59db8277d95b33c0d766124901780f2f0616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 22:38:42 +0200 Subject: [PATCH 248/639] Move imports in syslog (#27602) --- homeassistant/components/syslog/notify.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/syslog/notify.py b/homeassistant/components/syslog/notify.py index d7696e43bef..67d4882e5c3 100644 --- a/homeassistant/components/syslog/notify.py +++ b/homeassistant/components/syslog/notify.py @@ -1,5 +1,6 @@ """Syslog notification service.""" import logging +import syslog import voluptuous as vol @@ -67,7 +68,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the syslog notification service.""" - import syslog facility = getattr(syslog, SYSLOG_FACILITY[config.get(CONF_FACILITY)]) option = getattr(syslog, SYSLOG_OPTION[config.get(CONF_OPTION)]) @@ -87,7 +87,6 @@ class SyslogNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - import syslog title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) From 48c1a0290c7ac82c6adbc1066095bf5a93387346 Mon Sep 17 00:00:00 2001 From: Moritz Fey Date: Sun, 13 Oct 2019 22:53:42 +0200 Subject: [PATCH 249/639] add content for services.yaml in component media_extractor (#27608) * add content for services.yaml for media_extractor component * remove example data * add empty line on end of file * Update services.yaml --- .../components/media_extractor/services.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/homeassistant/components/media_extractor/services.yaml b/homeassistant/components/media_extractor/services.yaml index e69de29bb2d..c5588c34134 100644 --- a/homeassistant/components/media_extractor/services.yaml +++ b/homeassistant/components/media_extractor/services.yaml @@ -0,0 +1,13 @@ +play_media: + description: Downloads file from given url. + fields: + entity_id: + description: Name(s) of entities to play media on. + example: 'media_player.living_room_chromecast' + media_content_id: + description: The ID of the content to play. Platform dependent. + example: 'https://soundcloud.com/bruttoband/brutto-11' + media_content_type: + description: The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC. + example: 'music' + From d8b73f945929721d03ab23089f9878c0bfb5b767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 22:55:24 +0200 Subject: [PATCH 250/639] move imports in ted5000 component (#27601) --- homeassistant/components/ted5000/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ted5000/sensor.py b/homeassistant/components/ted5000/sensor.py index ea0963a092e..e0025a050c3 100644 --- a/homeassistant/components/ted5000/sensor.py +++ b/homeassistant/components/ted5000/sensor.py @@ -1,9 +1,10 @@ -"""Support gathering ted500 information.""" -import logging +"""Support gathering ted5000 information.""" from datetime import timedelta +import logging import requests import voluptuous as vol +import xmltodict from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, POWER_WATT @@ -94,7 +95,6 @@ class Ted5000Gateway: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the Ted5000 XML API.""" - import xmltodict try: request = requests.get(self.url, timeout=10) From c5dc670b36bd3aa7b80749e79e5ef4ac8ef0a2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 13 Oct 2019 22:55:43 +0200 Subject: [PATCH 251/639] move imports in tellstick component (#27600) --- .../components/tellstick/__init__.py | 31 +++++++------------ homeassistant/components/tellstick/sensor.py | 10 +++--- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/tellstick/__init__.py b/homeassistant/components/tellstick/__init__.py index 526db5e73df..e7f341c90b2 100644 --- a/homeassistant/components/tellstick/__init__.py +++ b/homeassistant/components/tellstick/__init__.py @@ -2,13 +2,22 @@ import logging import threading +from tellcore.constants import ( + TELLSTICK_DIM, + TELLSTICK_TURNOFF, + TELLSTICK_TURNON, + TELLSTICK_UP, +) +from tellcore.library import TelldusError +from tellcore.telldus import AsyncioCallbackDispatcher, TelldusCore +from tellcorenet import TellCoreClient import voluptuous as vol -from homeassistant.helpers import discovery +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT -from homeassistant.helpers.entity import Entity +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -73,10 +82,6 @@ def _discover(hass, config, component_name, found_tellcore_devices): def setup(hass, config): """Set up the Tellstick component.""" - from tellcore.constants import TELLSTICK_DIM, TELLSTICK_UP - from tellcore.telldus import AsyncioCallbackDispatcher - from tellcore.telldus import TelldusCore - from tellcorenet import TellCoreClient conf = config.get(DOMAIN, {}) net_host = conf.get(CONF_HOST) @@ -219,7 +224,6 @@ class TellstickDevice(Entity): def _send_repeated_command(self): """Send a tellstick command once and decrease the repeat count.""" - from tellcore.library import TelldusError with TELLSTICK_LOCK: if self._repeats_left > 0: @@ -259,11 +263,6 @@ class TellstickDevice(Entity): def _update_model_from_command(self, tellcore_command, tellcore_data): """Update the model, from a sent tellcore command and data.""" - from tellcore.constants import ( - TELLSTICK_TURNON, - TELLSTICK_TURNOFF, - TELLSTICK_DIM, - ) if tellcore_command not in [TELLSTICK_TURNON, TELLSTICK_TURNOFF, TELLSTICK_DIM]: _LOGGER.debug("Unhandled tellstick command: %d", tellcore_command) @@ -289,12 +288,6 @@ class TellstickDevice(Entity): def _update_from_tellcore(self): """Read the current state of the device from the tellcore library.""" - from tellcore.library import TelldusError - from tellcore.constants import ( - TELLSTICK_TURNON, - TELLSTICK_TURNOFF, - TELLSTICK_DIM, - ) with TELLSTICK_LOCK: try: diff --git a/homeassistant/components/tellstick/sensor.py b/homeassistant/components/tellstick/sensor.py index 98d162d6d81..1a55e67ac43 100644 --- a/homeassistant/components/tellstick/sensor.py +++ b/homeassistant/components/tellstick/sensor.py @@ -1,13 +1,15 @@ """Support for Tellstick sensors.""" -import logging from collections import namedtuple +import logging +from tellcore import telldus +import tellcore.constants as tellcore_constants import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import TEMP_CELSIUS, CONF_ID, CONF_NAME, CONF_PROTOCOL -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_ID, CONF_NAME, CONF_PROTOCOL, TEMP_CELSIUS import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -48,8 +50,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tellstick sensors.""" - from tellcore import telldus - import tellcore.constants as tellcore_constants sensor_value_descriptions = { tellcore_constants.TELLSTICK_TEMPERATURE: DatatypeDescription( From 0235487a229a871010a45edc7d60ec583396e450 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sun, 13 Oct 2019 22:56:01 +0200 Subject: [PATCH 252/639] Move top level imports (#27597) --- homeassistant/components/tradfri/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tradfri/__init__.py b/homeassistant/components/tradfri/__init__.py index c719fa41614..bdfabb4b00a 100644 --- a/homeassistant/components/tradfri/__init__.py +++ b/homeassistant/components/tradfri/__init__.py @@ -2,6 +2,8 @@ import logging import voluptuous as vol +from pytradfri import Gateway, RequestError +from pytradfri.api.aiocoap_api import APIFactory import homeassistant.helpers.config_validation as cv from homeassistant import config_entries @@ -91,8 +93,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Create a gateway.""" # host, identity, key, allow_tradfri_groups - from pytradfri import Gateway, RequestError # pylint: disable=import-error - from pytradfri.api.aiocoap_api import APIFactory factory = APIFactory( entry.data[CONF_HOST], From 9fb0812ce5bdfb3dd4f47656f3406ba0bc2cae69 Mon Sep 17 00:00:00 2001 From: Santobert Date: Sun, 13 Oct 2019 22:56:34 +0200 Subject: [PATCH 253/639] Improve neato tests (#27578) * Improve tests * Rename account to configflow * configflow to config_flow * Patch pybotvac instead of own code --- homeassistant/components/neato/__init__.py | 13 ++--- tests/components/neato/test_init.py | 59 ++++++++++++++++++++-- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index 839c24568d8..ddf9789f678 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -92,10 +92,7 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up config entry.""" - if entry.data[CONF_VENDOR] == "neato": - hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Neato) - elif entry.data[CONF_VENDOR] == "vorwerk": - hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account, Vorwerk) + hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account) hub = hass.data[NEATO_LOGIN] await hass.async_add_executor_job(hub.login) @@ -132,12 +129,16 @@ async def async_unload_entry(hass, entry): class NeatoHub: """A My Neato hub wrapper class.""" - def __init__(self, hass, domain_config, neato, vendor): + def __init__(self, hass, domain_config, neato): """Initialize the Neato hub.""" self.config = domain_config self._neato = neato self._hass = hass - self._vendor = vendor + + if self.config[CONF_VENDOR] == "vorwerk": + self._vendor = Vorwerk() + else: # Neato + self._vendor = Neato() self.my_neato = None self.logged_in = False diff --git a/tests/components/neato/test_init.py b/tests/components/neato/test_init.py index 361f9eab1db..444cbe8cc5d 100644 --- a/tests/components/neato/test_init.py +++ b/tests/components/neato/test_init.py @@ -2,6 +2,8 @@ import pytest from unittest.mock import patch +from pybotvac.exceptions import NeatoLoginException + from homeassistant.components.neato.const import CONF_VENDOR, NEATO_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component @@ -20,6 +22,12 @@ VALID_CONFIG = { CONF_VENDOR: VENDOR_NEATO, } +DIFFERENT_CONFIG = { + CONF_USERNAME: "anotherUsername", + CONF_PASSWORD: "anotherPassword", + CONF_VENDOR: VENDOR_VORWERK, +} + INVALID_CONFIG = { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, @@ -27,20 +35,40 @@ INVALID_CONFIG = { } -@pytest.fixture(name="account") -def mock_controller_login(): +@pytest.fixture(name="config_flow") +def mock_config_flow_login(): """Mock a successful login.""" with patch("homeassistant.components.neato.config_flow.Account", return_value=True): yield +@pytest.fixture(name="hub") +def mock_controller_login(): + """Mock a successful login.""" + with patch("homeassistant.components.neato.Account", return_value=True): + yield + + async def test_no_config_entry(hass): """There is nothing in configuration.yaml.""" res = await async_setup_component(hass, NEATO_DOMAIN, {}) assert res is True -async def test_config_entries_in_sync(hass, account): +async def test_create_valid_config_entry(hass, config_flow, hub): + """There is something in configuration.yaml.""" + assert hass.config_entries.async_entries(NEATO_DOMAIN) == [] + assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO + + +async def test_config_entries_in_sync(hass, hub): """The config entry and configuration.yaml are in sync.""" MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) @@ -55,9 +83,9 @@ async def test_config_entries_in_sync(hass, account): assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO -async def test_config_entries_not_in_sync(hass, account): +async def test_config_entries_not_in_sync(hass, config_flow, hub): """The config entry and configuration.yaml are not in sync.""" - MockConfigEntry(domain=NEATO_DOMAIN, data=INVALID_CONFIG).add_to_hass(hass) + MockConfigEntry(domain=NEATO_DOMAIN, data=DIFFERENT_CONFIG).add_to_hass(hass) assert hass.config_entries.async_entries(NEATO_DOMAIN) assert await async_setup_component(hass, NEATO_DOMAIN, {NEATO_DOMAIN: VALID_CONFIG}) @@ -68,3 +96,24 @@ async def test_config_entries_not_in_sync(hass, account): assert entries[0].data[CONF_USERNAME] == USERNAME assert entries[0].data[CONF_PASSWORD] == PASSWORD assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO + + +async def test_config_entries_not_in_sync_error(hass): + """The config entry and configuration.yaml are not in sync, the new configuration is wrong.""" + MockConfigEntry(domain=NEATO_DOMAIN, data=VALID_CONFIG).add_to_hass(hass) + + assert hass.config_entries.async_entries(NEATO_DOMAIN) + with patch( + "homeassistant.components.neato.config_flow.Account", + side_effect=NeatoLoginException(), + ): + assert not await async_setup_component( + hass, NEATO_DOMAIN, {NEATO_DOMAIN: DIFFERENT_CONFIG} + ) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(NEATO_DOMAIN) + assert entries + assert entries[0].data[CONF_USERNAME] == USERNAME + assert entries[0].data[CONF_PASSWORD] == PASSWORD + assert entries[0].data[CONF_VENDOR] == VENDOR_NEATO From 3454b6fa877f22e7b25e59d1d27ebcd5e39e36ab Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Sun, 13 Oct 2019 22:59:28 +0200 Subject: [PATCH 254/639] Refactor Tradfri base class (#27589) * Refactor Tradfri base class * Clarify doc * Fix pylint * Review fix * Move --- .../components/tradfri/base_class.py | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 8430a342c09..632ce6b164e 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -10,8 +10,11 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class TradfriBaseDevice(Entity): - """Base class for a TRADFRI device.""" +class TradfriBaseClass(Entity): + """Base class for IKEA TRADFRI. + + All devices and groups should ultimately inherit from this class. + """ def __init__(self, device, api, gateway_id): """Initialize a device.""" @@ -54,20 +57,6 @@ class TradfriBaseDevice(Entity): """Return True if entity is available.""" return self._available - @property - def device_info(self): - """Return the device info.""" - info = self._device.device_info - - return { - "identifiers": {(DOMAIN, self._device.id)}, - "manufacturer": info.manufacturer, - "model": info.model_number, - "name": self._name, - "sw_version": info.firmware_version, - "via_device": (DOMAIN, self._gateway_id), - } - @property def name(self): """Return the display name of this device.""" @@ -94,3 +83,24 @@ class TradfriBaseDevice(Entity): self._device = device self._name = device.name self._available = device.reachable + + +class TradfriBaseDevice(TradfriBaseClass): + """Base class for a TRADFRI device. + + All devices should inherit from this class. + """ + + @property + def device_info(self): + """Return the device info.""" + info = self._device.device_info + + return { + "identifiers": {(DOMAIN, self._device.id)}, + "manufacturer": info.manufacturer, + "model": info.model_number, + "name": self._name, + "sw_version": info.firmware_version, + "via_device": (DOMAIN, self._gateway_id), + } From e866d769e8248440270d3e9a51e89d6049dd2634 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 13 Oct 2019 14:16:27 -0700 Subject: [PATCH 255/639] Google Assistant Local SDK (#27428) * Local Google * Fix test * Fix tests --- homeassistant/components/cloud/client.py | 9 +- homeassistant/components/cloud/const.py | 1 + .../components/cloud/google_config.py | 27 +++- homeassistant/components/cloud/prefs.py | 35 ++++- .../components/google_assistant/helpers.py | 95 +++++++++++- .../components/google_assistant/smart_home.py | 55 +++++-- homeassistant/components/http/__init__.py | 1 + homeassistant/components/webhook/__init__.py | 6 +- homeassistant/components/zeroconf/__init__.py | 17 ++- tests/common.py | 1 - tests/components/cloud/__init__.py | 2 +- tests/components/cloud/test_client.py | 4 +- tests/components/cloud/test_http_api.py | 20 +-- tests/components/google_assistant/__init__.py | 21 +++ .../google_assistant/test_helpers.py | 130 +++++++++++++++++ .../google_assistant/test_smart_home.py | 136 +++++++++++++++++- .../components/google_assistant/test_trait.py | 4 +- 17 files changed, 512 insertions(+), 52 deletions(-) create mode 100644 tests/components/google_assistant/test_helpers.py diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index 38ae09ced93..c7626777943 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -110,14 +110,17 @@ class CloudClient(Interface): if not self.cloud.is_logged_in: return - if self.alexa_config.should_report_state: + if self.alexa_config.enabled and self.alexa_config.should_report_state: try: await self.alexa_config.async_enable_proactive_mode() except alexa_errors.NoTokenAvailable: pass - if self.google_config.should_report_state: - self.google_config.async_enable_report_state() + if self.google_config.enabled: + self.google_config.async_enable_local_sdk() + + if self.google_config.should_report_state: + self.google_config.async_enable_report_state() async def cleanups(self) -> None: """Cleanup some stuff after logout.""" diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index e28d75f017d..6495cba23b7 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -16,6 +16,7 @@ PREF_OVERRIDE_NAME = "override_name" PREF_DISABLE_2FA = "disable_2fa" PREF_ALIASES = "aliases" PREF_SHOULD_EXPOSE = "should_expose" +PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id" DEFAULT_SHOULD_EXPOSE = True DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 38e4aec56e0..582fa007550 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -63,6 +63,19 @@ class CloudGoogleConfig(AbstractConfig): """Return if states should be proactively reported.""" return self._prefs.google_report_state + @property + def local_sdk_webhook_id(self): + """Return the local SDK webhook. + + Return None to disable the local SDK. + """ + return self._prefs.google_local_webhook_id + + @property + def local_sdk_user_id(self): + """Return the user ID to be used for actions received via the local SDK.""" + return self._prefs.cloud_user + def should_expose(self, state): """If a state object should be exposed.""" return self._should_expose_entity_id(state.entity_id) @@ -131,17 +144,19 @@ class CloudGoogleConfig(AbstractConfig): # State reporting is reported as a property on entities. # So when we change it, we need to sync all entities. await self.async_sync_entities() - return # If entity prefs are the same or we have filter in config.yaml, # don't sync. - if ( - self._cur_entity_prefs is prefs.google_entity_configs - or not self._config["filter"].empty_filter + elif ( + self._cur_entity_prefs is not prefs.google_entity_configs + and self._config["filter"].empty_filter ): - return + self.async_schedule_google_sync() - self.async_schedule_google_sync() + if self.enabled and not self.is_local_sdk_active: + self.async_enable_local_sdk() + elif not self.enabled and self.is_local_sdk_active: + self.async_disable_local_sdk() async def _handle_entity_registry_updated(self, event): """Handle when entity registry updated.""" diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index a8ff775a227..0599b00a8bd 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -21,6 +21,7 @@ from .const import ( PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE, PREF_GOOGLE_REPORT_STATE, + PREF_GOOGLE_LOCAL_WEBHOOK_ID, DEFAULT_GOOGLE_REPORT_STATE, InvalidTrustedNetworks, InvalidTrustedProxies, @@ -59,6 +60,14 @@ class CloudPreferences: self._prefs = prefs + if PREF_GOOGLE_LOCAL_WEBHOOK_ID not in self._prefs: + await self._save_prefs( + { + **self._prefs, + PREF_GOOGLE_LOCAL_WEBHOOK_ID: self._hass.components.webhook.async_generate_id(), + } + ) + @callback def async_listen_updates(self, listener): """Listen for updates to the preferences.""" @@ -79,6 +88,8 @@ class CloudPreferences: google_report_state=_UNDEF, ): """Update user preferences.""" + prefs = {**self._prefs} + for key, value in ( (PREF_ENABLE_GOOGLE, google_enabled), (PREF_ENABLE_ALEXA, alexa_enabled), @@ -92,20 +103,17 @@ class CloudPreferences: (PREF_GOOGLE_REPORT_STATE, google_report_state), ): if value is not _UNDEF: - self._prefs[key] = value + prefs[key] = value if remote_enabled is True and self._has_local_trusted_network: - self._prefs[PREF_ENABLE_REMOTE] = False + prefs[PREF_ENABLE_REMOTE] = False raise InvalidTrustedNetworks if remote_enabled is True and self._has_local_trusted_proxies: - self._prefs[PREF_ENABLE_REMOTE] = False + prefs[PREF_ENABLE_REMOTE] = False raise InvalidTrustedProxies - await self._store.async_save(self._prefs) - - for listener in self._listeners: - self._hass.async_create_task(async_create_catching_coro(listener(self))) + await self._save_prefs(prefs) async def async_update_google_entity_config( self, @@ -216,6 +224,11 @@ class CloudPreferences: """Return Google Entity configurations.""" return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {}) + @property + def google_local_webhook_id(self): + """Return Google webhook ID to receive local messages.""" + return self._prefs[PREF_GOOGLE_LOCAL_WEBHOOK_ID] + @property def alexa_entity_configs(self): """Return Alexa Entity configurations.""" @@ -262,3 +275,11 @@ class CloudPreferences: return True return False + + async def _save_prefs(self, prefs): + """Save preferences to disk.""" + self._prefs = prefs + await self._store.async_save(self._prefs) + + for listener in self._listeners: + self._hass.async_create_task(async_create_catching_coro(listener(self))) diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 933f0c07999..96b9b93d70a 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -1,10 +1,15 @@ """Helper classes for Google Assistant integration.""" from asyncio import gather from collections.abc import Mapping -from typing import List +import logging +import pprint +from typing import List, Optional + +from aiohttp.web import json_response from homeassistant.core import Context, callback, HomeAssistant, State from homeassistant.helpers.event import async_call_later +from homeassistant.components import webhook from homeassistant.const import ( CONF_NAME, STATE_UNAVAILABLE, @@ -15,6 +20,7 @@ from homeassistant.const import ( from . import trait from .const import ( + DOMAIN, DOMAIN_TO_GOOGLE_TYPES, CONF_ALIASES, ERR_FUNCTION_NOT_SUPPORTED, @@ -24,6 +30,7 @@ from .const import ( from .error import SmartHomeError SYNC_DELAY = 15 +_LOGGER = logging.getLogger(__name__) class AbstractConfig: @@ -35,6 +42,7 @@ class AbstractConfig: """Initialize abstract config.""" self.hass = hass self._google_sync_unsub = None + self._local_sdk_active = False @property def enabled(self): @@ -61,12 +69,30 @@ class AbstractConfig: """Return if we're actively reporting states.""" return self._unsub_report_state is not None + @property + def is_local_sdk_active(self): + """Return if we're actively accepting local messages.""" + return self._local_sdk_active + @property def should_report_state(self): """Return if states should be proactively reported.""" # pylint: disable=no-self-use return False + @property + def local_sdk_webhook_id(self): + """Return the local SDK webhook ID. + + Return None to disable the local SDK. + """ + return None + + @property + def local_sdk_user_id(self): + """Return the user ID to be used for actions received via the local SDK.""" + raise NotImplementedError + def should_expose(self, state) -> bool: """Return if entity should be exposed.""" raise NotImplementedError @@ -131,15 +157,66 @@ class AbstractConfig: Called when the user disconnects their account from Google. """ + @callback + def async_enable_local_sdk(self): + """Enable the local SDK.""" + webhook_id = self.local_sdk_webhook_id + + if webhook_id is None: + return + + webhook.async_register( + self.hass, DOMAIN, "Local Support", webhook_id, self._handle_local_webhook + ) + + self._local_sdk_active = True + + @callback + def async_disable_local_sdk(self): + """Disable the local SDK.""" + if not self._local_sdk_active: + return + + webhook.async_unregister(self.hass, self.local_sdk_webhook_id) + self._local_sdk_active = False + + async def _handle_local_webhook(self, hass, webhook_id, request): + """Handle an incoming local SDK message.""" + from . import smart_home + + payload = await request.json() + + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("Received local message:\n%s\n", pprint.pformat(payload)) + + if not self.enabled: + return json_response(smart_home.turned_off_response(payload)) + + result = await smart_home.async_handle_message( + self.hass, self, self.local_sdk_user_id, payload + ) + + if _LOGGER.isEnabledFor(logging.DEBUG): + _LOGGER.debug("Responding to local message:\n%s\n", pprint.pformat(result)) + + return json_response(result) + class RequestData: """Hold data associated with a particular request.""" - def __init__(self, config: AbstractConfig, user_id: str, request_id: str): + def __init__( + self, + config: AbstractConfig, + user_id: str, + request_id: str, + devices: Optional[List[dict]], + ): """Initialize the request data.""" self.config = config self.request_id = request_id self.context = Context(user_id=user_id) + self.devices = devices def get_google_type(domain, device_class): @@ -234,6 +311,15 @@ class GoogleEntity: if aliases: device["name"]["nicknames"] = aliases + if self.config.is_local_sdk_active: + device["otherDeviceIds"] = [{"deviceId": self.entity_id}] + device["customData"] = { + "webhookId": self.config.local_sdk_webhook_id, + "httpPort": self.hass.config.api.port, + "httpSSL": self.hass.config.api.use_ssl, + "proxyDeviceId": self.config.agent_user_id, + } + for trt in traits: device["attributes"].update(trt.sync_attributes()) @@ -280,6 +366,11 @@ class GoogleEntity: return attrs + @callback + def reachable_device_serialize(self): + """Serialize entity for a REACHABLE_DEVICE response.""" + return {"verificationId": self.entity_id} + async def execute(self, data, command_payload): """Execute a command. diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index f9b311a3880..0944c9532ef 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -5,7 +5,7 @@ import logging from homeassistant.util.decorator import Registry -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, __version__ from .const import ( ERR_PROTOCOL_ERROR, @@ -24,9 +24,7 @@ _LOGGER = logging.getLogger(__name__) async def async_handle_message(hass, config, user_id, message): """Handle incoming API messages.""" - request_id: str = message.get("requestId") - - data = RequestData(config, user_id, request_id) + data = RequestData(config, user_id, message["requestId"], message.get("devices")) response = await _process(hass, data, message) @@ -67,6 +65,7 @@ async def _process(hass, data, message): if result is None: return None + return {"requestId": data.request_id, "payload": result} @@ -74,7 +73,7 @@ async def _process(hass, data, message): async def async_devices_sync(hass, data, payload): """Handle action.devices.SYNC request. - https://developers.google.com/actions/smarthome/create-app#actiondevicessync + https://developers.google.com/assistant/smarthome/develop/process-intents#SYNC """ hass.bus.async_fire( EVENT_SYNC_RECEIVED, {"request_id": data.request_id}, context=data.context @@ -84,7 +83,7 @@ async def async_devices_sync(hass, data, payload): *( entity.sync_serialize() for entity in async_get_entities(hass, data.config) - if data.config.should_expose(entity.state) + if entity.should_expose() ) ) @@ -100,7 +99,7 @@ async def async_devices_sync(hass, data, payload): async def async_devices_query(hass, data, payload): """Handle action.devices.QUERY request. - https://developers.google.com/actions/smarthome/create-app#actiondevicesquery + https://developers.google.com/assistant/smarthome/develop/process-intents#QUERY """ devices = {} for device in payload.get("devices", []): @@ -128,7 +127,7 @@ async def async_devices_query(hass, data, payload): async def handle_devices_execute(hass, data, payload): """Handle action.devices.EXECUTE request. - https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute + https://developers.google.com/assistant/smarthome/develop/process-intents#EXECUTE """ entities = {} results = {} @@ -196,12 +195,50 @@ async def handle_devices_execute(hass, data, payload): async def async_devices_disconnect(hass, data: RequestData, payload): """Handle action.devices.DISCONNECT request. - https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect + https://developers.google.com/assistant/smarthome/develop/process-intents#DISCONNECT """ await data.config.async_deactivate_report_state() return None +@HANDLERS.register("action.devices.IDENTIFY") +async def async_devices_identify(hass, data: RequestData, payload): + """Handle action.devices.IDENTIFY request. + + https://developers.google.com/assistant/smarthome/develop/local#implement_the_identify_handler + """ + return { + "device": { + "id": data.config.agent_user_id, + "isLocalOnly": True, + "isProxy": True, + "deviceInfo": { + "hwVersion": "UNKNOWN_HW_VERSION", + "manufacturer": "Home Assistant", + "model": "Home Assistant", + "swVersion": __version__, + }, + } + } + + +@HANDLERS.register("action.devices.REACHABLE_DEVICES") +async def async_devices_reachable(hass, data: RequestData, payload): + """Handle action.devices.REACHABLE_DEVICES request. + + https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect + """ + google_ids = set(dev["id"] for dev in (data.devices or [])) + + return { + "devices": [ + entity.reachable_device_serialize() + for entity in async_get_entities(hass, data.config) + if entity.entity_id in google_ids and entity.should_expose() + ] + } + + def turned_off_response(message): """Return a device turned off response.""" return { diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index a8aaa3390a7..1b61e74769f 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -128,6 +128,7 @@ class ApiConfig: """Initialize a new API config object.""" self.host = host self.port = port + self.use_ssl = use_ssl host = host.rstrip("/") if host.startswith(("http://", "https://")): diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index a12e55c771a..5a41bfa9851 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -1,7 +1,7 @@ """Webhooks for Home Assistant.""" import logging -from aiohttp.web import Response +from aiohttp.web import Response, Request import voluptuous as vol from homeassistant.core import callback @@ -98,9 +98,11 @@ class WebhookView(HomeAssistantView): url = URL_WEBHOOK_PATH name = "api:webhook" requires_auth = False + cors_allowed = True - async def _handle(self, request, webhook_id): + async def _handle(self, request: Request, webhook_id): """Handle webhook call.""" + _LOGGER.debug("Handling webhook %s payload for %s", request.method, webhook_id) hass = request.app["hass"] return await async_handle_webhook(hass, webhook_id, request) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index af107a6ae0d..2f9fb7b4580 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -11,7 +11,11 @@ import voluptuous as vol from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf from homeassistant import util -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, __version__ +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STOP, + EVENT_HOMEASSISTANT_START, + __version__, +) from homeassistant.generated.zeroconf import ZEROCONF, HOMEKIT _LOGGER = logging.getLogger(__name__) @@ -33,6 +37,7 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Set up Zeroconf and make Home Assistant discoverable.""" + zeroconf = Zeroconf() zeroconf_name = f"{hass.config.location_name}.{ZEROCONF_TYPE}" params = { @@ -58,9 +63,15 @@ def setup(hass, config): properties=params, ) - zeroconf = Zeroconf() + def zeroconf_hass_start(_event): + """Expose Home Assistant on zeroconf when it starts. - zeroconf.register_service(info) + Wait till started or otherwise HTTP is not up and running. + """ + _LOGGER.info("Starting Zeroconf broadcast") + zeroconf.register_service(info) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_START, zeroconf_hass_start) def service_update(zeroconf, service_type, name, state_change): """Service state changed.""" diff --git a/tests/common.py b/tests/common.py index 0684e6daafc..40e02842146 100644 --- a/tests/common.py +++ b/tests/common.py @@ -230,7 +230,6 @@ def get_test_instance_port(): return _TEST_INSTANCE_PORT -@ha.callback def async_mock_service(hass, domain, service, schema=None): """Set up a fake service & return a calls log list to this service.""" calls = [] diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index a0196cae32a..45ea4e43ee4 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -25,4 +25,4 @@ def mock_cloud_prefs(hass, prefs={}): } prefs_to_set.update(prefs) hass.data[cloud.DOMAIN].client._prefs._prefs = prefs_to_set - return prefs_to_set + return hass.data[cloud.DOMAIN].client._prefs diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index b7ac5f4cffd..054b38daffc 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -61,7 +61,7 @@ async def test_handler_alexa(hass): async def test_handler_alexa_disabled(hass, mock_cloud_fixture): """Test handler Alexa when user has disabled it.""" - mock_cloud_fixture[PREF_ENABLE_ALEXA] = False + mock_cloud_fixture._prefs[PREF_ENABLE_ALEXA] = False cloud = hass.data["cloud"] resp = await cloud.client.async_alexa_message( @@ -125,7 +125,7 @@ async def test_handler_google_actions(hass): async def test_handler_google_actions_disabled(hass, mock_cloud_fixture): """Test handler Google Actions when user has disabled it.""" - mock_cloud_fixture[PREF_ENABLE_GOOGLE] = False + mock_cloud_fixture._prefs[PREF_ENABLE_GOOGLE] = False with patch("hass_nabucasa.Cloud.start", return_value=mock_coro()): assert await async_setup_component(hass, "cloud", {}) diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 8e03fb82b2c..314db3a9e88 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -10,13 +10,7 @@ from hass_nabucasa.const import STATE_CONNECTED from homeassistant.core import State from homeassistant.auth.providers import trusted_networks as tn_auth -from homeassistant.components.cloud.const import ( - PREF_ENABLE_GOOGLE, - PREF_ENABLE_ALEXA, - PREF_GOOGLE_SECURE_DEVICES_PIN, - DOMAIN, - RequireRelink, -) +from homeassistant.components.cloud.const import DOMAIN, RequireRelink from homeassistant.components.google_assistant.helpers import GoogleEntity from homeassistant.components.alexa.entities import LightCapabilities from homeassistant.components.alexa import errors as alexa_errors @@ -474,9 +468,9 @@ async def test_websocket_update_preferences( hass, hass_ws_client, aioclient_mock, setup_api, mock_cloud_login ): """Test updating preference.""" - assert setup_api[PREF_ENABLE_GOOGLE] - assert setup_api[PREF_ENABLE_ALEXA] - assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] is None + assert setup_api.google_enabled + assert setup_api.alexa_enabled + assert setup_api.google_secure_devices_pin is None client = await hass_ws_client(hass) await client.send_json( { @@ -490,9 +484,9 @@ async def test_websocket_update_preferences( response = await client.receive_json() assert response["success"] - assert not setup_api[PREF_ENABLE_GOOGLE] - assert not setup_api[PREF_ENABLE_ALEXA] - assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] == "1234" + assert not setup_api.google_enabled + assert not setup_api.alexa_enabled + assert setup_api.google_secure_devices_pin == "1234" async def test_websocket_update_preferences_require_relink( diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index 8049ac4b0db..09522e9c86f 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -12,12 +12,23 @@ class MockConfig(helpers.AbstractConfig): should_expose=None, entity_config=None, hass=None, + local_sdk_webhook_id=None, + local_sdk_user_id=None, + enabled=True, ): """Initialize config.""" super().__init__(hass) self._should_expose = should_expose self._secure_devices_pin = secure_devices_pin self._entity_config = entity_config or {} + self._local_sdk_webhook_id = local_sdk_webhook_id + self._local_sdk_user_id = local_sdk_user_id + self._enabled = enabled + + @property + def enabled(self): + """Return if Google is enabled.""" + return self._enabled @property def secure_devices_pin(self): @@ -29,6 +40,16 @@ class MockConfig(helpers.AbstractConfig): """Return secure devices pin.""" return self._entity_config + @property + def local_sdk_webhook_id(self): + """Return local SDK webhook id.""" + return self._local_sdk_webhook_id + + @property + def local_sdk_user_id(self): + """Return local SDK webhook id.""" + return self._local_sdk_user_id + def should_expose(self, state): """Expose it all.""" return self._should_expose is None or self._should_expose(state) diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py new file mode 100644 index 00000000000..497b7b1f0ae --- /dev/null +++ b/tests/components/google_assistant/test_helpers.py @@ -0,0 +1,130 @@ +"""Test Google Assistant helpers.""" +from unittest.mock import Mock +from homeassistant.setup import async_setup_component +from homeassistant.components.google_assistant import helpers +from homeassistant.components.google_assistant.const import EVENT_COMMAND_RECEIVED +from . import MockConfig + +from tests.common import async_capture_events, async_mock_service + + +async def test_google_entity_sync_serialize_with_local_sdk(hass): + """Test sync serialize attributes of a GoogleEntity.""" + hass.states.async_set("light.ceiling_lights", "off") + hass.config.api = Mock(port=1234, use_ssl=True) + config = MockConfig( + hass=hass, + local_sdk_webhook_id="mock-webhook-id", + local_sdk_user_id="mock-user-id", + ) + entity = helpers.GoogleEntity(hass, config, hass.states.get("light.ceiling_lights")) + + serialized = await entity.sync_serialize() + assert "otherDeviceIds" not in serialized + assert "customData" not in serialized + + config.async_enable_local_sdk() + + serialized = await entity.sync_serialize() + assert serialized["otherDeviceIds"] == [{"deviceId": "light.ceiling_lights"}] + assert serialized["customData"] == { + "httpPort": 1234, + "httpSSL": True, + "proxyDeviceId": None, + "webhookId": "mock-webhook-id", + } + + +async def test_config_local_sdk(hass, hass_client): + """Test the local SDK.""" + command_events = async_capture_events(hass, EVENT_COMMAND_RECEIVED) + turn_on_calls = async_mock_service(hass, "light", "turn_on") + hass.states.async_set("light.ceiling_lights", "off") + + assert await async_setup_component(hass, "webhook", {}) + + config = MockConfig( + hass=hass, + local_sdk_webhook_id="mock-webhook-id", + local_sdk_user_id="mock-user-id", + ) + + client = await hass_client() + + config.async_enable_local_sdk() + + resp = await client.post( + "/api/webhook/mock-webhook-id", + json={ + "inputs": [ + { + "context": {"locale_country": "US", "locale_language": "en"}, + "intent": "action.devices.EXECUTE", + "payload": { + "commands": [ + { + "devices": [{"id": "light.ceiling_lights"}], + "execution": [ + { + "command": "action.devices.commands.OnOff", + "params": {"on": True}, + } + ], + } + ], + "structureData": {}, + }, + } + ], + "requestId": "mock-req-id", + }, + ) + assert resp.status == 200 + result = await resp.json() + assert result["requestId"] == "mock-req-id" + + assert len(command_events) == 1 + assert command_events[0].context.user_id == config.local_sdk_user_id + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].context is command_events[0].context + + config.async_disable_local_sdk() + + # Webhook is no longer active + resp = await client.post("/api/webhook/mock-webhook-id") + assert resp.status == 200 + assert await resp.read() == b"" + + +async def test_config_local_sdk_if_disabled(hass, hass_client): + """Test the local SDK.""" + assert await async_setup_component(hass, "webhook", {}) + + config = MockConfig( + hass=hass, + local_sdk_webhook_id="mock-webhook-id", + local_sdk_user_id="mock-user-id", + enabled=False, + ) + + client = await hass_client() + + config.async_enable_local_sdk() + + resp = await client.post( + "/api/webhook/mock-webhook-id", json={"requestId": "mock-req-id"} + ) + assert resp.status == 200 + result = await resp.json() + assert result == { + "payload": {"errorCode": "deviceTurnedOff"}, + "requestId": "mock-req-id", + } + + config.async_disable_local_sdk() + + # Webhook is no longer active + resp = await client.post("/api/webhook/mock-webhook-id") + assert resp.status == 200 + assert await resp.read() == b"" diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index 6ecd4af446b..2f7fdb8e131 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -3,7 +3,7 @@ from unittest.mock import patch, Mock import pytest from homeassistant.core import State, EVENT_CALL_SERVICE -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS, __version__ from homeassistant.setup import async_setup_component from homeassistant.components import camera from homeassistant.components.climate.const import ( @@ -734,3 +734,137 @@ async def test_trait_execute_adding_query_data(hass): ] }, } + + +async def test_identify(hass): + """Test identify message.""" + result = await sh.async_handle_message( + hass, + BASIC_CONFIG, + None, + { + "requestId": REQ_ID, + "inputs": [ + { + "intent": "action.devices.IDENTIFY", + "payload": { + "device": { + "mdnsScanData": { + "additionals": [ + { + "type": "TXT", + "class": "IN", + "name": "devhome._home-assistant._tcp.local", + "ttl": 4500, + "data": [ + "version=0.101.0.dev0", + "base_url=http://192.168.1.101:8123", + "requires_api_password=true", + ], + } + ] + } + }, + "structureData": {}, + }, + } + ], + "devices": [ + { + "id": "light.ceiling_lights", + "customData": { + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", + }, + } + ], + }, + ) + + assert result == { + "requestId": REQ_ID, + "payload": { + "device": { + "id": BASIC_CONFIG.agent_user_id, + "isLocalOnly": True, + "isProxy": True, + "deviceInfo": { + "hwVersion": "UNKNOWN_HW_VERSION", + "manufacturer": "Home Assistant", + "model": "Home Assistant", + "swVersion": __version__, + }, + } + }, + } + + +async def test_reachable_devices(hass): + """Test REACHABLE_DEVICES intent.""" + # Matching passed in device. + hass.states.async_set("light.ceiling_lights", "on") + + # Unsupported entity + hass.states.async_set("not_supported.entity", "something") + + # Excluded via config + hass.states.async_set("light.not_expose", "on") + + # Not passed in as google_id + hass.states.async_set("light.not_mentioned", "on") + + config = MockConfig( + should_expose=lambda state: state.entity_id != "light.not_expose" + ) + + result = await sh.async_handle_message( + hass, + config, + None, + { + "requestId": REQ_ID, + "inputs": [ + { + "intent": "action.devices.REACHABLE_DEVICES", + "payload": { + "device": { + "proxyDevice": { + "id": "6a04f0f7-6125-4356-a846-861df7e01497", + "customData": "{}", + "proxyData": "{}", + } + }, + "structureData": {}, + }, + } + ], + "devices": [ + { + "id": "light.ceiling_lights", + "customData": { + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", + }, + }, + { + "id": "light.not_expose", + "customData": { + "httpPort": 8123, + "httpSSL": False, + "proxyDeviceId": BASIC_CONFIG.agent_user_id, + "webhookId": "dde3b9800a905e886cc4d38e226a6e7e3f2a6993d2b9b9f63d13e42ee7de3219", + }, + }, + {"id": BASIC_CONFIG.agent_user_id, "customData": {}}, + ], + }, + ) + + assert result == { + "requestId": REQ_ID, + "payload": {"devices": [{"verificationId": "light.ceiling_lights"}]}, + } diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index a5c527dacfe..d6ec24a7867 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -48,11 +48,11 @@ _LOGGER = logging.getLogger(__name__) REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" -BASIC_DATA = helpers.RequestData(BASIC_CONFIG, "test-agent", REQ_ID) +BASIC_DATA = helpers.RequestData(BASIC_CONFIG, "test-agent", REQ_ID, None) PIN_CONFIG = MockConfig(secure_devices_pin="1234") -PIN_DATA = helpers.RequestData(PIN_CONFIG, "test-agent", REQ_ID) +PIN_DATA = helpers.RequestData(PIN_CONFIG, "test-agent", REQ_ID, None) async def test_brightness_light(hass): From fe7467cd5c894a87c723bad77c2c228d9027f607 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Mon, 14 Oct 2019 00:01:14 +0200 Subject: [PATCH 256/639] Update pyhomematic to 0.1.61 (#27620) --- homeassistant/components/homematic/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 260e54e65c4..5db547e3f0a 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -3,7 +3,7 @@ "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", "requirements": [ - "pyhomematic==0.1.60" + "pyhomematic==0.1.61" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index bf36c898b1e..46a6916e787 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1226,7 +1226,7 @@ pyhik==0.2.4 pyhiveapi==0.2.19.3 # homeassistant.components.homematic -pyhomematic==0.1.60 +pyhomematic==0.1.61 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c3fa3024041..1c3444c37f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -420,7 +420,7 @@ pyhaversion==3.1.0 pyheos==0.6.0 # homeassistant.components.homematic -pyhomematic==0.1.60 +pyhomematic==0.1.61 # homeassistant.components.ipma pyipma==1.2.1 From b37f0ad8124f0077d699f218720148e167a420b7 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 14 Oct 2019 00:32:27 +0000 Subject: [PATCH 257/639] [ci skip] Translation update --- .../components/abode/.translations/en.json | 22 +++++++++++++++++++ .../components/abode/.translations/no.json | 22 +++++++++++++++++++ .../binary_sensor/.translations/es.json | 2 ++ .../components/cover/.translations/es.json | 10 +++++++++ .../components/cover/.translations/no.json | 4 ++-- .../components/cover/.translations/ru.json | 10 +++++++++ .../cover/.translations/zh-Hant.json | 10 +++++++++ .../components/deconz/.translations/no.json | 2 +- .../components/ecobee/.translations/no.json | 6 ++--- .../components/lock/.translations/es.json | 8 +++++++ .../components/lock/.translations/ru.json | 8 +++++++ .../lock/.translations/zh-Hant.json | 8 +++++++ .../components/plex/.translations/no.json | 4 ++-- .../components/soma/.translations/no.json | 2 +- .../transmission/.translations/no.json | 2 +- .../components/zha/.translations/no.json | 16 +++++++------- 16 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 homeassistant/components/abode/.translations/en.json create mode 100644 homeassistant/components/abode/.translations/no.json create mode 100644 homeassistant/components/cover/.translations/es.json create mode 100644 homeassistant/components/cover/.translations/ru.json create mode 100644 homeassistant/components/cover/.translations/zh-Hant.json create mode 100644 homeassistant/components/lock/.translations/es.json create mode 100644 homeassistant/components/lock/.translations/ru.json create mode 100644 homeassistant/components/lock/.translations/zh-Hant.json diff --git a/homeassistant/components/abode/.translations/en.json b/homeassistant/components/abode/.translations/en.json new file mode 100644 index 00000000000..e8daeb22c0a --- /dev/null +++ b/homeassistant/components/abode/.translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Only a single configuration of Abode is allowed." + }, + "error": { + "connection_error": "Unable to connect to Abode.", + "identifier_exists": "Account already registered.", + "invalid_credentials": "Invalid credentials." + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email Address" + }, + "title": "Fill in your Abode login information" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/no.json b/homeassistant/components/abode/.translations/no.json new file mode 100644 index 00000000000..542381cbb64 --- /dev/null +++ b/homeassistant/components/abode/.translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bare en enkelt konfigurasjon av Abode er tillatt." + }, + "error": { + "connection_error": "Kan ikke koble til Abode.", + "identifier_exists": "Kontoen er allerede registrert.", + "invalid_credentials": "Ugyldig brukerinformasjon" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "E-postadresse" + }, + "title": "Fyll ut innloggingsinformasjonen for Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/es.json b/homeassistant/components/binary_sensor/.translations/es.json index 8e2d326d9d3..756a370ca3c 100644 --- a/homeassistant/components/binary_sensor/.translations/es.json +++ b/homeassistant/components/binary_sensor/.translations/es.json @@ -53,6 +53,7 @@ "hot": "{entity_name} se est\u00e1 calentando", "light": "{entity_name} empez\u00f3 a detectar la luz", "locked": "{entity_name} bloqueado", + "moist": "{entity_name} se humedece", "moist\u00a7": "{entity_name} se humedeci\u00f3", "motion": "{entity_name} comenz\u00f3 a detectar movimiento", "moving": "{entity_name} empez\u00f3 a moverse", @@ -71,6 +72,7 @@ "not_moist": "{entity_name} se sec\u00f3", "not_moving": "{entity_name} dej\u00f3 de moverse", "not_occupied": "{entity_name} no est\u00e1 ocupado", + "not_opened": "{nombre_de_la_entidad} cerrado", "not_plugged_in": "{entity_name} desconectado", "not_powered": "{entity_name} no est\u00e1 activado", "not_present": "{entity_name} no est\u00e1 presente", diff --git a/homeassistant/components/cover/.translations/es.json b/homeassistant/components/cover/.translations/es.json new file mode 100644 index 00000000000..d0193b939a5 --- /dev/null +++ b/homeassistant/components/cover/.translations/es.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est\u00e1 cerrado", + "is_closing": "{entity_name} se est\u00e1 cerrando", + "is_open": "{entity_name} est\u00e1 abierto", + "is_opening": "{entity_name} se est\u00e1 abriendo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/no.json b/homeassistant/components/cover/.translations/no.json index ff37aa27d58..af567bcfcfc 100644 --- a/homeassistant/components/cover/.translations/no.json +++ b/homeassistant/components/cover/.translations/no.json @@ -1,8 +1,8 @@ { "device_automation": { "condition_type": { - "is_closed": "{entity_name} er lukket", - "is_closing": "{entity_name} lukker", + "is_closed": "{entity_name} er stengt", + "is_closing": "{entity_name} stenges", "is_open": "{entity_name} er \u00e5pen", "is_opening": "{entity_name} \u00e5pnes" } diff --git a/homeassistant/components/cover/.translations/ru.json b/homeassistant/components/cover/.translations/ru.json new file mode 100644 index 00000000000..46456bb9464 --- /dev/null +++ b/homeassistant/components/cover/.translations/ru.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_closing": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", + "is_open": "{entity_name} \u0432 \u043e\u0442\u043a\u0440\u044b\u0442\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_opening": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/zh-Hant.json b/homeassistant/components/cover/.translations/zh-Hant.json new file mode 100644 index 00000000000..9723d1a0dd6 --- /dev/null +++ b/homeassistant/components/cover/.translations/zh-Hant.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u5df2\u95dc\u9589", + "is_closing": "{entity_name} \u6b63\u5728\u95dc\u9589", + "is_open": "{entity_name} \u5df2\u958b\u555f", + "is_opening": "{entity_name} \u6b63\u5728\u958b\u555f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 7d05a366cf2..71fba6043f7 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -64,7 +64,7 @@ "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", "remote_button_quintuple_press": "\"{subtype}\"-knappen femdobbelt klikket", "remote_button_rotated": "Knappen roterte \"{subtype}\"", - "remote_button_rotation_stopped": "Knappe rotasjon \"{subtype}\" stoppet", + "remote_button_rotation_stopped": "Knapperotasjon \"{subtype}\" stoppet", "remote_button_short_press": "\"{subtype}\" -knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", "remote_button_triple_press": "\"{subtype}\"-knappen trippel klikket", diff --git a/homeassistant/components/ecobee/.translations/no.json b/homeassistant/components/ecobee/.translations/no.json index 2bf141f6489..efaa566c424 100644 --- a/homeassistant/components/ecobee/.translations/no.json +++ b/homeassistant/components/ecobee/.translations/no.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare en ecobee-forekomst." + "one_instance_only": "Denne integrasjonen st\u00f8tter forel\u00f8pig bare \u00e9n ecobee-forekomst." }, "error": { "pin_request_failed": "Feil under foresp\u00f8rsel om PIN-kode fra ecobee. Kontroller at API-n\u00f8kkelen er riktig.", - "token_request_failed": "Feil ved foresp\u00f8rsel om tokener fra ecobee; Pr\u00f8v p\u00e5 nytt." + "token_request_failed": "Feil ved foresp\u00f8rsel om tokener fra ecobee: Pr\u00f8v p\u00e5 nytt." }, "step": { "authorize": { - "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nDeretter, trykk p\u00e5 Send.", + "description": "Vennligst autoriser denne appen p\u00e5 https://www.ecobee.com/consumerportal/index.html med pin-kode:\n\n{pin}\n\nTrykk deretter p\u00e5 Send.", "title": "Autoriser app p\u00e5 ecobee.com" }, "user": { diff --git a/homeassistant/components/lock/.translations/es.json b/homeassistant/components/lock/.translations/es.json new file mode 100644 index 00000000000..5c23c270f61 --- /dev/null +++ b/homeassistant/components/lock/.translations/es.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_unlocked": "{entity_name} est\u00e1 desbloqueado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ru.json b/homeassistant/components/lock/.translations/ru.json new file mode 100644 index 00000000000..f74df838ae5 --- /dev/null +++ b/homeassistant/components/lock/.translations/ru.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", + "is_unlocked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/zh-Hant.json b/homeassistant/components/lock/.translations/zh-Hant.json new file mode 100644 index 00000000000..a423c4331b7 --- /dev/null +++ b/homeassistant/components/lock/.translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} \u5df2\u4e0a\u9396", + "is_unlocked": "{entity_name} \u5df2\u89e3\u9396" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index 18c4e865a84..8ebd2b69bb9 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -24,7 +24,7 @@ "token": "Token (hvis n\u00f8dvendig)", "verify_ssl": "Verifisere SSL-sertifikat" }, - "title": "Plex server" + "title": "Plex-server" }, "select_server": { "data": { @@ -35,7 +35,7 @@ }, "start_website_auth": { "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv.", - "title": "Koble til Plex server" + "title": "Koble til Plex-server" }, "user": { "data": { diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json index 1ea53b778ea..c3d9d7e70d4 100644 --- a/homeassistant/components/soma/.translations/no.json +++ b/homeassistant/components/soma/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_setup": "Du kan bare konfigurere en Soma-konto.", + "already_setup": "Du kan bare konfigurere \u00e9n Soma-konto.", "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", "missing_configuration": "Soma-komponenten er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." }, diff --git a/homeassistant/components/transmission/.translations/no.json b/homeassistant/components/transmission/.translations/no.json index f6ddce2a4a7..94044e692d9 100644 --- a/homeassistant/components/transmission/.translations/no.json +++ b/homeassistant/components/transmission/.translations/no.json @@ -22,7 +22,7 @@ "port": "Port", "username": "Brukernavn" }, - "title": "Oppsett av klient for Transmission" + "title": "Oppsett av Transmission-klient" } }, "title": "Transmission" diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index 18c4c3c9ff2..a70f5ad1c33 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -19,8 +19,8 @@ }, "device_automation": { "action_type": { - "squawk": "Squawk", - "warn": "Advarer" + "squawk": "Varsle", + "warn": "Advar" }, "trigger_subtype": { "both_buttons": "Begge knapper", @@ -39,7 +39,7 @@ "face_4": "med ansikt 4 aktivert", "face_5": "med ansikt 5 aktivert", "face_6": "med ansikt 6 aktivert", - "face_any": "Med alle/angitte ansikt (er) aktivert", + "face_any": "Med alle/angitte ansikt(er) aktivert", "left": "Venstre", "open": "\u00c5pen", "right": "H\u00f8yre", @@ -47,7 +47,7 @@ "turn_on": "Sl\u00e5 p\u00e5" }, "trigger_type": { - "device_dropped": "Enheten ble brutt", + "device_dropped": "Enheten ble sluppet", "device_flipped": "Enheten snudd \"{subtype}\"", "device_knocked": "Enheten sl\u00e5tt \"{subtype}\"", "device_rotated": "Enheten roterte \"{subtype}\"", @@ -55,13 +55,13 @@ "device_slid": "Enheten skled \"{subtype}\"", "device_tilted": "Enheten skr\u00e5stilt", "remote_button_double_press": "\"{subtype}\"-knappen ble dobbeltklikket", - "remote_button_long_press": "\"{subtype}\"-knappen ble kontinuerlig trykket", + "remote_button_long_press": "\"{subtype}\"-knappen ble holdt inne", "remote_button_long_release": "\"{subtype}\"-knappen sluppet etter langt trykk", - "remote_button_quadruple_press": "\"{subtype}\"-knappen ble firedoblet klikket", - "remote_button_quintuple_press": "\"{subtype}\"-knappen ble femdobbelt klikket", + "remote_button_quadruple_press": "\"{subtype}\"-knappen ble trykket fire ganger", + "remote_button_quintuple_press": "\"{subtype}\"-knappen ble trykket fem ganger", "remote_button_short_press": "\"{subtype}\"-knappen ble trykket", "remote_button_short_release": "\"{subtype}\"-knappen sluppet", - "remote_button_triple_press": "\"{subtype}\"-knappen ble trippel klikket" + "remote_button_triple_press": "\"{subtype}\"-knappen ble trippelklikket" } } } \ No newline at end of file From cf76f22c8933ccac9ccbaf2bfe7e97cb6c7e3feb Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 13 Oct 2019 23:59:25 -0500 Subject: [PATCH 258/639] Rewrite Plex tests (#27624) --- tests/components/plex/mock_classes.py | 96 ++++-- tests/components/plex/test_config_flow.py | 339 +++++++++------------- 2 files changed, 217 insertions(+), 218 deletions(-) diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 87fb6df5971..756249110ed 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -1,35 +1,97 @@ """Mock classes used in tests.""" +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.components.plex.const import CONF_SERVER, CONF_SERVER_IDENTIFIER -MOCK_HOST_1 = "1.2.3.4" -MOCK_PORT_1 = 32400 -MOCK_HOST_2 = "4.3.2.1" -MOCK_PORT_2 = 32400 +MOCK_SERVERS = [ + { + CONF_HOST: "1.2.3.4", + CONF_PORT: 32400, + CONF_SERVER: "Plex Server 1", + CONF_SERVER_IDENTIFIER: "unique_id_123", + }, + { + CONF_HOST: "4.3.2.1", + CONF_PORT: 32400, + CONF_SERVER: "Plex Server 2", + CONF_SERVER_IDENTIFIER: "unique_id_456", + }, +] -class MockAvailableServer: # pylint: disable=too-few-public-methods - """Mock avilable server objects.""" +class MockResource: + """Mock a PlexAccount resource.""" - def __init__(self, name, client_id): + def __init__(self, index): """Initialize the object.""" - self.name = name - self.clientIdentifier = client_id # pylint: disable=invalid-name + self.name = MOCK_SERVERS[index][CONF_SERVER] + self.clientIdentifier = MOCK_SERVERS[index][ # pylint: disable=invalid-name + CONF_SERVER_IDENTIFIER + ] self.provides = ["server"] + self._mock_plex_server = MockPlexServer(index) + self._connections = [] + for connection in range(2): + self._connections.append(MockConnection(connection)) + + @property + def connections(self): + """Mock the resource connection listing method.""" + return self._connections + + def connect(self): + """Mock the resource connect method.""" + return self._mock_plex_server class MockConnection: # pylint: disable=too-few-public-methods """Mock a single account resource connection object.""" - def __init__(self, ssl): + def __init__(self, index, ssl=True): """Initialize the object.""" prefix = "https" if ssl else "http" - self.httpuri = f"{prefix}://{MOCK_HOST_1}:{MOCK_PORT_1}" - self.uri = "{prefix}://{MOCK_HOST_2}:{MOCK_PORT_2}" - self.local = True + self.httpuri = ( + f"http://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" + ) + self.uri = f"{prefix}://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" + # Only first server is local + self.local = not bool(index) -class MockConnections: # pylint: disable=too-few-public-methods - """Mock a list of resource connections.""" +class MockPlexAccount: + """Mock a PlexAccount instance.""" - def __init__(self, ssl=False): + def __init__(self, servers=1): """Initialize the object.""" - self.connections = [MockConnection(ssl)] + self._resources = [] + for index in range(servers): + self._resources.append(MockResource(index)) + + def resource(self, name): + """Mock the PlexAccount resource lookup method.""" + return [x for x in self._resources if x.name == name][0] + + def resources(self): + """Mock the PlexAccount resources listing method.""" + return self._resources + + +class MockPlexServer: + """Mock a PlexServer instance.""" + + def __init__(self, index=0, ssl=True): + """Initialize the object.""" + host = MOCK_SERVERS[index][CONF_HOST] + port = MOCK_SERVERS[index][CONF_PORT] + self.friendlyName = MOCK_SERVERS[index][ # pylint: disable=invalid-name + CONF_SERVER + ] + self.machineIdentifier = MOCK_SERVERS[index][ # pylint: disable=invalid-name + CONF_SERVER_IDENTIFIER + ] + prefix = "https" if ssl else "http" + self._baseurl = f"{prefix}://{host}:{port}" + + @property + def url_in_use(self): + """Return URL used by PlexServer.""" + return self._baseurl diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index e9f48f6a4f8..2a2178da9d5 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,28 +1,26 @@ """Tests for Plex config flow.""" -from unittest.mock import MagicMock, Mock, patch, PropertyMock +from unittest.mock import patch import asynctest import plexapi.exceptions import requests.exceptions from homeassistant.components.plex import config_flow -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL, CONF_TOKEN, CONF_URL from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -from .mock_classes import MOCK_HOST_1, MOCK_PORT_1, MockAvailableServer, MockConnections +from .mock_classes import MOCK_SERVERS, MockPlexAccount, MockPlexServer -MOCK_NAME_1 = "Plex Server 1" -MOCK_ID_1 = "unique_id_123" -MOCK_NAME_2 = "Plex Server 2" -MOCK_ID_2 = "unique_id_456" MOCK_TOKEN = "secret_token" MOCK_FILE_CONTENTS = { - f"{MOCK_HOST_1}:{MOCK_PORT_1}": {"ssl": False, "token": MOCK_TOKEN, "verify": True} + f"{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}": { + "ssl": False, + "token": MOCK_TOKEN, + "verify": True, + } } -MOCK_SERVER_1 = MockAvailableServer(MOCK_NAME_1, MOCK_ID_1) -MOCK_SERVER_2 = MockAvailableServer(MOCK_NAME_2, MOCK_ID_2) DEFAULT_OPTIONS = { config_flow.MP_DOMAIN: { @@ -41,25 +39,21 @@ def init_config_flow(hass): async def test_bad_credentials(hass): """Test when provided credentials are rejected.""" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + assert result["step_id"] == "start_website_auth" + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + + with patch( + "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value="BAD TOKEN" ): - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} - ) - assert result["type"] == "form" - assert result["step_id"] == "start_website_auth" - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -74,31 +68,32 @@ async def test_import_file_from_discovery(hass): """Test importing a legacy file during discovery.""" file_host_and_port, file_config = list(MOCK_FILE_CONTENTS.items())[0] - used_url = f"http://{file_host_and_port}" + file_use_ssl = file_config[CONF_SSL] + file_prefix = "https" if file_use_ssl else "http" + used_url = f"{file_prefix}://{file_host_and_port}" - with patch("plexapi.server.PlexServer") as mock_plex_server, patch( + mock_plex_server = MockPlexServer(ssl=file_use_ssl) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( "homeassistant.components.plex.config_flow.load_json", return_value=MOCK_FILE_CONTENTS, ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_ID_1 - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_NAME_1 - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=used_url) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "discovery"}, - data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, ) assert result["type"] == "create_entry" - assert result["title"] == MOCK_NAME_1 - assert result["data"][config_flow.CONF_SERVER] == MOCK_NAME_1 - assert result["data"][config_flow.CONF_SERVER_IDENTIFIER] == MOCK_ID_1 + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName + assert ( + result["data"][config_flow.CONF_SERVER_IDENTIFIER] + == mock_plex_server.machineIdentifier + ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] == used_url assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] @@ -112,7 +107,10 @@ async def test_discovery(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "discovery"}, - data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, ) assert result["type"] == "abort" assert result["reason"] == "discovery_no_file" @@ -128,7 +126,10 @@ async def test_discovery_while_in_progress(hass): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "discovery"}, - data={CONF_HOST: MOCK_HOST_1, CONF_PORT: MOCK_PORT_1}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -137,42 +138,28 @@ async def test_discovery_while_in_progress(hass): async def test_import_success(hass): """Test a successful configuration import.""" - mock_connections = MockConnections(ssl=True) - - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - - with patch("plexapi.server.PlexServer") as mock_plex_server: - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) + mock_plex_server = MockPlexServer() + with patch("plexapi.server.PlexServer", return_value=mock_plex_server): result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": "import"}, data={ CONF_TOKEN: MOCK_TOKEN, - CONF_URL: f"https://{MOCK_HOST_1}:{MOCK_PORT_1}", + CONF_URL: f"https://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", }, ) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -188,7 +175,7 @@ async def test_import_bad_hostname(hass): context={"source": "import"}, data={ CONF_TOKEN: MOCK_TOKEN, - CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}", + CONF_URL: f"http://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", }, ) assert result["type"] == "form" @@ -205,19 +192,12 @@ async def test_unknown_exception(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer", side_effect=Exception - ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( - "plexauth.PlexAuth.token", return_value="MOCK_TOKEN" - ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception), asynctest.patch( + "plexauth.PlexAuth.initiate_auth" + ), asynctest.patch("plexauth.PlexAuth.token", return_value="MOCK_TOKEN"): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -237,18 +217,15 @@ async def test_no_servers_found(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[]) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" with patch( - "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0) ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -261,6 +238,8 @@ async def test_no_servers_found(hass): async def test_single_available_server(hass): """Test creating an entry with one server available.""" + mock_plex_server = MockPlexServer() + await async_setup_component(hass, "http", {"http": {}}) result = await hass.config_entries.flow.async_init( @@ -269,46 +248,28 @@ async def test_single_available_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer" - ) as mock_plex_server, asynctest.patch( - "plexauth.PlexAuth.initiate_auth" - ), asynctest.patch( + with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -316,6 +277,8 @@ async def test_single_available_server(hass): async def test_multiple_servers_with_selection(hass): """Test creating an entry with multiple servers available.""" + mock_plex_server = MockPlexServer() + await async_setup_component(hass, "http", {"http": {}}) result = await hass.config_entries.flow.async_init( @@ -324,31 +287,18 @@ async def test_multiple_servers_with_selection(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) - mm_plex_account.resource = Mock(return_value=mock_connections) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer" - ) as mock_plex_server, asynctest.patch( + with patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) + ), patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch( "plexauth.PlexAuth.initiate_auth" ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -357,18 +307,21 @@ async def test_multiple_servers_with_selection(hass): assert result["step_id"] == "select_server" result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={config_flow.CONF_SERVER: MOCK_SERVER_1.name} + result["flow_id"], + user_input={ + config_flow.CONF_SERVER: MOCK_SERVERS[0][config_flow.CONF_SERVER] + }, ) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -376,13 +329,17 @@ async def test_multiple_servers_with_selection(hass): async def test_adding_last_unconfigured_server(hass): """Test automatically adding last unconfigured server when multiple servers on account.""" + mock_plex_server = MockPlexServer() + await async_setup_component(hass, "http", {"http": {}}) MockConfigEntry( domain=config_flow.DOMAIN, data={ - config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2, - config_flow.CONF_SERVER: MOCK_NAME_2, + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[1][ + config_flow.CONF_SERVER_IDENTIFIER + ], + config_flow.CONF_SERVER: MOCK_SERVERS[1][config_flow.CONF_SERVER], }, ).add_to_hass(hass) @@ -392,45 +349,32 @@ async def test_adding_last_unconfigured_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) - mm_plex_account.resource = Mock(return_value=mock_connections) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", return_value=mm_plex_account), patch( - "plexapi.server.PlexServer" - ) as mock_plex_server, asynctest.patch( + with patch( + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) + ), patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch( "plexauth.PlexAuth.initiate_auth" ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" - assert result["title"] == MOCK_SERVER_1.name - assert result["data"][config_flow.CONF_SERVER] == MOCK_SERVER_1.name + assert result["title"] == mock_plex_server.friendlyName + assert result["data"][config_flow.CONF_SERVER] == mock_plex_server.friendlyName assert ( result["data"][config_flow.CONF_SERVER_IDENTIFIER] - == MOCK_SERVER_1.clientIdentifier + == mock_plex_server.machineIdentifier ) assert ( result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_URL] - == mock_connections.connections[0].httpuri + == mock_plex_server.url_in_use ) assert result["data"][config_flow.PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN @@ -438,32 +382,28 @@ async def test_adding_last_unconfigured_server(hass): async def test_already_configured(hass): """Test a duplicated successful flow.""" + mock_plex_server = MockPlexServer() + flow = init_config_flow(hass) MockConfigEntry( - domain=config_flow.DOMAIN, data={config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1} + domain=config_flow.DOMAIN, + data={ + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][ + config_flow.CONF_SERVER_IDENTIFIER + ] + }, ).add_to_hass(hass) - mock_connections = MockConnections() - - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1]) - mm_plex_account.resource = Mock(return_value=mock_connections) - - with patch("plexapi.server.PlexServer") as mock_plex_server, asynctest.patch( - "plexauth.PlexAuth.initiate_auth" - ), asynctest.patch("plexauth.PlexAuth.token", return_value=MOCK_TOKEN): - type(mock_plex_server.return_value).machineIdentifier = PropertyMock( - return_value=MOCK_SERVER_1.clientIdentifier - ) - type(mock_plex_server.return_value).friendlyName = PropertyMock( - return_value=MOCK_SERVER_1.name - ) - type( # pylint: disable=protected-access - mock_plex_server.return_value - )._baseurl = PropertyMock(return_value=mock_connections.connections[0].httpuri) - + with patch( + "plexapi.server.PlexServer", return_value=mock_plex_server + ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( + "plexauth.PlexAuth.token", return_value=MOCK_TOKEN + ): result = await flow.async_step_import( - {CONF_TOKEN: MOCK_TOKEN, CONF_URL: f"http://{MOCK_HOST_1}:{MOCK_PORT_1}"} + { + CONF_TOKEN: MOCK_TOKEN, + CONF_URL: f"http://{MOCK_SERVERS[0][CONF_HOST]}:{MOCK_SERVERS[0][CONF_PORT]}", + } ) assert result["type"] == "abort" assert result["reason"] == "already_configured" @@ -477,16 +417,20 @@ async def test_all_available_servers_configured(hass): MockConfigEntry( domain=config_flow.DOMAIN, data={ - config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_1, - config_flow.CONF_SERVER: MOCK_NAME_1, + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][ + config_flow.CONF_SERVER_IDENTIFIER + ], + config_flow.CONF_SERVER: MOCK_SERVERS[0][config_flow.CONF_SERVER], }, ).add_to_hass(hass) MockConfigEntry( domain=config_flow.DOMAIN, data={ - config_flow.CONF_SERVER_IDENTIFIER: MOCK_ID_2, - config_flow.CONF_SERVER: MOCK_NAME_2, + config_flow.CONF_SERVER_IDENTIFIER: MOCK_SERVERS[1][ + config_flow.CONF_SERVER_IDENTIFIER + ], + config_flow.CONF_SERVER: MOCK_SERVERS[1][config_flow.CONF_SERVER], }, ).add_to_hass(hass) @@ -496,20 +440,14 @@ async def test_all_available_servers_configured(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - mock_connections = MockConnections() - mm_plex_account = MagicMock() - mm_plex_account.resources = Mock(return_value=[MOCK_SERVER_1, MOCK_SERVER_2]) - mm_plex_account.resource = Mock(return_value=mock_connections) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" with patch( - "plexapi.myplex.MyPlexAccount", return_value=mm_plex_account + "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -557,13 +495,12 @@ async def test_external_timed_out(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=None ): - - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -583,12 +520,12 @@ async def test_callback_view(hass, aiohttp_client): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - client = await aiohttp_client(hass.http.app) forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' From afa7e0bfe875ec2df146658f85eddf97e5816cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 07:01:40 +0200 Subject: [PATCH 259/639] fix: exception after kaiterra api call timeout (#27622) --- homeassistant/components/kaiterra/__init__.py | 20 ++++++++--------- .../components/kaiterra/air_quality.py | 8 +++---- homeassistant/components/kaiterra/api_data.py | 22 ++++++++----------- homeassistant/components/kaiterra/sensor.py | 8 +++---- 4 files changed, 24 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/kaiterra/__init__.py b/homeassistant/components/kaiterra/__init__.py index 8c61ad54184..d043dc15eaf 100644 --- a/homeassistant/components/kaiterra/__init__.py +++ b/homeassistant/components/kaiterra/__init__.py @@ -1,35 +1,33 @@ """Support for Kaiterra devices.""" import voluptuous as vol -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.discovery import async_load_platform -from homeassistant.helpers import config_validation as cv - from homeassistant.const import ( CONF_API_KEY, - CONF_DEVICES, CONF_DEVICE_ID, + CONF_DEVICES, + CONF_NAME, CONF_SCAN_INTERVAL, CONF_TYPE, - CONF_NAME, ) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.event import async_track_time_interval +from .api_data import KaiterraApiData from .const import ( AVAILABLE_AQI_STANDARDS, - AVAILABLE_UNITS, AVAILABLE_DEVICE_TYPES, + AVAILABLE_UNITS, CONF_AQI_STANDARD, CONF_PREFERRED_UNITS, - DOMAIN, DEFAULT_AQI_STANDARD, DEFAULT_PREFERRED_UNIT, DEFAULT_SCAN_INTERVAL, + DOMAIN, KAITERRA_COMPONENTS, ) -from .api_data import KaiterraApiData - KAITERRA_DEVICE_SCHEMA = vol.Schema( { vol.Required(CONF_DEVICE_ID): cv.string, diff --git a/homeassistant/components/kaiterra/air_quality.py b/homeassistant/components/kaiterra/air_quality.py index 70699de394c..1de1a4bd6c5 100644 --- a/homeassistant/components/kaiterra/air_quality.py +++ b/homeassistant/components/kaiterra/air_quality.py @@ -1,16 +1,14 @@ """Support for Kaiterra Air Quality Sensors.""" from homeassistant.components.air_quality import AirQualityEntity - +from homeassistant.const import CONF_DEVICE_ID, CONF_NAME from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.const import CONF_DEVICE_ID, CONF_NAME - from .const import ( - DOMAIN, - ATTR_VOC, ATTR_AQI_LEVEL, ATTR_AQI_POLLUTANT, + ATTR_VOC, DISPATCHER_KAITERRA, + DOMAIN, ) diff --git a/homeassistant/components/kaiterra/api_data.py b/homeassistant/components/kaiterra/api_data.py index 81e28438d56..e0f4d817e03 100644 --- a/homeassistant/components/kaiterra/api_data.py +++ b/homeassistant/components/kaiterra/api_data.py @@ -1,21 +1,17 @@ """Data for all Kaiterra devices.""" +import asyncio from logging import getLogger -import asyncio - -import async_timeout - from aiohttp.client_exceptions import ClientResponseError +import async_timeout +from kaiterra_async_client import AQIStandard, KaiterraAPIClient, Units -from kaiterra_async_client import KaiterraAPIClient, AQIStandard, Units - +from homeassistant.const import CONF_API_KEY, CONF_DEVICE_ID, CONF_DEVICES, CONF_TYPE from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_DEVICE_ID, CONF_TYPE - from .const import ( - AQI_SCALE, AQI_LEVEL, + AQI_SCALE, CONF_AQI_STANDARD, CONF_PREFERRED_UNITS, DISPATCHER_KAITERRA, @@ -60,9 +56,10 @@ class KaiterraApiData: with async_timeout.timeout(10): data = await self._api.get_latest_sensor_readings(self._devices) except (ClientResponseError, asyncio.TimeoutError): - _LOGGER.debug("Couldn't fetch data") + _LOGGER.debug("Couldn't fetch data from Kaiterra API") self.data = {} async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + return _LOGGER.debug("New data retrieved: %s", data) @@ -102,8 +99,7 @@ class KaiterraApiData: device["aqi_pollutant"] = {"value": main_pollutant} self.data[self._devices_ids[i]] = device - - async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) except IndexError as err: _LOGGER.error("Parsing error %s", err) - async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) + + async_dispatcher_send(self._hass, DISPATCHER_KAITERRA) diff --git a/homeassistant/components/kaiterra/sensor.py b/homeassistant/components/kaiterra/sensor.py index 4ff6435b64d..e86d6f7d836 100644 --- a/homeassistant/components/kaiterra/sensor.py +++ b/homeassistant/components/kaiterra/sensor.py @@ -1,11 +1,9 @@ """Support for Kaiterra Temperature ahn Humidity Sensors.""" +from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from homeassistant.const import CONF_DEVICE_ID, CONF_NAME, TEMP_CELSIUS, TEMP_FAHRENHEIT - -from .const import DOMAIN, DISPATCHER_KAITERRA +from .const import DISPATCHER_KAITERRA, DOMAIN SENSORS = [ {"name": "Temperature", "prop": "rtemp", "device_class": "temperature"}, From 2cf3f6bffaa0782a5bfbf890f357c843dea6a204 Mon Sep 17 00:00:00 2001 From: "Steven D. Lander" <3169732+stevendlander@users.noreply.github.com> Date: Mon, 14 Oct 2019 01:02:01 -0400 Subject: [PATCH 260/639] Issue #27288 Moving imports to top for tesla component (#27618) --- homeassistant/components/tesla/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 4c90f0784af..a08112d66b3 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -3,6 +3,8 @@ from collections import defaultdict import logging import voluptuous as vol +from teslajsonpy import Controller as teslaAPI, TeslaException + from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -52,8 +54,6 @@ TESLA_COMPONENTS = [ def setup(hass, base_config): """Set up of Tesla component.""" - from teslajsonpy import Controller as teslaAPI, TeslaException - config = base_config.get(DOMAIN) email = config.get(CONF_USERNAME) From da29c1125fac4fd9059ffec82899d65e751ef6be Mon Sep 17 00:00:00 2001 From: Moritz Fey Date: Mon, 14 Oct 2019 07:13:32 +0200 Subject: [PATCH 261/639] add content for services.yaml for ccomponent stream (#27610) --- homeassistant/components/stream/services.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/homeassistant/components/stream/services.yaml b/homeassistant/components/stream/services.yaml index e69de29bb2d..c3b25e06348 100644 --- a/homeassistant/components/stream/services.yaml +++ b/homeassistant/components/stream/services.yaml @@ -0,0 +1,15 @@ +record: + description: Make a .mp4 recording from a provided stream. + fields: + stream_source: + description: The input source for the stream. + example: "rtsp://my.stream.feed:554" + filename: + description: The file name string. + example: "/tmp/my_stream.mp4" + duration: + description: "Target recording length (in seconds). Default: 30" + example: 30 + lookback: + description: "Target lookback period (in seconds) to include in addition to duration. Only available if there is currently an active HLS stream for stream_source. Default: 0" + example: 5 \ No newline at end of file From ff4e35e0ad86d2d569dfeda96277a723594a4f28 Mon Sep 17 00:00:00 2001 From: Askarov Rishat Date: Mon, 14 Oct 2019 11:12:08 +0300 Subject: [PATCH 262/639] Update yandex transport after api change (#27591) * yandex maps api changed ("threads" in "Transport" added), ya_ma=>0.3.8 bug_fixed * Update homeassistant/components/yandex_transport/sensor.py Co-Authored-By: Paulus Schoutsen * additional fix * reformat * fix mistake --- .../components/yandex_transport/manifest.json | 2 +- .../components/yandex_transport/sensor.py | 27 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../test_yandex_transport_sensor.py | 8 +- tests/fixtures/yandex_transport_reply.json | 3656 +++++++---------- 6 files changed, 1577 insertions(+), 2120 deletions(-) diff --git a/homeassistant/components/yandex_transport/manifest.json b/homeassistant/components/yandex_transport/manifest.json index 91267b43480..44dcf5b100c 100644 --- a/homeassistant/components/yandex_transport/manifest.json +++ b/homeassistant/components/yandex_transport/manifest.json @@ -3,7 +3,7 @@ "name": "Yandex Transport", "documentation": "https://www.home-assistant.io/integrations/yandex_transport", "requirements": [ - "ya_ma==0.3.7" + "ya_ma==0.3.8" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/yandex_transport/sensor.py b/homeassistant/components/yandex_transport/sensor.py index 26311a4c72e..4bf634a61f4 100644 --- a/homeassistant/components/yandex_transport/sensor.py +++ b/homeassistant/components/yandex_transport/sensor.py @@ -79,18 +79,21 @@ class DiscoverMoscowYandexTransport(Entity): transport_list = stop_metadata["Transport"] for transport in transport_list: route = transport["name"] - if self._routes and route not in self._routes: - # skip unnecessary route info - continue - if "Events" in transport["BriefSchedule"]: - for event in transport["BriefSchedule"]["Events"]: - if "Estimated" in event: - posix_time_next = int(event["Estimated"]["value"]) - if closer_time is None or closer_time > posix_time_next: - closer_time = posix_time_next - if route not in attrs: - attrs[route] = [] - attrs[route].append(event["Estimated"]["text"]) + for thread in transport["threads"]: + if self._routes and route not in self._routes: + # skip unnecessary route info + continue + if "Events" not in thread["BriefSchedule"]: + continue + for event in thread["BriefSchedule"]["Events"]: + if "Estimated" not in event: + continue + posix_time_next = int(event["Estimated"]["value"]) + if closer_time is None or closer_time > posix_time_next: + closer_time = posix_time_next + if route not in attrs: + attrs[route] = [] + attrs[route].append(event["Estimated"]["text"]) attrs[STOP_NAME] = stop_name attrs[ATTR_ATTRIBUTION] = ATTRIBUTION if closer_time is None: diff --git a/requirements_all.txt b/requirements_all.txt index 46a6916e787..e4c608793b8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ xmltodict==0.12.0 xs1-api-client==2.3.5 # homeassistant.components.yandex_transport -ya_ma==0.3.7 +ya_ma==0.3.8 # homeassistant.components.yweather yahooweather==0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1c3444c37f9..ec8477c9369 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -626,7 +626,7 @@ withings-api==2.0.0b8 xmltodict==0.12.0 # homeassistant.components.yandex_transport -ya_ma==0.3.7 +ya_ma==0.3.8 # homeassistant.components.yweather yahooweather==0.10 diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index 50d945e7fae..7997d01bd13 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -38,14 +38,14 @@ TEST_CONFIG = { } FILTERED_ATTRS = { - "т36": ["21:43", "21:47", "22:02"], - "т47": ["21:40", "22:01"], - "м10": ["21:48", "22:00"], + "т36": ["16:10", "16:17", "16:26"], + "т47": ["16:09", "16:10"], + "м10": ["16:12", "16:20"], "stop_name": "7-й автобусный парк", "attribution": "Data provided by maps.yandex.ru", } -RESULT_STATE = dt_util.utc_from_timestamp(1568659253).isoformat(timespec="seconds") +RESULT_STATE = dt_util.utc_from_timestamp(1570972183).isoformat(timespec="seconds") async def assert_setup_sensor(hass, config, count=1): diff --git a/tests/fixtures/yandex_transport_reply.json b/tests/fixtures/yandex_transport_reply.json index c5e4857297a..3189d7a9d9b 100644 --- a/tests/fixtures/yandex_transport_reply.json +++ b/tests/fixtures/yandex_transport_reply.json @@ -1,2106 +1,1560 @@ { - "data": { - "geometries": [ - { - "type": "Point", - "coordinates": [ - 37.565280044, - 55.851959656 - ] - } - ], - "geometry": { - "type": "Point", - "coordinates": [ - 37.565280044, - 55.851959656 + "data": { + "geometries": [ + { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + } + ], + "geometry": { + "type": "Point", + "coordinates": [ + 37.565280044, + 55.851959656 + ] + }, + "properties": { + "name": "7-й автобусный парк", + "description": "7-й автобусный парк", + "currentTime": 1570971868567, + "tzOffset": 10800, + "StopMetaData": { + "id": "stop__9639579", + "name": "7-й автобусный парк", + "type": "urban", + "region": { + "id": 213, + "type": 6, + "parent_id": 1, + "capital_id": 0, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow", + "native_name": "", + "iso_name": "RU MOW", + "is_main": true, + "en_name": "Moscow", + "short_en_name": "MSK", + "phone_code": "495 499", + "phone_code_old": "095", + "zip_code": "", + "population": 12506468, + "synonyms": "Moskau, Moskva", + "latitude": 55.753215, + "longitude": 37.622504, + "latitude_size": 0.878654, + "longitude_size": 1.164423, + "zoom": 10, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "weather", + "afisha", + "maps", + "tv", + "ad", + "etrain", + "subway", + "delivery", + "route" + ], + "ename": "moscow", + "bounds": [ + [ + 37.0402925, + 55.31141404514547 + ], + [ + 38.2047155, + 56.190068045145466 ] - }, - "properties": { - "name": "7-й автобусный парк", - "description": "7-й автобусный парк", - "currentTime": "Mon Sep 16 2019 21:40:40 GMT+0300 (Moscow Standard Time)", - "StopMetaData": { - "id": "stop__9639579", - "name": "7-й автобусный парк", - "type": "urban", - "region": { - "id": 213, - "type": 6, - "parent_id": 1, - "capital_id": 0, - "geo_parent_id": 0, - "city_id": 213, - "name": "moscow", - "native_name": "", - "iso_name": "RU MOW", - "is_main": true, - "en_name": "Moscow", - "short_en_name": "MSK", - "phone_code": "495 499", - "phone_code_old": "095", - "zip_code": "", - "population": 12506468, - "synonyms": "Moskau, Moskva", - "latitude": 55.753215, - "longitude": 37.622504, - "latitude_size": 0.878654, - "longitude_size": 1.164423, - "zoom": 10, - "tzname": "Europe/Moscow", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "weather", - "afisha", - "maps", - "tv", - "ad", - "etrain", - "subway", - "delivery", - "route" - ], - "ename": "moscow", - "bounds": [ - [ - 37.0402925, - 55.31141404514547 - ], - [ - 38.2047155, - 56.190068045145466 - ] - ], - "names": { - "ablative": "", - "accusative": "Москву", - "dative": "Москве", - "directional": "", - "genitive": "Москвы", - "instrumental": "Москвой", - "locative": "", - "nominative": "Москва", - "preposition": "в", - "prepositional": "Москве" - }, - "parent": { - "id": 1, - "type": 5, - "parent_id": 3, - "capital_id": 213, - "geo_parent_id": 0, - "city_id": 213, - "name": "moscow-and-moscow-oblast", - "native_name": "", - "iso_name": "RU-MOS", - "is_main": true, - "en_name": "Moscow and Moscow Oblast", - "short_en_name": "RU-MOS", - "phone_code": "495 496 498 499", - "phone_code_old": "", - "zip_code": "", - "population": 7503385, - "synonyms": "Московская область, Подмосковье, Podmoskovye", - "latitude": 55.815792, - "longitude": 37.380031, - "latitude_size": 2.705659, - "longitude_size": 5.060749, - "zoom": 8, - "tzname": "Europe/Moscow", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [ - 213, - 10716, - 10747, - 10758, - 20728, - 10740, - 10738, - 20523, - 10735, - 10734, - 10743, - 21622 - ], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "ad" - ], - "ename": "moscow-and-moscow-oblast", - "bounds": [ - [ - 34.8496565, - 54.439456064325434 - ], - [ - 39.9104055, - 57.14511506432543 - ] - ], - "names": { - "ablative": "", - "accusative": "Москву и Московскую область", - "dative": "Москве и Московской области", - "directional": "", - "genitive": "Москвы и Московской области", - "instrumental": "Москвой и Московской областью", - "locative": "", - "nominative": "Москва и Московская область", - "preposition": "в", - "prepositional": "Москве и Московской области" - }, - "parent": { - "id": 225, - "type": 3, - "parent_id": 10001, - "capital_id": 213, - "geo_parent_id": 0, - "city_id": 213, - "name": "russia", - "native_name": "", - "iso_name": "RU", - "is_main": false, - "en_name": "Russia", - "short_en_name": "RU", - "phone_code": "7", - "phone_code_old": "", - "zip_code": "", - "population": 146880432, - "synonyms": "Russian Federation,Российская Федерация", - "latitude": 61.698653, - "longitude": 99.505405, - "latitude_size": 40.700127, - "longitude_size": 171.643239, - "zoom": 3, - "tzname": "", - "official_languages": "ru", - "widespread_languages": "ru", - "suggest_list": [ - 213, - 2, - 65, - 54, - 47, - 43, - 66, - 51, - 56, - 172, - 39, - 62 - ], - "is_eu": false, - "services_names": [ - "bs", - "yaca", - "ad" - ], - "ename": "russia", - "bounds": [ - [ - 13.683785499999999, - 35.290400699917846 - ], - [ - -174.6729755, - 75.99052769991785 - ] - ], - "names": { - "ablative": "", - "accusative": "Россию", - "dative": "России", - "directional": "", - "genitive": "России", - "instrumental": "Россией", - "locative": "", - "nominative": "Россия", - "preposition": "в", - "prepositional": "России" - } - } - } - }, - "Transport": [ - { - "lineId": "2036925416", - "name": "194", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036927196", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9648742", - "name": "Коровино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659860", - "tzOffset": 10800, - "text": "21:51" - } - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661840", - "tzOffset": 10800, - "text": "22:24" - } - } - ], - "departureTime": "21:51" - } - } - ], - "threadId": "2036927196", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9648742", - "name": "Коровино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659860", - "tzOffset": 10800, - "text": "21:51" - } - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661840", - "tzOffset": 10800, - "text": "22:24" - } - } - ], - "departureTime": "21:51" - } - }, - { - "lineId": "213_114_bus_mosgortrans", - "name": "114", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_114_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568603405", - "tzOffset": 10800, - "text": "6:10" - }, - "end": { - "value": "1568672165", - "tzOffset": 10800, - "text": "1:16" - } - } - } - } - ], - "threadId": "213B_114_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568603405", - "tzOffset": 10800, - "text": "6:10" - }, - "end": { - "value": "1568672165", - "tzOffset": 10800, - "text": "1:16" - } - } - } - }, - { - "lineId": "213_154_bus_mosgortrans", - "name": "154", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_154_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642548", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659260", - "tzOffset": 10800, - "text": "21:41" - }, - "Estimated": { - "value": "1568659252", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1054764%5F191500" - }, - { - "Scheduled": { - "value": "1568660580", - "tzOffset": 10800, - "text": "22:03" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:41" - } - } - ], - "threadId": "213B_154_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642548", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659260", - "tzOffset": 10800, - "text": "21:41" - }, - "Estimated": { - "value": "1568659252", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1054764%5F191500" - }, - { - "Scheduled": { - "value": "1568660580", - "tzOffset": 10800, - "text": "22:03" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:41" - } - }, - { - "lineId": "213_179_bus_mosgortrans", - "name": "179", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_179_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568659351", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|59832%5F31359" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661660", - "tzOffset": 10800, - "text": "22:21" - } - } - ], - "departureTime": "21:52" - } - } - ], - "threadId": "213B_179_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568659351", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|59832%5F31359" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661660", - "tzOffset": 10800, - "text": "22:21" - } - } - ], - "departureTime": "21:52" - } - }, - { - "lineId": "213_191m_minibus_default", - "name": "591", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_191m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660525", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|38278%5F9345312" - } - ], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568602033", - "tzOffset": 10800, - "text": "5:47" - }, - "end": { - "value": "1568672233", - "tzOffset": 10800, - "text": "1:17" - } - } - } - } - ], - "threadId": "213A_191m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9647199", - "name": "Метро Войковская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660525", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|38278%5F9345312" - } - ], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568602033", - "tzOffset": 10800, - "text": "5:47" - }, - "end": { - "value": "1568672233", - "tzOffset": 10800, - "text": "1:17" - } - } - } - }, - { - "lineId": "213_206m_minibus_default", - "name": "206к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_206m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568601239", - "tzOffset": 10800, - "text": "5:33" - }, - "end": { - "value": "1568671439", - "tzOffset": 10800, - "text": "1:03" - } - } - } - } - ], - "threadId": "213A_206m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568601239", - "tzOffset": 10800, - "text": "5:33" - }, - "end": { - "value": "1568671439", - "tzOffset": 10800, - "text": "1:03" - } - } - } - }, - { - "lineId": "213_215_bus_mosgortrans", - "name": "215", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_215_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "27 мин", - "value": 1620, - "begin": { - "value": "1568601276", - "tzOffset": 10800, - "text": "5:34" - }, - "end": { - "value": "1568671476", - "tzOffset": 10800, - "text": "1:04" - } - } - } - } - ], - "threadId": "213B_215_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9711780", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9711744", - "name": "Станция Ховрино" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "27 мин", - "value": 1620, - "begin": { - "value": "1568601276", - "tzOffset": 10800, - "text": "5:34" - }, - "end": { - "value": "1568671476", - "tzOffset": 10800, - "text": "1:04" - } - } - } - }, - { - "lineId": "213_282_bus_mosgortrans", - "name": "282", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_282_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9641102", - "name": "Улица Корнейчука" - }, - { - "id": "2532226085", - "name": "Метро Войковская" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659888", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|34874%5F9345408" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568602180", - "tzOffset": 10800, - "text": "5:49" - }, - "end": { - "value": "1568673460", - "tzOffset": 10800, - "text": "1:37" - } - } - } - } - ], - "threadId": "213A_282_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9641102", - "name": "Улица Корнейчука" - }, - { - "id": "2532226085", - "name": "Метро Войковская" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659888", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|34874%5F9345408" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568602180", - "tzOffset": 10800, - "text": "5:49" - }, - "end": { - "value": "1568673460", - "tzOffset": 10800, - "text": "1:37" - } - } - } - }, - { - "lineId": "213_294m_minibus_default", - "name": "994", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_294m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9649459", - "name": "Метро Алтуфьево" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "30 мин", - "value": 1800, - "begin": { - "value": "1568601527", - "tzOffset": 10800, - "text": "5:38" - }, - "end": { - "value": "1568671727", - "tzOffset": 10800, - "text": "1:08" - } - } - } - } - ], - "threadId": "213A_294m_minibus_default", - "EssentialStops": [ - { - "id": "stop__9640756", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9649459", - "name": "Метро Алтуфьево" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "30 мин", - "value": 1800, - "begin": { - "value": "1568601527", - "tzOffset": 10800, - "text": "5:38" - }, - "end": { - "value": "1568671727", - "tzOffset": 10800, - "text": "1:08" - } - } - } - }, - { - "lineId": "213_36_trolleybus_mosgortrans", - "name": "т36", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_36_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642550", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9640641", - "name": "Дмитровское шоссе, 155" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - }, - "Estimated": { - "value": "1568659426", - "tzOffset": 10800, - "text": "21:43" - }, - "vehicleId": "codd%5Fnew|1084829%5F430260" - }, - { - "Scheduled": { - "value": "1568660520", - "tzOffset": 10800, - "text": "22:02" - }, - "Estimated": { - "value": "1568659656", - "tzOffset": 10800, - "text": "21:47" - }, - "vehicleId": "codd%5Fnew|1117016%5F430280" - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - }, - "Estimated": { - "value": "1568660538", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|1054576%5F430226" - } - ], - "departureTime": "21:48" - } - } - ], - "threadId": "213A_36_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9642550", - "name": "ВДНХ (южная)" - }, - { - "id": "stop__9640641", - "name": "Дмитровское шоссе, 155" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - }, - "Estimated": { - "value": "1568659426", - "tzOffset": 10800, - "text": "21:43" - }, - "vehicleId": "codd%5Fnew|1084829%5F430260" - }, - { - "Scheduled": { - "value": "1568660520", - "tzOffset": 10800, - "text": "22:02" - }, - "Estimated": { - "value": "1568659656", - "tzOffset": 10800, - "text": "21:47" - }, - "vehicleId": "codd%5Fnew|1117016%5F430280" - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - }, - "Estimated": { - "value": "1568660538", - "tzOffset": 10800, - "text": "22:02" - }, - "vehicleId": "codd%5Fnew|1054576%5F430226" - } - ], - "departureTime": "21:48" - } - }, - { - "lineId": "213_47_trolleybus_mosgortrans", - "name": "т47", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_47_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639568", - "name": "Бескудниковский переулок" - }, - { - "id": "stop__9641903", - "name": "Бескудниковский переулок" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - }, - "Estimated": { - "value": "1568659253", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1112219%5F430329" - }, - { - "Scheduled": { - "value": "1568660940", - "tzOffset": 10800, - "text": "22:09" - }, - "Estimated": { - "value": "1568660519", - "tzOffset": 10800, - "text": "22:01" - }, - "vehicleId": "codd%5Fnew|1139620%5F430382" - }, - { - "Scheduled": { - "value": "1568663580", - "tzOffset": 10800, - "text": "22:53" - } - } - ], - "departureTime": "21:53" - } - } - ], - "threadId": "213B_47_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639568", - "name": "Бескудниковский переулок" - }, - { - "id": "stop__9641903", - "name": "Бескудниковский переулок" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - }, - "Estimated": { - "value": "1568659253", - "tzOffset": 10800, - "text": "21:40" - }, - "vehicleId": "codd%5Fnew|1112219%5F430329" - }, - { - "Scheduled": { - "value": "1568660940", - "tzOffset": 10800, - "text": "22:09" - }, - "Estimated": { - "value": "1568660519", - "tzOffset": 10800, - "text": "22:01" - }, - "vehicleId": "codd%5Fnew|1139620%5F430382" - }, - { - "Scheduled": { - "value": "1568663580", - "tzOffset": 10800, - "text": "22:53" - } - } - ], - "departureTime": "21:53" - } - }, - { - "lineId": "213_56_trolleybus_mosgortrans", - "name": "т56", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_56_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639561", - "name": "Коровинское шоссе" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660675", - "tzOffset": 10800, - "text": "22:04" - }, - "vehicleId": "codd%5Fnew|146304%5F31207" - } - ], - "Frequency": { - "text": "8 мин", - "value": 480, - "begin": { - "value": "1568606244", - "tzOffset": 10800, - "text": "6:57" - }, - "end": { - "value": "1568670144", - "tzOffset": 10800, - "text": "0:42" - } - } - } - } - ], - "threadId": "213A_56_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639561", - "name": "Коровинское шоссе" - }, - { - "id": "stop__9639588", - "name": "Коровинское шоссе" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568660675", - "tzOffset": 10800, - "text": "22:04" - }, - "vehicleId": "codd%5Fnew|146304%5F31207" - } - ], - "Frequency": { - "text": "8 мин", - "value": 480, - "begin": { - "value": "1568606244", - "tzOffset": 10800, - "text": "6:57" - }, - "end": { - "value": "1568670144", - "tzOffset": 10800, - "text": "0:42" - } - } - } - }, - { - "lineId": "213_63_bus_mosgortrans", - "name": "63", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_63_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|38921%5F9215306" - }, - { - "Estimated": { - "value": "1568660136", - "tzOffset": 10800, - "text": "21:55" - }, - "vehicleId": "codd%5Fnew|38918%5F9215303" - } - ], - "Frequency": { - "text": "17 мин", - "value": 1020, - "begin": { - "value": "1568600987", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568670227", - "tzOffset": 10800, - "text": "0:43" - } - } - } - } - ], - "threadId": "213A_63_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|38921%5F9215306" - }, - { - "Estimated": { - "value": "1568660136", - "tzOffset": 10800, - "text": "21:55" - }, - "vehicleId": "codd%5Fnew|38918%5F9215303" - } - ], - "Frequency": { - "text": "17 мин", - "value": 1020, - "begin": { - "value": "1568600987", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568670227", - "tzOffset": 10800, - "text": "0:43" - } - } - } - }, - { - "lineId": "213_677_bus_mosgortrans", - "name": "677", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213B_677_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639495", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|11731%5F31376" - } - ], - "Frequency": { - "text": "4 мин", - "value": 240, - "begin": { - "value": "1568600940", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568672640", - "tzOffset": 10800, - "text": "1:24" - } - } - } - } - ], - "threadId": "213B_677_bus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9639495", - "name": "Метро Петровско-Разумовская" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659369", - "tzOffset": 10800, - "text": "21:42" - }, - "vehicleId": "codd%5Fnew|11731%5F31376" - } - ], - "Frequency": { - "text": "4 мин", - "value": 240, - "begin": { - "value": "1568600940", - "tzOffset": 10800, - "text": "5:29" - }, - "end": { - "value": "1568672640", - "tzOffset": 10800, - "text": "1:24" - } - } - } - }, - { - "lineId": "213_692_bus_mosgortrans", - "name": "692", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036928706", - "EssentialStops": [ - { - "id": "3163417967", - "name": "Платформа Дегунино" - }, - { - "id": "3163417967", - "name": "Платформа Дегунино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568660280", - "tzOffset": 10800, - "text": "21:58" - }, - "Estimated": { - "value": "1568660255", - "tzOffset": 10800, - "text": "21:57" - }, - "vehicleId": "codd%5Fnew|63029%5F31485" - }, - { - "Scheduled": { - "value": "1568693340", - "tzOffset": 10800, - "text": "7:09" - } - }, - { - "Scheduled": { - "value": "1568696940", - "tzOffset": 10800, - "text": "8:09" - } - } - ], - "departureTime": "21:58" - } - } - ], - "threadId": "2036928706", - "EssentialStops": [ - { - "id": "3163417967", - "name": "Платформа Дегунино" - }, - { - "id": "3163417967", - "name": "Платформа Дегунино" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568660280", - "tzOffset": 10800, - "text": "21:58" - }, - "Estimated": { - "value": "1568660255", - "tzOffset": 10800, - "text": "21:57" - }, - "vehicleId": "codd%5Fnew|63029%5F31485" - }, - { - "Scheduled": { - "value": "1568693340", - "tzOffset": 10800, - "text": "7:09" - } - }, - { - "Scheduled": { - "value": "1568696940", - "tzOffset": 10800, - "text": "8:09" - } - } - ], - "departureTime": "21:58" - } - }, - { - "lineId": "213_78_trolleybus_mosgortrans", - "name": "т78", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "213A_78_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9887464", - "name": "9-я Северная линия" - }, - { - "id": "stop__9887464", - "name": "9-я Северная линия" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659620", - "tzOffset": 10800, - "text": "21:47" - }, - "Estimated": { - "value": "1568659898", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|147522%5F31184" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:47" - } - } - ], - "threadId": "213A_78_trolleybus_mosgortrans", - "EssentialStops": [ - { - "id": "stop__9887464", - "name": "9-я Северная линия" - }, - { - "id": "stop__9887464", - "name": "9-я Северная линия" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659620", - "tzOffset": 10800, - "text": "21:47" - }, - "Estimated": { - "value": "1568659898", - "tzOffset": 10800, - "text": "21:51" - }, - "vehicleId": "codd%5Fnew|147522%5F31184" - }, - { - "Scheduled": { - "value": "1568660760", - "tzOffset": 10800, - "text": "22:06" - } - }, - { - "Scheduled": { - "value": "1568661900", - "tzOffset": 10800, - "text": "22:25" - } - } - ], - "departureTime": "21:47" - } - }, - { - "lineId": "213_82_bus_mosgortrans", - "name": "82", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036925244", - "EssentialStops": [ - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - }, - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - } - }, - { - "Scheduled": { - "value": "1568661780", - "tzOffset": 10800, - "text": "22:23" - } - }, - { - "Scheduled": { - "value": "1568663760", - "tzOffset": 10800, - "text": "22:56" - } - } - ], - "departureTime": "21:48" - } - } - ], - "threadId": "2036925244", - "EssentialStops": [ - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - }, - { - "id": "2310890052", - "name": "Метро Верхние Лихоборы" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659680", - "tzOffset": 10800, - "text": "21:48" - } - }, - { - "Scheduled": { - "value": "1568661780", - "tzOffset": 10800, - "text": "22:23" - } - }, - { - "Scheduled": { - "value": "1568663760", - "tzOffset": 10800, - "text": "22:56" - } - } - ], - "departureTime": "21:48" - } - }, - { - "lineId": "2465131598", - "name": "179к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2465131758", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659500", - "tzOffset": 10800, - "text": "21:45" - } - }, - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - } - }, - { - "Scheduled": { - "value": "1568660880", - "tzOffset": 10800, - "text": "22:08" - } - } - ], - "departureTime": "21:45" - } - } - ], - "threadId": "2465131758", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659500", - "tzOffset": 10800, - "text": "21:45" - } - }, - { - "Scheduled": { - "value": "1568659980", - "tzOffset": 10800, - "text": "21:53" - } - }, - { - "Scheduled": { - "value": "1568660880", - "tzOffset": 10800, - "text": "22:08" - } - } - ], - "departureTime": "21:45" - } - }, - { - "lineId": "466_bus_default", - "name": "466", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "466B_bus_default", - "EssentialStops": [ - { - "id": "stop__9640546", - "name": "Станция Бескудниково" - }, - { - "id": "stop__9640545", - "name": "Станция Бескудниково" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568604647", - "tzOffset": 10800, - "text": "6:30" - }, - "end": { - "value": "1568675447", - "tzOffset": 10800, - "text": "2:10" - } - } - } - } - ], - "threadId": "466B_bus_default", - "EssentialStops": [ - { - "id": "stop__9640546", - "name": "Станция Бескудниково" - }, - { - "id": "stop__9640545", - "name": "Станция Бескудниково" - } - ], - "BriefSchedule": { - "Events": [], - "Frequency": { - "text": "22 мин", - "value": 1320, - "begin": { - "value": "1568604647", - "tzOffset": 10800, - "text": "6:30" - }, - "end": { - "value": "1568675447", - "tzOffset": 10800, - "text": "2:10" - } - } - } - }, - { - "lineId": "677k_bus_default", - "name": "677к", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "677kA_bus_default", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568660003", - "tzOffset": 10800, - "text": "21:53" - }, - "vehicleId": "codd%5Fnew|130308%5F31319" - }, - { - "Scheduled": { - "value": "1568661240", - "tzOffset": 10800, - "text": "22:14" - } - }, - { - "Scheduled": { - "value": "1568662500", - "tzOffset": 10800, - "text": "22:35" - } - } - ], - "departureTime": "21:52" - } - } - ], - "threadId": "677kA_bus_default", - "EssentialStops": [ - { - "id": "stop__9640244", - "name": "Платформа Лианозово" - }, - { - "id": "stop__9639480", - "name": "Платформа Лианозово" - } - ], - "BriefSchedule": { - "Events": [ - { - "Scheduled": { - "value": "1568659920", - "tzOffset": 10800, - "text": "21:52" - }, - "Estimated": { - "value": "1568660003", - "tzOffset": 10800, - "text": "21:53" - }, - "vehicleId": "codd%5Fnew|130308%5F31319" - }, - { - "Scheduled": { - "value": "1568661240", - "tzOffset": 10800, - "text": "22:14" - } - }, - { - "Scheduled": { - "value": "1568662500", - "tzOffset": 10800, - "text": "22:35" - } - } - ], - "departureTime": "21:52" - } - }, - { - "lineId": "m10_bus_default", - "name": "м10", - "Types": [ - "bus" - ], - "type": "bus", - "threads": [ - { - "threadId": "2036926048", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659718", - "tzOffset": 10800, - "text": "21:48" - }, - "vehicleId": "codd%5Fnew|146260%5F31212" - }, - { - "Estimated": { - "value": "1568660422", - "tzOffset": 10800, - "text": "22:00" - }, - "vehicleId": "codd%5Fnew|13997%5F31247" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568606903", - "tzOffset": 10800, - "text": "7:08" - }, - "end": { - "value": "1568675183", - "tzOffset": 10800, - "text": "2:06" - } - } - } - } - ], - "threadId": "2036926048", - "EssentialStops": [ - { - "id": "stop__9640554", - "name": "Лобненская улица" - }, - { - "id": "stop__9640553", - "name": "Лобненская улица" - } - ], - "BriefSchedule": { - "Events": [ - { - "Estimated": { - "value": "1568659718", - "tzOffset": 10800, - "text": "21:48" - }, - "vehicleId": "codd%5Fnew|146260%5F31212" - }, - { - "Estimated": { - "value": "1568660422", - "tzOffset": 10800, - "text": "22:00" - }, - "vehicleId": "codd%5Fnew|13997%5F31247" - } - ], - "Frequency": { - "text": "15 мин", - "value": 900, - "begin": { - "value": "1568606903", - "tzOffset": 10800, - "text": "7:08" - }, - "end": { - "value": "1568675183", - "tzOffset": 10800, - "text": "2:06" - } - } - } - } + ], + "names": { + "ablative": "", + "accusative": "Москву", + "dative": "Москве", + "directional": "", + "genitive": "Москвы", + "instrumental": "Москвой", + "locative": "", + "nominative": "Москва", + "preposition": "в", + "prepositional": "Москве" + }, + "parent": { + "id": 1, + "type": 5, + "parent_id": 3, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "moscow-and-moscow-oblast", + "native_name": "", + "iso_name": "RU-MOS", + "is_main": true, + "en_name": "Moscow and Moscow Oblast", + "short_en_name": "RU-MOS", + "phone_code": "495 496 498 499", + "phone_code_old": "", + "zip_code": "", + "population": 7503385, + "synonyms": "Московская область, Подмосковье, Podmoskovye", + "latitude": 55.815792, + "longitude": 37.380031, + "latitude_size": 2.705659, + "longitude_size": 5.060749, + "zoom": 8, + "tzname": "Europe/Moscow", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 10716, + 10747, + 10758, + 20728, + 10740, + 10738, + 20523, + 10735, + 10734, + 10743, + 21622 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "moscow-and-moscow-oblast", + "bounds": [ + [ + 34.8496565, + 54.439456064325434 + ], + [ + 39.9104055, + 57.14511506432543 + ] + ], + "names": { + "ablative": "", + "accusative": "Москву и Московскую область", + "dative": "Москве и Московской области", + "directional": "", + "genitive": "Москвы и Московской области", + "instrumental": "Москвой и Московской областью", + "locative": "", + "nominative": "Москва и Московская область", + "preposition": "в", + "prepositional": "Москве и Московской области" + }, + "parent": { + "id": 225, + "type": 3, + "parent_id": 10001, + "capital_id": 213, + "geo_parent_id": 0, + "city_id": 213, + "name": "russia", + "native_name": "", + "iso_name": "RU", + "is_main": false, + "en_name": "Russia", + "short_en_name": "RU", + "phone_code": "7", + "phone_code_old": "", + "zip_code": "", + "population": 146880432, + "synonyms": "Russian Federation,Российская Федерация", + "latitude": 61.698653, + "longitude": 99.505405, + "latitude_size": 40.700127, + "longitude_size": 171.643239, + "zoom": 3, + "tzname": "", + "official_languages": "ru", + "widespread_languages": "ru", + "suggest_list": [ + 213, + 2, + 65, + 54, + 47, + 43, + 66, + 51, + 56, + 172, + 39, + 62 + ], + "is_eu": false, + "services_names": [ + "bs", + "yaca", + "ad" + ], + "ename": "russia", + "bounds": [ + [ + 13.683785499999999, + 35.290400699917846 + ], + [ + -174.6729755, + 75.99052769991785 ] + ], + "names": { + "ablative": "", + "accusative": "Россию", + "dative": "России", + "directional": "", + "genitive": "России", + "instrumental": "Россией", + "locative": "", + "nominative": "Россия", + "preposition": "в", + "prepositional": "России" + } } + } }, - "toponymSeoname": "dmitrovskoye_shosse" - } -} + "Transport": [ + { + "lineId": "2036924720", + "name": "692", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036928706", + "EssentialStops": [ + { + "id": "3163417967", + "name": "Платформа Дегунино" + }, + { + "id": "3163417967", + "name": "Платформа Дегунино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570973441", + "tzOffset": 10800, + "text": "16:30" + }, + "vehicleId": "codd%5Fnew|144020%5F31402" + } + ], + "Frequency": { + "text": "1 ч", + "value": 3600, + "begin": { + "value": "1570938428", + "tzOffset": 10800, + "text": "6:47" + }, + "end": { + "value": "1570990628", + "tzOffset": 10800, + "text": "21:17" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036924720&ll=37.577436%2C55.828981&name=692&r=4037&type=bus", + "seoname": "692" + }, + { + "lineId": "2036924968", + "name": "82", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036925244", + "EssentialStops": [ + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "34 мин", + "value": 2040, + "begin": { + "value": "1570944072", + "tzOffset": 10800, + "text": "8:21" + }, + "end": { + "value": "1570997592", + "tzOffset": 10800, + "text": "23:13" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036924968&ll=37.571504%2C55.816622&name=82&r=4164&type=bus", + "seoname": "82" + }, + { + "lineId": "2036925416", + "name": "194", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036927196", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9648742", + "name": "Коровино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "12 мин", + "value": 720, + "begin": { + "value": "1570933976", + "tzOffset": 10800, + "text": "5:32" + }, + "end": { + "value": "1571004356", + "tzOffset": 10800, + "text": "1:05" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036925416&ll=37.544800%2C55.865286&name=194&r=3667&type=bus", + "seoname": "194" + }, + { + "lineId": "2036925728", + "name": "282", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_282_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9641102", + "name": "Улица Корнейчука" + }, + { + "id": "2532226085", + "name": "Метро Войковская" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570971861", + "tzOffset": 10800, + "text": "16:04" + }, + "vehicleId": "codd%5Fnew|34854%5F9345401" + }, + { + "Estimated": { + "value": "1570973231", + "tzOffset": 10800, + "text": "16:27" + }, + "vehicleId": "codd%5Fnew|37913%5F9225419" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570934963", + "tzOffset": 10800, + "text": "5:49" + }, + "end": { + "value": "1571005163", + "tzOffset": 10800, + "text": "1:19" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036925728&ll=37.553526%2C55.860385&name=282&r=5779&type=bus", + "seoname": "282" + }, + { + "lineId": "2036926781", + "name": "154", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_154_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642548", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972424", + "tzOffset": 10800, + "text": "16:13" + }, + "vehicleId": "codd%5Fnew|1161539%5F191543" + }, + { + "Estimated": { + "value": "1570973620", + "tzOffset": 10800, + "text": "16:33" + }, + "vehicleId": "codd%5Fnew|58773%5F190599" + } + ], + "Frequency": { + "text": "20 мин", + "value": 1200, + "begin": { + "value": "1570938166", + "tzOffset": 10800, + "text": "6:42" + }, + "end": { + "value": "1571006446", + "tzOffset": 10800, + "text": "1:40" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036926781&ll=37.576158%2C55.846301&name=154&r=4917&type=bus", + "seoname": "154" + }, + { + "lineId": "2036926818", + "name": "994", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_294m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9649459", + "name": "Метро Алтуфьево" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "30 мин", + "value": 1800, + "begin": { + "value": "1570934327", + "tzOffset": 10800, + "text": "5:38" + }, + "end": { + "value": "1571004527", + "tzOffset": 10800, + "text": "1:08" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036926818&ll=37.560060%2C55.868431&name=994&r=3637&type=bus", + "seoname": "994" + }, + { + "lineId": "2036926890", + "name": "466", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "466B_bus_default", + "EssentialStops": [ + { + "id": "stop__9640546", + "name": "Станция Бескудниково" + }, + { + "id": "stop__9640545", + "name": "Станция Бескудниково" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570937447", + "tzOffset": 10800, + "text": "6:30" + }, + "end": { + "value": "1571008247", + "tzOffset": 10800, + "text": "2:10" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2036926890&ll=37.564238%2C55.845050&name=466&r=4163&type=bus", + "seoname": "466" + }, + { + "lineId": "213_114_bus_mosgortrans", + "name": "114", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_114_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972913", + "tzOffset": 10800, + "text": "16:21" + }, + "vehicleId": "codd%5Fnew|1092230%5F191422" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570936205", + "tzOffset": 10800, + "text": "6:10" + }, + "end": { + "value": "1571004965", + "tzOffset": 10800, + "text": "1:16" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_114_bus_mosgortrans&ll=37.508487%2C55.852137&name=114&r=3544&type=bus", + "seoname": "114" + }, + { + "lineId": "213_179_bus_mosgortrans", + "name": "179", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_179_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570971963", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|194519%5F31367" + }, + { + "Estimated": { + "value": "1570973105", + "tzOffset": 10800, + "text": "16:25" + }, + "vehicleId": "codd%5Fnew|56358%5F31365" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570936823", + "tzOffset": 10800, + "text": "6:20" + }, + "end": { + "value": "1571005583", + "tzOffset": 10800, + "text": "1:26" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_179_bus_mosgortrans&ll=37.526151%2C55.858031&name=179&r=4634&type=bus", + "seoname": "179" + }, + { + "lineId": "213_191m_minibus_default", + "name": "591", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_191m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9647199", + "name": "Метро Войковская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972150", + "tzOffset": 10800, + "text": "16:09" + }, + "vehicleId": "codd%5Fnew|35595%5F9345307" + } + ], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570934833", + "tzOffset": 10800, + "text": "5:47" + }, + "end": { + "value": "1571005033", + "tzOffset": 10800, + "text": "1:17" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_191m_minibus_default&ll=37.510906%2C55.848214&name=591&r=3384&type=bus", + "seoname": "591" + }, + { + "lineId": "213_206m_minibus_default", + "name": "206к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_206m_minibus_default", + "EssentialStops": [ + { + "id": "stop__9640756", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "22 мин", + "value": 1320, + "begin": { + "value": "1570934039", + "tzOffset": 10800, + "text": "5:33" + }, + "end": { + "value": "1571004239", + "tzOffset": 10800, + "text": "1:03" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_206m_minibus_default&ll=37.548997%2C55.864997&name=206%D0%BA&r=3515&type=bus", + "seoname": "206k" + }, + { + "lineId": "213_215_bus_mosgortrans", + "name": "215", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_215_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9711780", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9711744", + "name": "Станция Ховрино" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "27 мин", + "value": 1620, + "begin": { + "value": "1570934076", + "tzOffset": 10800, + "text": "5:34" + }, + "end": { + "value": "1571004276", + "tzOffset": 10800, + "text": "1:04" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_215_bus_mosgortrans&ll=37.543701%2C55.854527&name=215&r=2763&type=bus", + "seoname": "215" + }, + { + "lineId": "213_36_trolleybus_mosgortrans", + "name": "т36", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_36_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9642550", + "name": "ВДНХ (южная)" + }, + { + "id": "stop__9640641", + "name": "Дмитровское шоссе, 155" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972236", + "tzOffset": 10800, + "text": "16:10" + }, + "vehicleId": "codd%5Fnew|1084830%5F430261" + }, + { + "Estimated": { + "value": "1570972641", + "tzOffset": 10800, + "text": "16:17" + }, + "vehicleId": "codd%5Fnew|1084829%5F430260" + }, + { + "Estimated": { + "value": "1570973178", + "tzOffset": 10800, + "text": "16:26" + }, + "vehicleId": "codd%5Fnew|1084827%5F430255" + } + ], + "Frequency": { + "text": "12 мин", + "value": 720, + "begin": { + "value": "1570932741", + "tzOffset": 10800, + "text": "5:12" + }, + "end": { + "value": "1571003121", + "tzOffset": 10800, + "text": "0:45" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_36_trolleybus_mosgortrans&ll=37.588604%2C55.859705&name=%D1%8236&r=5104&type=bus", + "seoname": "t36" + }, + { + "lineId": "213_47_trolleybus_mosgortrans", + "name": "т47", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_47_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639568", + "name": "Бескудниковский переулок" + }, + { + "id": "stop__9641903", + "name": "Бескудниковский переулок" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1570972080", + "tzOffset": 10800, + "text": "16:08" + }, + "Estimated": { + "value": "1570972183", + "tzOffset": 10800, + "text": "16:09" + }, + "vehicleId": "codd%5Fnew|1132404%5F430361" + }, + { + "Scheduled": { + "value": "1570972980", + "tzOffset": 10800, + "text": "16:23" + }, + "Estimated": { + "value": "1570972219", + "tzOffset": 10800, + "text": "16:10" + }, + "vehicleId": "codd%5Fnew|1136132%5F430358" + }, + { + "Scheduled": { + "value": "1570973940", + "tzOffset": 10800, + "text": "16:39" + } + } + ], + "departureTime": "16:08" + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_47_trolleybus_mosgortrans&ll=37.588308%2C55.818685&name=%D1%8247&r=5359&type=bus", + "seoname": "t47" + }, + { + "lineId": "213_56_trolleybus_mosgortrans", + "name": "т56", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_56_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639561", + "name": "Коровинское шоссе" + }, + { + "id": "stop__9639588", + "name": "Коровинское шоссе" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1570971900", + "tzOffset": 10800, + "text": "16:05" + }, + "Estimated": { + "value": "1570972560", + "tzOffset": 10800, + "text": "16:16" + }, + "vehicleId": "codd%5Fnew|1117148%5F430351" + }, + { + "Scheduled": { + "value": "1570972680", + "tzOffset": 10800, + "text": "16:18" + }, + "Estimated": { + "value": "1570973442", + "tzOffset": 10800, + "text": "16:30" + }, + "vehicleId": "codd%5Fnew|1080552%5F430302" + }, + { + "Scheduled": { + "value": "1570973400", + "tzOffset": 10800, + "text": "16:30" + } + } + ], + "departureTime": "16:05" + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_56_trolleybus_mosgortrans&ll=37.551454%2C55.830147&name=%D1%8256&r=6304&type=bus", + "seoname": "t56" + }, + { + "lineId": "213_63_bus_mosgortrans", + "name": "63", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_63_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972434", + "tzOffset": 10800, + "text": "16:13" + }, + "vehicleId": "codd%5Fnew|38700%5F9215301" + } + ], + "Frequency": { + "text": "17 мин", + "value": 1020, + "begin": { + "value": "1570934207", + "tzOffset": 10800, + "text": "5:36" + }, + "end": { + "value": "1571003507", + "tzOffset": 10800, + "text": "0:51" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_63_bus_mosgortrans&ll=37.550792%2C55.872690&name=63&r=3057&type=bus", + "seoname": "63" + }, + { + "lineId": "213_677_bus_mosgortrans", + "name": "677", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213B_677_bus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9639495", + "name": "Метро Петровско-Разумовская" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1570972200", + "tzOffset": 10800, + "text": "16:10" + }, + "Estimated": { + "value": "1570971838", + "tzOffset": 10800, + "text": "16:03" + }, + "vehicleId": "codd%5Fnew|58581%5F31321" + }, + { + "Scheduled": { + "value": "1570972560", + "tzOffset": 10800, + "text": "16:16" + } + }, + { + "Scheduled": { + "value": "1570972920", + "tzOffset": 10800, + "text": "16:22" + } + } + ], + "departureTime": "16:10" + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_677_bus_mosgortrans&ll=37.564191%2C55.866620&name=677&r=3386&type=bus", + "seoname": "677" + }, + { + "lineId": "213_78_trolleybus_mosgortrans", + "name": "т78", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "213A_78_trolleybus_mosgortrans", + "EssentialStops": [ + { + "id": "stop__9887464", + "name": "9-я Северная линия" + }, + { + "id": "stop__9887464", + "name": "9-я Северная линия" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570971984", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|59694%5F31155" + }, + { + "Estimated": { + "value": "1570972003", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|55041%5F31116" + }, + { + "Estimated": { + "value": "1570972550", + "tzOffset": 10800, + "text": "16:15" + }, + "vehicleId": "codd%5Fnew|62710%5F31142" + }, + { + "Estimated": { + "value": "1570973307", + "tzOffset": 10800, + "text": "16:28" + }, + "vehicleId": "codd%5Fnew|1037437%5F31144" + }, + { + "Estimated": { + "value": "1570973456", + "tzOffset": 10800, + "text": "16:30" + }, + "vehicleId": "codd%5Fnew|318517%5F31136" + } + ], + "Frequency": { + "text": "11 мин", + "value": 660, + "begin": { + "value": "1570937045", + "tzOffset": 10800, + "text": "6:24" + }, + "end": { + "value": "1571002385", + "tzOffset": 10800, + "text": "0:33" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=213_78_trolleybus_mosgortrans&ll=37.569453%2C55.855402&name=%D1%8278&r=8810&type=bus", + "seoname": "t78" + }, + { + "lineId": "2465131598", + "name": "179к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2465131758", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570935230", + "tzOffset": 10800, + "text": "5:53" + }, + "end": { + "value": "1571003030", + "tzOffset": 10800, + "text": "0:43" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=2465131598&ll=37.561423%2C55.871807&name=179%D0%BA&r=2787&type=bus", + "seoname": "179k" + }, + { + "lineId": "677k_bus_default", + "name": "677к", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "677kA_bus_default", + "EssentialStops": [ + { + "id": "stop__9640244", + "name": "Платформа Лианозово" + }, + { + "id": "stop__9639480", + "name": "Платформа Лианозово" + } + ], + "BriefSchedule": { + "Events": [ + { + "Scheduled": { + "value": "1570972560", + "tzOffset": 10800, + "text": "16:16" + }, + "Estimated": { + "value": "1570971986", + "tzOffset": 10800, + "text": "16:06" + }, + "vehicleId": "codd%5Fnew|1038096%5F31398" + }, + { + "Scheduled": { + "value": "1570973280", + "tzOffset": 10800, + "text": "16:28" + }, + "Estimated": { + "value": "1570972342", + "tzOffset": 10800, + "text": "16:12" + }, + "vehicleId": "codd%5Fnew|58590%5F31348" + }, + { + "Scheduled": { + "value": "1570974000", + "tzOffset": 10800, + "text": "16:40" + }, + "Estimated": { + "value": "1570973387", + "tzOffset": 10800, + "text": "16:29" + }, + "vehicleId": "codd%5Fnew|58902%5F31316" + } + ], + "departureTime": "16:16" + } + } + ], + "uri": "ymapsbm1://transit/line?id=677k_bus_default&ll=37.565257%2C55.870397&name=677%D0%BA&r=2987&type=bus", + "seoname": "677k" + }, + { + "lineId": "m10_bus_default", + "name": "м10", + "Types": [ + "bus" + ], + "type": "bus", + "threads": [ + { + "threadId": "2036926048", + "EssentialStops": [ + { + "id": "stop__9640554", + "name": "Лобненская улица" + }, + { + "id": "stop__9640553", + "name": "Лобненская улица" + } + ], + "BriefSchedule": { + "Events": [ + { + "Estimated": { + "value": "1570972343", + "tzOffset": 10800, + "text": "16:12" + }, + "vehicleId": "codd%5Fnew|62922%5F31434" + }, + { + "Estimated": { + "value": "1570972813", + "tzOffset": 10800, + "text": "16:20" + }, + "vehicleId": "codd%5Fnew|57281%5F31242" + } + ], + "Frequency": { + "text": "15 мин", + "value": 900, + "begin": { + "value": "1570939772", + "tzOffset": 10800, + "text": "7:09" + }, + "end": { + "value": "1571008052", + "tzOffset": 10800, + "text": "2:07" + } + } + } + } + ], + "uri": "ymapsbm1://transit/line?id=m10_bus_default&ll=37.579221%2C55.823763&name=%D0%BC10&r=8474&type=bus", + "seoname": "m10" + } + ] + } + }, + "searchResult": { + "requestId": "1570971868582853-530182592-man1-6817", + "title": "7-й автобусный парк", + "description": "Россия, Москва, Дмитровское шоссе", + "address": "Россия, Москва, Дмитровское шоссе", + "coordinates": [ + 37.56528, + 55.85196 + ], + "bounds": [ + [ + 37.543123, + 55.77889866 + ], + [ + 37.587437, + 55.92488366 + ] + ], + "displayCoordinates": [ + 37.56528, + 55.85196 + ], + "metro": [ + { + "id": "2244536395", + "name": "Верхние Лихоборы", + "distance": "510 м", + "distanceValue": 509.265, + "coordinates": [ + 37.56121218, + 55.854501501 + ], + "type": "metro", + "color": "#99cc33" + }, + { + "id": "1727539211", + "name": "Окружная", + "distance": "640 м", + "distanceValue": 641.333, + "coordinates": [ + 37.572849014, + 55.848814359 + ], + "type": "metro", + "color": "#ffa8af" + }, + { + "id": "2244535785", + "name": "Окружная", + "distance": "1,3 км", + "distanceValue": 1263.44, + "coordinates": [ + 37.575977155, + 55.844377845 + ], + "type": "metro", + "color": "#99cc33" + } + ], + "stops": [ + { + "id": "stop__9639579", + "name": "7-й автобусный парк", + "distance": "0 м", + "distanceValue": 0.0383997, + "coordinates": [ + 37.565280044, + 55.851959656 + ], + "type": "common" + }, + { + "id": "2310890052", + "name": "Метро Верхние Лихоборы", + "distance": "420 м", + "distanceValue": 424.274, + "coordinates": [ + 37.563047501, + 55.853727589 + ], + "type": "common" + }, + { + "id": "stop__9639678", + "name": "Метро Верхние Лихоборы (северный вестибюль)", + "distance": "630 м", + "distanceValue": 629.689, + "coordinates": [ + 37.562346735, + 55.857147019 + ], + "type": "common" + }, + { + "id": "station__lh_9601830", + "name": "Окружная", + "distance": "860 м", + "distanceValue": 857.487, + "coordinates": [ + 37.574303, + 55.847684 + ], + "type": "common" + }, + { + "id": "stop__9639906", + "name": "Платформа Окружная", + "distance": "930 м", + "distanceValue": 926.144, + "coordinates": [ + 37.576123886, + 55.847913668 + ], + "type": "common" + } + ], + "logId": "dHlwZT1iaXpmaW5kZXI7aWQ9MjM5MzY2OTUwNjU4", + "type": "business", + "id": "239366950658", + "shortTitle": "7-й автобусный парк", + "additionalAddress": "", + "fullAddress": "Россия, Москва, Дмитровское шоссе", + "postalCode": "", + "addressDetails": { + "locality": "Москва", + "street": "Дмитровское шоссе" + }, + "categories": [ + { + "name": "Остановка общественного транспорта", + "class": "bus stop", + "seoname": "public_transport_stop", + "pluralName": "Остановки общественного транспорта", + "id": "223677355200" + } + ], + "status": "open", + "businessLinks": [], + "businessProperties": { + "geoproduct_poi_color": "#ABAEB3", + "snippet_show_title": "short_title", + "snippet_show_rating": "five_star_rating", + "snippet_show_photo": "single_photo", + "snippet_show_eta": "show_eta", + "snippet_show_category": "single_category", + "snippet_show_subline": [ + "no_subline" + ], + "snippet_show_geoproduct_offer": "show_geoproduct_offer", + "snippet_show_bookmark": "show_bookmark", + "detailview_show_claim_organization": "not_show_claim_organization", + "detailview_show_reviews": "show_reviews", + "detailview_show_add_photo_button": "show_add_photo_button", + "detailview_show_taxi_button": "show_taxi_button", + "sensitive": "1" + }, + "seoname": "7_y_avtobusny_park", + "geoId": 117015, + "uri": "ymapsbm1://org?oid=239366950658", + "uriList": [ + "ymapsbm1://org?oid=239366950658", + "ymapsbm1://transit/stop?id=stop__9639579" + ], + "references": [ + { + "id": "2036929560", + "scope": "nyak" + } + ], + "ratingData": { + "ratingCount": 0, + "ratingValue": 0, + "reviewCount": 0 + }, + "sources": [ + { + "id": "yandex", + "name": "Яндекс", + "href": "https://www.yandex.ru" + } + ], + "analyticsId": "1" + }, + "toponymSeoname": "dmitrovskoye_shosse" + } +} \ No newline at end of file From b7023a96a3e5aa349e75c2790c1c14c7158f430f Mon Sep 17 00:00:00 2001 From: "Steven D. Lander" <3169732+stevendlander@users.noreply.github.com> Date: Mon, 14 Oct 2019 04:51:37 -0400 Subject: [PATCH 263/639] Issue #27288 Move imports to top for FFMPEG (#27613) --- homeassistant/components/ffmpeg/__init__.py | 2 +- homeassistant/components/ffmpeg/camera.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ffmpeg/__init__.py b/homeassistant/components/ffmpeg/__init__.py index 51e1cac3859..673a34230fc 100644 --- a/homeassistant/components/ffmpeg/__init__.py +++ b/homeassistant/components/ffmpeg/__init__.py @@ -3,6 +3,7 @@ import logging import re import voluptuous as vol +from haffmpeg.tools import FFVersion from homeassistant.core import callback from homeassistant.const import ( @@ -105,7 +106,6 @@ class FFmpegManager: async def async_get_version(self): """Return ffmpeg version.""" - from haffmpeg.tools import FFVersion ffversion = FFVersion(self._bin, self.hass.loop) self._version = await ffversion.get_version() diff --git a/homeassistant/components/ffmpeg/camera.py b/homeassistant/components/ffmpeg/camera.py index 598ffe36bd4..0f500176933 100644 --- a/homeassistant/components/ffmpeg/camera.py +++ b/homeassistant/components/ffmpeg/camera.py @@ -3,6 +3,8 @@ import asyncio import logging import voluptuous as vol +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import ImageFrame, IMAGE_JPEG from homeassistant.components.camera import PLATFORM_SCHEMA, Camera, SUPPORT_STREAM from homeassistant.const import CONF_NAME @@ -53,7 +55,6 @@ class FFmpegCamera(Camera): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG ffmpeg = ImageFrame(self._manager.binary, loop=self.hass.loop) @@ -66,7 +67,6 @@ class FFmpegCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg stream = CameraMjpeg(self._manager.binary, loop=self.hass.loop) await stream.open_camera(self._input, extra_cmd=self._extra_arguments) From 1cae6e664c39f50c7254c80adf228e362e8099b9 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 14 Oct 2019 19:56:40 +1100 Subject: [PATCH 264/639] move imports to top-level (#27630) --- homeassistant/components/pushover/notify.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/pushover/notify.py b/homeassistant/components/pushover/notify.py index 83da9a657fe..3f78897838d 100644 --- a/homeassistant/components/pushover/notify.py +++ b/homeassistant/components/pushover/notify.py @@ -1,7 +1,9 @@ """Pushover platform for notify component.""" import logging +import requests import voluptuous as vol +from pushover import InitError, Client, RequestError from homeassistant.const import CONF_API_KEY import homeassistant.helpers.config_validation as cv @@ -28,8 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the Pushover notification service.""" - from pushover import InitError - try: return PushoverNotificationService( hass, config[CONF_USER_KEY], config[CONF_API_KEY] @@ -44,8 +44,6 @@ class PushoverNotificationService(BaseNotificationService): def __init__(self, hass, user_key, api_token): """Initialize the service.""" - from pushover import Client - self._hass = hass self._user_key = user_key self._api_token = api_token @@ -53,8 +51,6 @@ class PushoverNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from pushover import RequestError - # Make a copy and use empty dict if necessary data = dict(kwargs.get(ATTR_DATA) or {}) @@ -65,8 +61,6 @@ class PushoverNotificationService(BaseNotificationService): # If attachment is a URL, use requests to open it as a stream. if data[ATTR_ATTACHMENT].startswith("http"): try: - import requests - response = requests.get( data[ATTR_ATTACHMENT], stream=True, timeout=5 ) From 017a5a5b09c471c4c393eba281f77a252c62b241 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 14 Oct 2019 14:30:08 +0200 Subject: [PATCH 265/639] Update azure-pipelines-wheels.yml for Azure Pipelines --- azure-pipelines-wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 42815d8c8ae..5092010c49c 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -18,7 +18,7 @@ schedules: always: true variables: - name: versionWheels - value: '1.3-3.7-alpine3.10' + value: '1.4-3.7-alpine3.10' resources: repositories: - repository: azure From 3c280565fa630fabe945129e60abc0e060a1eb80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 14:59:26 +0200 Subject: [PATCH 266/639] move imports in synology_srm component (#27603) --- homeassistant/components/synology_srm/device_tracker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/synology_srm/device_tracker.py b/homeassistant/components/synology_srm/device_tracker.py index b45b393e332..36306efa93e 100644 --- a/homeassistant/components/synology_srm/device_tracker.py +++ b/homeassistant/components/synology_srm/device_tracker.py @@ -4,9 +4,10 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/device_tracker.synology_srm/ """ import logging + +import synology_srm import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, @@ -14,12 +15,13 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.const import ( CONF_HOST, - CONF_USERNAME, CONF_PASSWORD, CONF_PORT, CONF_SSL, + CONF_USERNAME, CONF_VERIFY_SSL, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -52,7 +54,6 @@ class SynologySrmDeviceScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - import synology_srm self.client = synology_srm.Client( host=config[CONF_HOST], From 14e3b3af6f10c71ed7cfe23a7c95cd314e040055 Mon Sep 17 00:00:00 2001 From: bouni Date: Mon, 14 Oct 2019 15:00:02 +0200 Subject: [PATCH 267/639] moved imports to top level (#27632) --- homeassistant/components/bt_smarthub/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/bt_smarthub/device_tracker.py b/homeassistant/components/bt_smarthub/device_tracker.py index ece67e3b635..45b18b963c5 100644 --- a/homeassistant/components/bt_smarthub/device_tracker.py +++ b/homeassistant/components/bt_smarthub/device_tracker.py @@ -1,15 +1,16 @@ """Support for BT Smart Hub (Sometimes referred to as BT Home Hub 6).""" import logging +import btsmarthub_devicelist import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -74,7 +75,6 @@ class BTSmartHubScanner(DeviceScanner): def get_bt_smarthub_data(self): """Retrieve data from BT Smart Hub and return parsed result.""" - import btsmarthub_devicelist # Request data from bt smarthub into a list of dicts. data = btsmarthub_devicelist.get_devicelist( From aefb807222f1ef949e01e8026b0c6f78c1923014 Mon Sep 17 00:00:00 2001 From: bouni Date: Mon, 14 Oct 2019 15:00:51 +0200 Subject: [PATCH 268/639] moved imports to top level (#27634) --- homeassistant/components/cisco_ios/device_tracker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cisco_ios/device_tracker.py b/homeassistant/components/cisco_ios/device_tracker.py index b442b24feb4..5a42ef1c8b8 100644 --- a/homeassistant/components/cisco_ios/device_tracker.py +++ b/homeassistant/components/cisco_ios/device_tracker.py @@ -1,15 +1,17 @@ """Support for Cisco IOS Routers.""" import logging +import re +from pexpect import pxssh import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -100,8 +102,6 @@ class CiscoDeviceScanner(DeviceScanner): def _get_arp_data(self): """Open connection to the router and get arp entries.""" - from pexpect import pxssh - import re try: cisco_ssh = pxssh.pxssh() From 91c6cd96465ba92469bc7a05e5848f7abb2ebccc Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Tue, 15 Oct 2019 00:02:00 +1100 Subject: [PATCH 269/639] Move imports in darksky component (#27633) * move imports to top-level * modify patch path * removed unused mocks and patches --- homeassistant/components/darksky/sensor.py | 3 +-- homeassistant/components/darksky/weather.py | 3 +-- tests/components/darksky/test_sensor.py | 23 +++++++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index d4e7e7ec63a..cd8417e3e84 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import forecastio import voluptuous as vol from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout @@ -797,8 +798,6 @@ class DarkSkyData: def _update(self): """Get the latest data from Dark Sky.""" - import forecastio - try: self.data = forecastio.load_forecast( self._api_key, diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index dc5708d12a0..41f063399c1 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import forecastio from requests.exceptions import ConnectionError as ConnectError, HTTPError, Timeout import voluptuous as vol @@ -244,8 +245,6 @@ class DarkSkyData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Dark Sky.""" - import forecastio - try: self.data = forecastio.load_forecast( self._api_key, self.latitude, self.longitude, units=self.requested_units diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index 23d16ed35f4..be66b74c186 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -112,7 +112,10 @@ class TestDarkSkySetup(unittest.TestCase): self.hass.stop() @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) + @patch( + "homeassistant.components.darksky.sensor.forecastio.load_forecast", + new=load_forecastMock, + ) def test_setup_with_config(self, mock_forecastio): """Test the platform setup with configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL) @@ -120,9 +123,7 @@ class TestDarkSkySetup(unittest.TestCase): state = self.hass.states.get("sensor.dark_sky_summary") assert state is not None - @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) - def test_setup_with_invalid_config(self, mock_forecastio): + def test_setup_with_invalid_config(self): """Test the platform setup with invalid configuration.""" setup_component(self.hass, "sensor", INVALID_CONFIG_MINIMAL) @@ -130,7 +131,10 @@ class TestDarkSkySetup(unittest.TestCase): assert state is None @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) + @patch( + "homeassistant.components.darksky.sensor.forecastio.load_forecast", + new=load_forecastMock, + ) def test_setup_with_language_config(self, mock_forecastio): """Test the platform setup with language configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_LANG_DE) @@ -138,9 +142,7 @@ class TestDarkSkySetup(unittest.TestCase): state = self.hass.states.get("sensor.dark_sky_summary") assert state is not None - @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) - def test_setup_with_invalid_language_config(self, mock_forecastio): + def test_setup_with_invalid_language_config(self): """Test the platform setup with language configuration.""" setup_component(self.hass, "sensor", INVALID_CONFIG_LANG) @@ -164,7 +166,10 @@ class TestDarkSkySetup(unittest.TestCase): assert not response @MockDependency("forecastio") - @patch("forecastio.load_forecast", new=load_forecastMock) + @patch( + "homeassistant.components.darksky.sensor.forecastio.load_forecast", + new=load_forecastMock, + ) def test_setup_with_alerts_config(self, mock_forecastio): """Test the platform setup with alert configuration.""" setup_component(self.hass, "sensor", VALID_CONFIG_ALERTS) From d7d7f6a1c9db109b5ff2612f6940901f2dd390c0 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 14 Oct 2019 15:03:07 +0200 Subject: [PATCH 270/639] Fix temperature and heating mode (#27604) --- homeassistant/components/vicare/climate.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 7010f943707..0dcb83f758a 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -21,6 +21,7 @@ _LOGGER = logging.getLogger(__name__) VICARE_MODE_DHW = "dhw" VICARE_MODE_DHWANDHEATING = "dhwAndHeating" +VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling" VICARE_MODE_FORCEDREDUCED = "forcedReduced" VICARE_MODE_FORCEDNORMAL = "forcedNormal" VICARE_MODE_OFF = "standby" @@ -46,6 +47,7 @@ SUPPORT_FLAGS_HEATING = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE VICARE_TO_HA_HVAC_HEATING = { VICARE_MODE_DHW: HVAC_MODE_OFF, VICARE_MODE_DHWANDHEATING: HVAC_MODE_AUTO, + VICARE_MODE_DHWANDHEATINGCOOLING: HVAC_MODE_AUTO, VICARE_MODE_FORCEDREDUCED: HVAC_MODE_OFF, VICARE_MODE_FORCEDNORMAL: HVAC_MODE_HEAT, VICARE_MODE_OFF: HVAC_MODE_OFF, @@ -200,9 +202,8 @@ class ViCareClimate(ClimateDevice): """Set new target temperatures.""" temp = kwargs.get(ATTR_TEMPERATURE) if temp is not None: - self._api.setProgramTemperature( - self._current_program, self._target_temperature - ) + self._api.setProgramTemperature(self._current_program, temp) + self._target_temperature = temp @property def preset_mode(self): From 79b391c673e61d069d9bbdd4e751a0aa8a0681d4 Mon Sep 17 00:00:00 2001 From: bouni Date: Mon, 14 Oct 2019 15:58:15 +0200 Subject: [PATCH 271/639] moved imports to top level (#27640) --- homeassistant/components/co2signal/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index 9098a053fff..7160d140b3f 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -1,16 +1,17 @@ """Support for the CO2signal platform.""" import logging +import CO2Signal import voluptuous as vol -import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_ATTRIBUTION, - CONF_TOKEN, CONF_LATITUDE, CONF_LONGITUDE, + CONF_TOKEN, ) -from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity CONF_COUNTRY_CODE = "country_code" @@ -97,7 +98,6 @@ class CO2Sensor(Entity): def update(self): """Get the latest data and updates the states.""" - import CO2Signal _LOGGER.debug("Update data for %s", self._friendly_name) From a79a9809f492e8ff594f9ef3b2d9fdc046a0f081 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 14 Oct 2019 16:02:39 +0200 Subject: [PATCH 272/639] ESPHome Fix intermediary state published (#27638) Fixes https://github.com/esphome/issues/issues/426 --- homeassistant/components/esphome/__init__.py | 17 ++++++++++++++--- homeassistant/components/esphome/camera.py | 4 ++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index bc06aba94ea..dd4ac699089 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -479,7 +479,9 @@ class EsphomeEntity(Entity): } self._remove_callbacks.append( async_dispatcher_connect( - self.hass, DISPATCHER_UPDATE_ENTITY.format(**kwargs), self._on_update + self.hass, + DISPATCHER_UPDATE_ENTITY.format(**kwargs), + self._on_state_update, ) ) @@ -493,14 +495,23 @@ class EsphomeEntity(Entity): async_dispatcher_connect( self.hass, DISPATCHER_ON_DEVICE_UPDATE.format(**kwargs), - self.async_schedule_update_ha_state, + self._on_device_update, ) ) - async def _on_update(self) -> None: + async def _on_state_update(self) -> None: """Update the entity state when state or static info changed.""" self.async_schedule_update_ha_state() + async def _on_device_update(self) -> None: + """Update the entity state when device info has changed.""" + if self._entry_data.available: + # Don't update the HA state yet when the device comes online. + # Only update the HA state when the full state arrives + # through the next entity state packet. + return + self.async_schedule_update_ha_state() + async def async_will_remove_from_hass(self) -> None: """Unregister callbacks.""" for remove_callback in self._remove_callbacks: diff --git a/homeassistant/components/esphome/camera.py b/homeassistant/components/esphome/camera.py index cc2e0cede23..c3615c4726d 100644 --- a/homeassistant/components/esphome/camera.py +++ b/homeassistant/components/esphome/camera.py @@ -47,9 +47,9 @@ class EsphomeCamera(Camera, EsphomeEntity): def _state(self) -> Optional[CameraState]: return super()._state - async def _on_update(self) -> None: + async def _on_state_update(self) -> None: """Notify listeners of new image when update arrives.""" - await super()._on_update() + await super()._on_state_update() async with self._image_cond: self._image_cond.notify_all() From 6d4e3945d627162299c4545e201127577a766df1 Mon Sep 17 00:00:00 2001 From: bouni Date: Mon, 14 Oct 2019 17:25:55 +0200 Subject: [PATCH 273/639] moved imports to top level (#27641) --- homeassistant/components/config/config_entries.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index b21991a8479..81065665e34 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -1,6 +1,7 @@ """Http views to control the config manager.""" import aiohttp.web_exceptions import voluptuous as vol +import voluptuous_serialize from homeassistant import config_entries, data_entry_flow from homeassistant.auth.permissions.const import CAT_CONFIG_ENTRIES @@ -41,8 +42,6 @@ def _prepare_json(result): if result["type"] != data_entry_flow.RESULT_TYPE_FORM: return result - import voluptuous_serialize - data = result.copy() schema = data["data_schema"] From 288d370ef53605d5b5f19b82be142ea51225a3fa Mon Sep 17 00:00:00 2001 From: ju Date: Mon, 14 Oct 2019 17:28:25 +0200 Subject: [PATCH 274/639] Fix html5 notification documentation url (#27636) --- homeassistant/components/html5/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index ac76911b9f6..a802609ac85 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -56,7 +56,7 @@ def gcm_api_deprecated(value): "Configuring html5_push_notifications via the GCM api" " has been deprecated and will stop working after April 11," " 2019. Use the VAPID configuration instead. For instructions," - " see https://www.home-assistant.io/components/notify.html5/" + " see https://www.home-assistant.io/integrations/html5/" ) return value From de7963544fc1faab23f659ed19f094e641839398 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Mon, 14 Oct 2019 17:38:34 +0200 Subject: [PATCH 275/639] Apply isort on rfxtrx classes (#27615) * Move imports in rfxtrx component * Apply isort on rfxtrx files * Update test_switch.py --- homeassistant/components/rfxtrx/__init__.py | 5 +++-- homeassistant/components/rfxtrx/binary_sensor.py | 1 + homeassistant/components/rfxtrx/light.py | 1 + homeassistant/components/rfxtrx/sensor.py | 2 +- homeassistant/components/rfxtrx/switch.py | 1 + tests/components/rfxtrx/test_cover.py | 7 ++++--- tests/components/rfxtrx/test_init.py | 3 ++- tests/components/rfxtrx/test_light.py | 7 ++++--- tests/components/rfxtrx/test_sensor.py | 2 +- tests/components/rfxtrx/test_switch.py | 11 ++++++----- 10 files changed, 24 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 73ee07cfb5f..1515ce33c6e 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -1,7 +1,8 @@ """Support for RFXtrx devices.""" -from collections import OrderedDict import binascii +from collections import OrderedDict import logging + import RFXtrx as rfxtrxmod import voluptuous as vol @@ -13,8 +14,8 @@ from homeassistant.const import ( CONF_DEVICES, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, - TEMP_CELSIUS, POWER_WATT, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 259f914b408..6465dc36326 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -1,5 +1,6 @@ """Support for RFXtrx binary sensors.""" import logging + import RFXtrx as rfxtrxmod import voluptuous as vol diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index 82b1407c798..a745a11388a 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -1,5 +1,6 @@ """Support for RFXtrx lights.""" import logging + import RFXtrx as rfxtrxmod import voluptuous as vol diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 5f6b90b600f..5429943a7a6 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -1,8 +1,8 @@ """Support for RFXtrx sensors.""" import logging -import voluptuous as vol from RFXtrx import SensorEvent +import voluptuous as vol from homeassistant.components import rfxtrx from homeassistant.components.sensor import PLATFORM_SCHEMA diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index b5c830a298d..6d91b261a4f 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -1,5 +1,6 @@ """Support for RFXtrx switches.""" import logging + import RFXtrx as rfxtrxmod import voluptuous as vol diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index d2bfb114804..d85ea5cf6f4 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -1,10 +1,11 @@ """The tests for the Rfxtrx cover platform.""" import unittest -import pytest -import RFXtrx as rfxtrxmod -from homeassistant.setup import setup_component +import RFXtrx as rfxtrxmod +import pytest + from homeassistant.components import rfxtrx as rfxtrx_core +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index ac046b99897..ec457af7575 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -4,9 +4,10 @@ import unittest import pytest +from homeassistant.components import rfxtrx as rfxtrx from homeassistant.core import callback from homeassistant.setup import setup_component -from homeassistant.components import rfxtrx as rfxtrx + from tests.common import get_test_home_assistant diff --git a/tests/components/rfxtrx/test_light.py b/tests/components/rfxtrx/test_light.py index 1254a6d6697..a5230cc5f3c 100644 --- a/tests/components/rfxtrx/test_light.py +++ b/tests/components/rfxtrx/test_light.py @@ -1,10 +1,11 @@ """The tests for the Rfxtrx light platform.""" import unittest -import pytest -import RFXtrx as rfxtrxmod -from homeassistant.setup import setup_component +import RFXtrx as rfxtrxmod +import pytest + from homeassistant.components import rfxtrx as rfxtrx_core +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component diff --git a/tests/components/rfxtrx/test_sensor.py b/tests/components/rfxtrx/test_sensor.py index 3f0cfead3e4..652c823e0cf 100644 --- a/tests/components/rfxtrx/test_sensor.py +++ b/tests/components/rfxtrx/test_sensor.py @@ -3,9 +3,9 @@ import unittest import pytest -from homeassistant.setup import setup_component from homeassistant.components import rfxtrx as rfxtrx_core from homeassistant.const import TEMP_CELSIUS +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index 1e39d4afb75..66da197aae8 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -1,17 +1,18 @@ -"""The tests for the Rfxtrx switch platform.""" +"""The tests for the RFXtrx switch platform.""" import unittest -import pytest -import RFXtrx as rfxtrxmod -from homeassistant.setup import setup_component +import RFXtrx as rfxtrxmod +import pytest + from homeassistant.components import rfxtrx as rfxtrx_core +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant, mock_component @pytest.mark.skipif("os.environ.get('RFXTRX') != 'RUN'") class TestSwitchRfxtrx(unittest.TestCase): - """Test the Rfxtrx switch platform.""" + """Test the RFXtrx switch platform.""" def setUp(self): """Set up things to be run when tests are started.""" From 09de6d58896b20c0116c5606294d58ede17bfe77 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 14 Oct 2019 17:41:16 +0200 Subject: [PATCH 276/639] Fix ESPHome climate preset mode refactor (#27637) Fixes https://github.com/home-assistant/home-assistant/issues/25613 --- homeassistant/components/esphome/climate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/climate.py b/homeassistant/components/esphome/climate.py index fa840078aa4..1dfe2184952 100644 --- a/homeassistant/components/esphome/climate.py +++ b/homeassistant/components/esphome/climate.py @@ -17,6 +17,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE_RANGE, PRESET_AWAY, HVAC_MODE_OFF, + PRESET_HOME, ) from homeassistant.const import ( ATTR_TEMPERATURE, @@ -96,7 +97,7 @@ class EsphomeClimateDevice(EsphomeEntity, ClimateDevice): @property def preset_modes(self): """Return preset modes.""" - return [PRESET_AWAY] if self._static_info.supports_away else [] + return [PRESET_AWAY, PRESET_HOME] if self._static_info.supports_away else [] @property def target_temperature_step(self) -> float: From 5a83a92390e8a3255885198c80622556f886b9b3 Mon Sep 17 00:00:00 2001 From: "Steven D. Lander" <3169732+stevendlander@users.noreply.github.com> Date: Mon, 14 Oct 2019 11:44:30 -0400 Subject: [PATCH 277/639] Refactor imports for tensorflow (#27617) * Refactoring imports for tensorflow * Removing whitespace spaces on blank line 110 * Moving tensorflow to try/except block * Fixed black formatting * Refactoring try/except to if/else --- .../components/tensorflow/image_processing.py | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 65e20f558a7..1f49888cb95 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -2,8 +2,22 @@ import logging import os import sys - +import io import voluptuous as vol +from PIL import Image, ImageDraw +import numpy as np + +try: + import cv2 +except ImportError: + cv2 = None + +try: + # Verify that the TensorFlow Object Detection API is pre-installed + import tensorflow as tf # noqa + from object_detection.utils import label_map_util # noqa +except ImportError: + label_map_util = None from homeassistant.components.image_processing import ( CONF_CONFIDENCE, @@ -84,14 +98,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # append custom model path to sys.path sys.path.append(model_dir) - try: - # Verify that the TensorFlow Object Detection API is pre-installed - # pylint: disable=unused-import,unused-variable - os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" - import tensorflow as tf # noqa - from object_detection.utils import label_map_util # noqa - except ImportError: - # pylint: disable=line-too-long + os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" + if label_map_util is None: _LOGGER.error( "No TensorFlow Object Detection library found! Install or compile " "for your system following instructions here: " @@ -99,11 +107,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) # noqa return - try: - # Display warning that PIL will be used if no OpenCV is found. - # pylint: disable=unused-import,unused-variable - import cv2 # noqa - except ImportError: + if cv2 is None: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " "PIL at reduced resolution" @@ -236,9 +240,6 @@ class TensorFlowImageProcessor(ImageProcessingEntity): } def _save_image(self, image, matches, paths): - from PIL import Image, ImageDraw - import io - img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") img_width, img_height = img.size draw = ImageDraw.Draw(img) @@ -280,18 +281,8 @@ class TensorFlowImageProcessor(ImageProcessingEntity): def process_image(self, image): """Process the image.""" - import numpy as np - - try: - import cv2 # pylint: disable=import-error - - img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) - inp = img[:, :, [2, 1, 0]] # BGR->RGB - inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) - except ImportError: - from PIL import Image - import io + if cv2 is None: img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") img.thumbnail((460, 460), Image.ANTIALIAS) img_width, img_height = img.size @@ -301,6 +292,10 @@ class TensorFlowImageProcessor(ImageProcessingEntity): .astype(np.uint8) ) inp_expanded = np.expand_dims(inp, axis=0) + else: + img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) + inp = img[:, :, [2, 1, 0]] # BGR->RGB + inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) image_tensor = self._graph.get_tensor_by_name("image_tensor:0") boxes = self._graph.get_tensor_by_name("detection_boxes:0") From 2295b332049af325a079605a9dbc39dc1d076c37 Mon Sep 17 00:00:00 2001 From: bouni Date: Mon, 14 Oct 2019 19:57:03 +0200 Subject: [PATCH 278/639] Move imports in bluesound component (#27502) * moved imports to top level * changed import order * changed import order --- homeassistant/components/bluesound/media_player.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index bf0568aed16..702cf5ddc30 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -3,14 +3,16 @@ import asyncio from asyncio.futures import CancelledError from datetime import timedelta import logging +from urllib import parse import aiohttp from aiohttp.client_exceptions import ClientError from aiohttp.hdrs import CONNECTION, KEEP_ALIVE import async_timeout import voluptuous as vol +import xmltodict -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, DOMAIN, @@ -329,7 +331,6 @@ class BluesoundPlayer(MediaPlayerDevice): self, method, raise_timeout=False, allow_offline=False ): """Send command to the player.""" - import xmltodict if not self._is_online and not allow_offline: return @@ -370,7 +371,6 @@ class BluesoundPlayer(MediaPlayerDevice): async def async_update_status(self): """Use the poll session to always get the status of the player.""" - import xmltodict response = None @@ -690,7 +690,6 @@ class BluesoundPlayer(MediaPlayerDevice): @property def source(self): """Name of the current input source.""" - from urllib import parse if self._status is None or (self.is_grouped and not self.is_master): return None From c7bd0fe909e3864ec2c34559091d7bd5855b6dad Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 14 Oct 2019 20:11:43 +0200 Subject: [PATCH 279/639] Fix ZHA regressions caused by "Support async validation of device trigger" (#27401) * Revert "Support async validation of device trigger (#27333)" This reverts commit fdf4f398a79e775e11dc9fdd391a8b53f7b773c5. * Revert only ZHA changes * Fix whitespace * Restore ZHA changes but add check to make sure ZHA is loaded * Address review comment * Remove additional check --- homeassistant/components/zha/device_trigger.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index 8d74ae108a2..cdd62b11d1e 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -25,14 +25,14 @@ async def async_validate_trigger_config(hass, config): """Validate config.""" config = TRIGGER_SCHEMA(config) - trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) - zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) - - if ( - zha_device.device_automation_triggers is None - or trigger not in zha_device.device_automation_triggers - ): - raise InvalidDeviceAutomationConfig + if "zha" in hass.config.components: + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + zha_device = await async_get_zha_device(hass, config[CONF_DEVICE_ID]) + if ( + zha_device.device_automation_triggers is None + or trigger not in zha_device.device_automation_triggers + ): + raise InvalidDeviceAutomationConfig return config From 2f6c2fadd0e033904a31afa1725fbbd1b0a0fcaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 21:20:15 +0200 Subject: [PATCH 280/639] move imports in squeezebox component (#27650) --- homeassistant/components/squeezebox/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 6540fca1405..6d67f67a3ce 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -2,13 +2,14 @@ import asyncio import json import logging +import socket import urllib.parse import aiohttp import async_timeout import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( ATTR_MEDIA_ENQUEUE, DOMAIN, @@ -100,7 +101,6 @@ SERVICE_TO_METHOD = { async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the squeezebox platform.""" - import socket known_servers = hass.data.get(KNOWN_SERVERS) if known_servers is None: From 759ad0893018bb8bce6f29c74aa007eb75e93d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 14 Oct 2019 23:03:37 +0300 Subject: [PATCH 281/639] Typing misc fixes (#27543) * Make async_get_conditions return type hint more specific * Exclude script/scaffold/templates/ from pre-commit mypy --- .pre-commit-config.yaml | 1 + .../components/binary_sensor/device_condition.py | 8 +++++--- .../components/device_automation/toggle_entity.py | 2 +- homeassistant/components/light/device_condition.py | 6 ++++-- homeassistant/components/sensor/device_condition.py | 8 +++++--- homeassistant/components/switch/device_condition.py | 6 ++++-- .../device_condition/integration/device_condition.py | 6 ++++-- 7 files changed, 24 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8e8792e88c9..55e00443ba1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,3 +17,4 @@ repos: rev: v0.730 hooks: - id: mypy + exclude: ^script/scaffold/templates/ diff --git a/homeassistant/components/binary_sensor/device_condition.py b/homeassistant/components/binary_sensor/device_condition.py index a38c0c09ee5..0766d82c727 100644 --- a/homeassistant/components/binary_sensor/device_condition.py +++ b/homeassistant/components/binary_sensor/device_condition.py @@ -1,5 +1,5 @@ """Implemenet device conditions for binary sensor.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.core import HomeAssistant @@ -193,9 +193,11 @@ CONDITION_SCHEMA = cv.DEVICE_CONDITION_BASE_SCHEMA.extend( ) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions.""" - conditions: List[dict] = [] + conditions: List[Dict[str, str]] = [] entity_registry = await async_get_registry(hass) entries = [ entry diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 98a1af9c4ca..5f01f4d9d71 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -205,7 +205,7 @@ async def async_get_actions( async def async_get_conditions( hass: HomeAssistant, device_id: str, domain: str -) -> List[dict]: +) -> List[Dict[str, str]]: """List device conditions.""" return await _async_get_automations(hass, device_id, ENTITY_CONDITIONS, domain) diff --git a/homeassistant/components/light/device_condition.py b/homeassistant/components/light/device_condition.py index 0b3cecbea41..e87ae3bf945 100644 --- a/homeassistant/components/light/device_condition.py +++ b/homeassistant/components/light/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for lights.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.core import HomeAssistant @@ -24,7 +24,9 @@ def async_condition_from_config( return toggle_entity.async_condition_from_config(config) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 18aa46d78e1..26479807991 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for sensors.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.core import HomeAssistant @@ -80,9 +80,11 @@ CONDITION_SCHEMA = vol.All( ) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions.""" - conditions: List[dict] = [] + conditions: List[Dict[str, str]] = [] entity_registry = await async_get_registry(hass) entries = [ entry diff --git a/homeassistant/components/switch/device_condition.py b/homeassistant/components/switch/device_condition.py index 7df972151c7..56f8f6c196e 100644 --- a/homeassistant/components/switch/device_condition.py +++ b/homeassistant/components/switch/device_condition.py @@ -1,5 +1,5 @@ """Provides device conditions for switches.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.core import HomeAssistant @@ -24,7 +24,9 @@ def async_condition_from_config( return toggle_entity.async_condition_from_config(config) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions.""" return await toggle_entity.async_get_conditions(hass, device_id, DOMAIN) diff --git a/script/scaffold/templates/device_condition/integration/device_condition.py b/script/scaffold/templates/device_condition/integration/device_condition.py index 9acb351b197..fa123cff8e0 100644 --- a/script/scaffold/templates/device_condition/integration/device_condition.py +++ b/script/scaffold/templates/device_condition/integration/device_condition.py @@ -1,5 +1,5 @@ """Provides device automations for NEW_NAME.""" -from typing import List +from typing import Dict, List import voluptuous as vol from homeassistant.const import ( @@ -29,7 +29,9 @@ CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( ) -async def async_get_conditions(hass: HomeAssistant, device_id: str) -> List[dict]: +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: """List device conditions for NEW_NAME devices.""" registry = await entity_registry.async_get_registry(hass) conditions = [] From 75eb33eb701bd973f01cbbc92c587fbcb84f6dec Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 14 Oct 2019 22:07:47 +0200 Subject: [PATCH 282/639] Updated frontend to 20191014.0 (#27661) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 67a66bc9612..43578420212 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191002.2" + "home-assistant-frontend==20191014.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2259f3053fe..1648bdc3db5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.7 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191002.2 +home-assistant-frontend==20191014.0 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index e4c608793b8..dbc94de9a1c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -640,7 +640,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.2 +home-assistant-frontend==20191014.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ec8477c9369..18970fcbac0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -236,7 +236,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191002.2 +home-assistant-frontend==20191014.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 487a5b25271929a0fa8a51dc1eeb567014cdd9f5 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Mon, 14 Oct 2019 23:15:29 +0200 Subject: [PATCH 283/639] Move imports in panasonic_viera component (#27665) --- homeassistant/components/panasonic_viera/media_player.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/panasonic_viera/media_player.py b/homeassistant/components/panasonic_viera/media_player.py index d0b013c3bf3..0b19a8fa552 100644 --- a/homeassistant/components/panasonic_viera/media_player.py +++ b/homeassistant/components/panasonic_viera/media_player.py @@ -1,9 +1,11 @@ """Support for interface with a Panasonic Viera TV.""" import logging +from panasonic_viera import RemoteControl import voluptuous as vol +import wakeonlan -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_URL, SUPPORT_NEXT_TRACK, @@ -62,8 +64,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Panasonic Viera TV platform.""" - from panasonic_viera import RemoteControl - mac = config.get(CONF_MAC) name = config.get(CONF_NAME) port = config.get(CONF_PORT) @@ -95,8 +95,6 @@ class PanasonicVieraTVDevice(MediaPlayerDevice): def __init__(self, mac, name, remote, host, app_power, uuid=None): """Initialize the Panasonic device.""" - import wakeonlan - # Save a reference to the imported class self._wol = wakeonlan self._mac = mac From 5c2bf6dc7c201fd442ea50b5e76074c6f16a44a1 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 14 Oct 2019 23:15:46 +0200 Subject: [PATCH 284/639] Improve discovery title (#27664) --- homeassistant/components/deconz/.translations/en.json | 1 + homeassistant/components/deconz/config_flow.py | 1 + homeassistant/components/deconz/strings.json | 1 + 3 files changed, 3 insertions(+) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index c00bfca3564..e9c64ffe5fa 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -11,6 +11,7 @@ "error": { "no_key": "Couldn't get an API key" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 91768584e8a..5ede8e715b9 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -189,6 +189,7 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context[CONF_BRIDGEID] = bridgeid + self.context["title_placeholders"] = {"host": discovery_info[CONF_HOST]} self.deconz_config = { CONF_HOST: discovery_info[CONF_HOST], diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index db43c022822..3571a9e1207 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -1,6 +1,7 @@ { "config": { "title": "deCONZ Zigbee gateway", + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "init": { "title": "Define deCONZ gateway", From 6c0efe9329d94cdee6319c3faaee254e5b6f9b46 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Mon, 14 Oct 2019 23:17:08 +0200 Subject: [PATCH 285/639] Move imports in panasonic_bluray component (#27658) --- homeassistant/components/panasonic_bluray/media_player.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/panasonic_bluray/media_player.py b/homeassistant/components/panasonic_bluray/media_player.py index 393ecb827cc..4a816252580 100644 --- a/homeassistant/components/panasonic_bluray/media_player.py +++ b/homeassistant/components/panasonic_bluray/media_player.py @@ -2,9 +2,10 @@ from datetime import timedelta import logging +from panacotta import PanasonicBD import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, @@ -53,9 +54,7 @@ class PanasonicBluRay(MediaPlayerDevice): def __init__(self, ip, name): """Initialize the Panasonic Blue-ray device.""" - import panacotta - - self._device = panacotta.PanasonicBD(ip) + self._device = PanasonicBD(ip) self._name = name self._state = STATE_OFF self._position = 0 From a49dbb9718f8e07dad9cd92c3b0382a1ba908ae6 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Mon, 14 Oct 2019 17:19:05 -0400 Subject: [PATCH 286/639] Update Unlock directive for Alexa LockController (#27653) * Update the Alexa.LockController Unlock directive to include the lockState property in the context of the response. * Added Test for Alexa.LockController Unlock directive to include the lockState property in the context of the response. --- homeassistant/components/alexa/handlers.py | 8 ++++++-- tests/components/alexa/test_smart_home.py | 10 +++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index bd07b71ca29..139defe8313 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -412,7 +412,6 @@ async def async_api_lock(hass, config, directive, context): return response -# Not supported by Alexa yet @HANDLERS.register(("Alexa.LockController", "Unlock")) async def async_api_unlock(hass, config, directive, context): """Process an unlock request.""" @@ -425,7 +424,12 @@ async def async_api_unlock(hass, config, directive, context): context=context, ) - return directive.response() + response = directive.response() + response.add_context_property( + {"namespace": "Alexa.LockController", "name": "lockState", "value": "UNLOCKED"} + ) + + return response @HANDLERS.register(("Alexa.Speaker", "SetVolume")) diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 78bdd8e0908..186cb850e34 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -403,12 +403,20 @@ async def test_lock(hass): "Alexa.LockController", "Lock", "lock#test", "lock.lock", hass ) - # always return LOCKED for now properties = msg["context"]["properties"][0] assert properties["name"] == "lockState" assert properties["namespace"] == "Alexa.LockController" assert properties["value"] == "LOCKED" + _, msg = await assert_request_calls_service( + "Alexa.LockController", "Unlock", "lock#test", "lock.unlock", hass + ) + + properties = msg["context"]["properties"][0] + assert properties["name"] == "lockState" + assert properties["namespace"] == "Alexa.LockController" + assert properties["value"] == "UNLOCKED" + async def test_media_player(hass): """Test media player discovery.""" From bcb14182c6128a0252643b65ecccb5f6749e953b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 23:19:37 +0200 Subject: [PATCH 287/639] move imports in statsd component (#27649) --- homeassistant/components/statsd/__init__.py | 4 ++-- tests/components/statsd/test_init.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/statsd/__init__.py b/homeassistant/components/statsd/__init__.py index 714de88dd87..79065f7ba53 100644 --- a/homeassistant/components/statsd/__init__.py +++ b/homeassistant/components/statsd/__init__.py @@ -1,11 +1,12 @@ """Support for sending data to StatsD.""" import logging +import statsd import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PREFIX, EVENT_STATE_CHANGED -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -40,7 +41,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the StatsD component.""" - import statsd conf = config[DOMAIN] host = conf.get(CONF_HOST) diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py index 6deb40c082d..4c7e9d29fee 100644 --- a/tests/components/statsd/test_init.py +++ b/tests/components/statsd/test_init.py @@ -2,15 +2,15 @@ import unittest from unittest import mock +import pytest import voluptuous as vol -from homeassistant.setup import setup_component -import homeassistant.core as ha import homeassistant.components.statsd as statsd -from homeassistant.const import STATE_ON, STATE_OFF, EVENT_STATE_CHANGED +from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON +import homeassistant.core as ha +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant -import pytest class TestStatsd(unittest.TestCase): From 4efa2f3244423c4cf804413362c4ee39e21a247a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 23:19:53 +0200 Subject: [PATCH 288/639] Move imports in steam_online component (#27648) * move imports in steam_online component * fix: import reassigment --- homeassistant/components/steam_online/sensor.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/steam_online/sensor.py b/homeassistant/components/steam_online/sensor.py index 6c9c5ac6079..85e5c49fb2c 100644 --- a/homeassistant/components/steam_online/sensor.py +++ b/homeassistant/components/steam_online/sensor.py @@ -1,15 +1,16 @@ """Sensor for Steam account status.""" -import logging from datetime import timedelta +import logging +import steam import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_API_KEY from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.const import CONF_API_KEY -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -38,13 +39,12 @@ BASE_INTERVAL = timedelta(minutes=1) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Steam platform.""" - import steam as steamod - steamod.api.key.set(config.get(CONF_API_KEY)) + steam.api.key.set(config.get(CONF_API_KEY)) # Initialize steammods app list before creating sensors # to benefit from internal caching of the list. - hass.data[APP_LIST_KEY] = steamod.apps.app_list() - entities = [SteamSensor(account, steamod) for account in config.get(CONF_ACCOUNTS)] + hass.data[APP_LIST_KEY] = steam.apps.app_list() + entities = [SteamSensor(account, steam) for account in config.get(CONF_ACCOUNTS)] if not entities: return add_entities(entities, True) From 9aa28dfd544f496c5e81b3823371e963ada9bc09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 23:20:18 +0200 Subject: [PATCH 289/639] move imports in stream component (#27647) --- homeassistant/components/stream/__init__.py | 41 +++++++++++---------- homeassistant/components/stream/core.py | 8 ++-- homeassistant/components/stream/hls.py | 2 +- homeassistant/components/stream/recorder.py | 7 ++-- homeassistant/components/stream/worker.py | 7 ++-- tests/components/stream/common.py | 7 ++-- tests/components/stream/test_hls.py | 2 +- tests/components/stream/test_init.py | 10 ++--- tests/components/stream/test_recorder.py | 3 +- 9 files changed, 45 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 2ae8dd5f714..4c93ce46135 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -4,31 +4,32 @@ import threading import voluptuous as vol +from homeassistant.auth.util import generate_secret +from homeassistant.const import CONF_FILENAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.loader import bind_hass + +from .const import ( + ATTR_ENDPOINTS, + ATTR_STREAMS, + CONF_DURATION, + CONF_LOOKBACK, + CONF_STREAM_SOURCE, + DOMAIN, + SERVICE_RECORD, +) +from .core import PROVIDERS +from .hls import async_setup_hls +from .recorder import async_setup_recorder +from .worker import stream_worker + try: import uvloop except ImportError: uvloop = None -from homeassistant.auth.util import generate_secret -import homeassistant.helpers.config_validation as cv -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_FILENAME -from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.loader import bind_hass - -from .const import ( - DOMAIN, - ATTR_STREAMS, - ATTR_ENDPOINTS, - CONF_STREAM_SOURCE, - CONF_DURATION, - CONF_LOOKBACK, - SERVICE_RECORD, -) -from .core import PROVIDERS -from .worker import stream_worker -from .hls import async_setup_hls -from .recorder import async_setup_recorder _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 81335783e1a..9282c2cb855 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -2,17 +2,17 @@ import asyncio from collections import deque import io -from typing import List, Any +from typing import Any, List -import attr from aiohttp import web +import attr -from homeassistant.core import callback from homeassistant.components.http import HomeAssistantView +from homeassistant.core import callback from homeassistant.helpers.event import async_call_later from homeassistant.util.decorator import Registry -from .const import DOMAIN, ATTR_STREAMS +from .const import ATTR_STREAMS, DOMAIN PROVIDERS = Registry() diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index c9e62f53a57..2cd98c0a00f 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -5,7 +5,7 @@ from homeassistant.core import callback from homeassistant.util.dt import utcnow from .const import FORMAT_CONTENT_TYPE -from .core import StreamView, StreamOutput, PROVIDERS +from .core import PROVIDERS, StreamOutput, StreamView @callback diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index cd25896aff3..1dd90b8b804 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -1,10 +1,13 @@ """Provide functionality to record stream.""" + import threading from typing import List +import av + from homeassistant.core import callback -from .core import Segment, StreamOutput, PROVIDERS +from .core import PROVIDERS, Segment, StreamOutput @callback @@ -14,8 +17,6 @@ def async_setup_recorder(hass): def recorder_save_worker(file_out: str, segments: List[Segment]): """Handle saving stream.""" - import av - output = av.open(file_out, "w", options={"movflags": "frag_keyframe"}) output_v = None diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index e87221304a3..99ffd833eb3 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -3,6 +3,8 @@ from fractions import Fraction import io import logging +import av + from .const import AUDIO_SAMPLE_RATE from .core import Segment, StreamBuffer @@ -11,9 +13,8 @@ _LOGGER = logging.getLogger(__name__) def generate_audio_frame(): """Generate a blank audio frame.""" - from av import AudioFrame - audio_frame = AudioFrame(format="dbl", layout="mono", samples=1024) + audio_frame = av.AudioFrame(format="dbl", layout="mono", samples=1024) # audio_bytes = b''.join(b'\x00\x00\x00\x00\x00\x00\x00\x00' # for i in range(0, 1024)) audio_bytes = b"\x00\x00\x00\x00\x00\x00\x00\x00" * 1024 @@ -25,7 +26,6 @@ def generate_audio_frame(): def create_stream_buffer(stream_output, video_stream, audio_frame): """Create a new StreamBuffer.""" - import av a_packet = None segment = io.BytesIO() @@ -45,7 +45,6 @@ def create_stream_buffer(stream_output, video_stream, audio_frame): def stream_worker(hass, stream, quit_event): """Handle consuming streams.""" - import av container = av.open(stream.source, options=stream.options) try: diff --git a/tests/components/stream/common.py b/tests/components/stream/common.py index 32ab36dc477..4c34ec0b341 100644 --- a/tests/components/stream/common.py +++ b/tests/components/stream/common.py @@ -1,8 +1,11 @@ """Collection of test helpers.""" import io +import av +import numpy as np + from homeassistant.components.stream import Stream -from homeassistant.components.stream.const import DOMAIN, ATTR_STREAMS +from homeassistant.components.stream.const import ATTR_STREAMS, DOMAIN def generate_h264_video(): @@ -11,8 +14,6 @@ def generate_h264_video(): See: http://docs.mikeboers.com/pyav/develop/cookbook/numpy.html """ - import numpy as np - import av duration = 5 fps = 24 diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index ac564ce7553..293f8d1e4cf 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -4,8 +4,8 @@ from urllib.parse import urlparse import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.stream import request_stream +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py index 80d703c801b..0661a5a9738 100644 --- a/tests/components/stream/test_init.py +++ b/tests/components/stream/test_init.py @@ -1,16 +1,16 @@ """The tests for stream.""" -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest -from homeassistant.const import CONF_FILENAME from homeassistant.components.stream.const import ( + ATTR_STREAMS, + CONF_LOOKBACK, + CONF_STREAM_SOURCE, DOMAIN, SERVICE_RECORD, - CONF_STREAM_SOURCE, - CONF_LOOKBACK, - ATTR_STREAMS, ) +from homeassistant.const import CONF_FILENAME from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index dce8b95d07c..95eeeecf7ad 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -2,11 +2,12 @@ from datetime import timedelta from io import BytesIO from unittest.mock import patch + import pytest -from homeassistant.setup import async_setup_component from homeassistant.components.stream.core import Segment from homeassistant.components.stream.recorder import recorder_save_worker +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed From 97478d1ef478aea3f2b3680598260d62de0c3891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Mon, 14 Oct 2019 23:20:35 +0200 Subject: [PATCH 290/639] Move imports in switchmate component (#27646) * move imports in switchmate component * fix: bring back pylint ignore line --- homeassistant/components/switchmate/switch.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/switchmate/switch.py b/homeassistant/components/switchmate/switch.py index 950a8a67930..6abbfd5fae5 100644 --- a/homeassistant/components/switchmate/switch.py +++ b/homeassistant/components/switchmate/switch.py @@ -1,12 +1,14 @@ """Support for Switchmate.""" -import logging from datetime import timedelta +import logging +# pylint: disable=import-error, no-member, no-value-for-parameter +import switchmate import voluptuous as vol +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_MAC, CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_MAC _LOGGER = logging.getLogger(__name__) @@ -37,8 +39,6 @@ class SwitchmateEntity(SwitchDevice): def __init__(self, mac, name, flip_on_off) -> None: """Initialize the Switchmate.""" - # pylint: disable=import-error, no-member, no-value-for-parameter - import switchmate self._mac = mac self._name = name From 3231e22ddf2fdd65a36028bec5f87d8e7032118b Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 14 Oct 2019 14:56:45 -0700 Subject: [PATCH 291/639] Remove direct authentication via trusted networks or API password (#27656) * Remove direct authentication via trusted networks and API password * Fix tests --- homeassistant/auth/__init__.py | 12 -- homeassistant/bootstrap.py | 6 +- homeassistant/components/auth/login_flow.py | 7 +- homeassistant/components/http/__init__.py | 41 ------ homeassistant/components/http/auth.py | 125 +++--------------- homeassistant/components/http/cors.py | 3 +- homeassistant/components/http/view.py | 10 +- .../components/websocket_api/auth.py | 14 -- homeassistant/config.py | 15 +-- homeassistant/const.py | 1 - tests/components/auth/__init__.py | 2 +- tests/components/auth/test_init.py | 2 +- tests/components/conftest.py | 4 +- .../google_assistant/test_google_assistant.py | 7 +- tests/components/hassio/__init__.py | 1 - tests/components/hassio/conftest.py | 12 +- tests/components/hassio/test_addon_panel.py | 18 +-- tests/components/hassio/test_auth.py | 13 +- tests/components/hassio/test_discovery.py | 8 +- tests/components/hassio/test_http.py | 16 +-- tests/components/http/__init__.py | 4 + tests/components/http/test_auth.py | 46 +++---- tests/components/http/test_ban.py | 4 +- tests/components/http/test_cors.py | 7 +- tests/components/http/test_init.py | 2 +- tests/components/mqtt/test_server.py | 7 +- tests/components/websocket_api/__init__.py | 1 - tests/components/websocket_api/conftest.py | 8 +- tests/components/websocket_api/test_auth.py | 72 ++++------ .../components/websocket_api/test_commands.py | 6 +- tests/components/websocket_api/test_sensor.py | 8 +- tests/scripts/test_check_config.py | 12 +- tests/test_config.py | 43 ------ 33 files changed, 114 insertions(+), 423 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index ee0d6c08441..64391debc10 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -86,18 +86,6 @@ class AuthManager: hass, self._async_create_login_flow, self._async_finish_login_flow ) - @property - def support_legacy(self) -> bool: - """ - Return if legacy_api_password auth providers are registered. - - Should be removed when we removed legacy_api_password auth providers. - """ - for provider_type, _ in self._providers: - if provider_type == "legacy_api_password": - return True - return False - @property def auth_providers(self) -> List[AuthProvider]: """Return a list of available auth providers.""" diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index ef294491141..422eab8ed4a 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -64,13 +64,9 @@ async def async_from_config_dict( ) core_config = config.get(core.DOMAIN, {}) - api_password = config.get("http", {}).get("api_password") - trusted_networks = config.get("http", {}).get("trusted_networks") try: - await conf_util.async_process_ha_core_config( - hass, core_config, api_password, trusted_networks - ) + await conf_util.async_process_ha_core_config(hass, core_config) except vol.Invalid as config_err: conf_util.async_log_exception(config_err, "homeassistant", core_config, hass) return None diff --git a/homeassistant/components/auth/login_flow.py b/homeassistant/components/auth/login_flow.py index 4fa0f866124..d6844396ce7 100644 --- a/homeassistant/components/auth/login_flow.py +++ b/homeassistant/components/auth/login_flow.py @@ -72,7 +72,11 @@ import voluptuous_serialize from homeassistant import data_entry_flow from homeassistant.components.http import KEY_REAL_IP -from homeassistant.components.http.ban import process_wrong_login, log_invalid_auth +from homeassistant.components.http.ban import ( + process_wrong_login, + process_success_login, + log_invalid_auth, +) from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from . import indieauth @@ -185,6 +189,7 @@ class LoginFlowIndexView(HomeAssistantView): return self.json_message("Handler does not support init", 400) if result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY: + await process_success_login(request) result.pop("data") result["result"] = self._store_result(data["client_id"], result["result"]) return self.json(result) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 1b61e74769f..4df606a3c1b 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -17,7 +17,6 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv import homeassistant.util as hass_util from homeassistant.util import ssl as ssl_util -from homeassistant.util.logging import HideSensitiveDataFilter from .auth import setup_auth from .ban import setup_bans @@ -32,7 +31,6 @@ from .view import HomeAssistantView # noqa DOMAIN = "http" -CONF_API_PASSWORD = "api_password" CONF_SERVER_HOST = "server_host" CONF_SERVER_PORT = "server_port" CONF_BASE_URL = "base_url" @@ -42,7 +40,6 @@ CONF_SSL_KEY = "ssl_key" CONF_CORS_ORIGINS = "cors_allowed_origins" CONF_USE_X_FORWARDED_FOR = "use_x_forwarded_for" CONF_TRUSTED_PROXIES = "trusted_proxies" -CONF_TRUSTED_NETWORKS = "trusted_networks" CONF_LOGIN_ATTEMPTS_THRESHOLD = "login_attempts_threshold" CONF_IP_BAN_ENABLED = "ip_ban_enabled" CONF_SSL_PROFILE = "ssl_profile" @@ -59,37 +56,8 @@ DEFAULT_CORS = "https://cast.home-assistant.io" NO_LOGIN_ATTEMPT_THRESHOLD = -1 -def trusted_networks_deprecated(value): - """Warn user trusted_networks config is deprecated.""" - if not value: - return value - - _LOGGER.warning( - "Configuring trusted_networks via the http integration has been" - " deprecated. Use the trusted networks auth provider instead." - " For instructions, see https://www.home-assistant.io/docs/" - "authentication/providers/#trusted-networks" - ) - return value - - -def api_password_deprecated(value): - """Warn user api_password config is deprecated.""" - if not value: - return value - - _LOGGER.warning( - "Configuring api_password via the http integration has been" - " deprecated. Use the legacy api password auth provider instead." - " For instructions, see https://www.home-assistant.io/docs/" - "authentication/providers/#legacy-api-password" - ) - return value - - HTTP_SCHEMA = vol.Schema( { - vol.Optional(CONF_API_PASSWORD): vol.All(cv.string, api_password_deprecated), vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string, vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port, vol.Optional(CONF_BASE_URL): cv.string, @@ -103,9 +71,6 @@ HTTP_SCHEMA = vol.Schema( vol.Inclusive(CONF_TRUSTED_PROXIES, "proxy"): vol.All( cv.ensure_list, [ip_network] ), - vol.Optional(CONF_TRUSTED_NETWORKS, default=[]): vol.All( - cv.ensure_list, [ip_network], trusted_networks_deprecated - ), vol.Optional( CONF_LOGIN_ATTEMPTS_THRESHOLD, default=NO_LOGIN_ATTEMPT_THRESHOLD ): vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD), @@ -149,7 +114,6 @@ async def async_setup(hass, config): if conf is None: conf = HTTP_SCHEMA({}) - api_password = conf.get(CONF_API_PASSWORD) server_host = conf[CONF_SERVER_HOST] server_port = conf[CONF_SERVER_PORT] ssl_certificate = conf.get(CONF_SSL_CERTIFICATE) @@ -162,11 +126,6 @@ async def async_setup(hass, config): login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD] ssl_profile = conf[CONF_SSL_PROFILE] - if api_password is not None: - logging.getLogger("aiohttp.access").addFilter( - HideSensitiveDataFilter(api_password) - ) - server = HomeAssistantHTTP( hass, server_host=server_host, diff --git a/homeassistant/components/http/auth.py b/homeassistant/components/http/auth.py index 4ff581aef02..97bd9b7d4bc 100644 --- a/homeassistant/components/http/auth.py +++ b/homeassistant/components/http/auth.py @@ -1,14 +1,11 @@ """Authentication for HTTP component.""" -import base64 import logging from aiohttp import hdrs from aiohttp.web import middleware import jwt -from homeassistant.auth.providers import legacy_api_password from homeassistant.auth.util import generate_secret -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.core import callback from homeassistant.util import dt as dt_util @@ -52,16 +49,6 @@ def async_sign_path(hass, refresh_token_id, path, expiration): @callback def setup_auth(hass, app): """Create auth middleware for the app.""" - old_auth_warning = set() - - support_legacy = hass.auth.support_legacy - if support_legacy: - _LOGGER.warning("legacy_api_password support has been enabled.") - - trusted_networks = [] - for prv in hass.auth.auth_providers: - if prv.type == "trusted_networks": - trusted_networks += prv.trusted_networks async def async_validate_auth_header(request): """ @@ -75,40 +62,16 @@ def setup_auth(hass, app): # If no space in authorization header return False - if auth_type == "Bearer": - refresh_token = await hass.auth.async_validate_access_token(auth_val) - if refresh_token is None: - return False + if auth_type != "Bearer": + return False - request[KEY_HASS_USER] = refresh_token.user - return True + refresh_token = await hass.auth.async_validate_access_token(auth_val) - if auth_type == "Basic" and support_legacy: - decoded = base64.b64decode(auth_val).decode("utf-8") - try: - username, password = decoded.split(":", 1) - except ValueError: - # If no ':' in decoded - return False + if refresh_token is None: + return False - if username != "homeassistant": - return False - - user = await legacy_api_password.async_validate_password(hass, password) - if user is None: - return False - - request[KEY_HASS_USER] = user - _LOGGER.info( - "Basic auth with api_password is going to deprecate," - " please use a bearer token to access %s from %s", - request.path, - request[KEY_REAL_IP], - ) - old_auth_warning.add(request.path) - return True - - return False + request[KEY_HASS_USER] = refresh_token.user + return True async def async_validate_signed_request(request): """Validate a signed request.""" @@ -140,50 +103,16 @@ def setup_auth(hass, app): request[KEY_HASS_USER] = refresh_token.user return True - async def async_validate_trusted_networks(request): - """Test if request is from a trusted ip.""" - ip_addr = request[KEY_REAL_IP] - - if not any(ip_addr in trusted_network for trusted_network in trusted_networks): - return False - - user = await hass.auth.async_get_owner() - if user is None: - return False - - request[KEY_HASS_USER] = user - return True - - async def async_validate_legacy_api_password(request, password): - """Validate api_password.""" - user = await legacy_api_password.async_validate_password(hass, password) - if user is None: - return False - - request[KEY_HASS_USER] = user - return True - @middleware async def auth_middleware(request, handler): """Authenticate as middleware.""" authenticated = False - if HTTP_HEADER_HA_AUTH in request.headers or DATA_API_PASSWORD in request.query: - if request.path not in old_auth_warning: - _LOGGER.log( - logging.INFO if support_legacy else logging.WARNING, - "api_password is going to deprecate. You need to use a" - " bearer token to access %s from %s", - request.path, - request[KEY_REAL_IP], - ) - old_auth_warning.add(request.path) - if hdrs.AUTHORIZATION in request.headers and await async_validate_auth_header( request ): - # it included both use_auth and api_password Basic auth authenticated = True + auth_type = "bearer token" # We first start with a string check to avoid parsing query params # for every request. @@ -193,39 +122,15 @@ def setup_auth(hass, app): and await async_validate_signed_request(request) ): authenticated = True + auth_type = "signed request" - elif trusted_networks and await async_validate_trusted_networks(request): - if request.path not in old_auth_warning: - # When removing this, don't forget to remove the print logic - # in http/view.py - request["deprecate_warning_message"] = ( - "Access from trusted networks without auth token is " - "going to be removed in Home Assistant 0.96. Configure " - "the trusted networks auth provider or use long-lived " - "access tokens to access {} from {}".format( - request.path, request[KEY_REAL_IP] - ) - ) - old_auth_warning.add(request.path) - authenticated = True - - elif ( - support_legacy - and HTTP_HEADER_HA_AUTH in request.headers - and await async_validate_legacy_api_password( - request, request.headers[HTTP_HEADER_HA_AUTH] + if authenticated: + _LOGGER.debug( + "Authenticated %s for %s using %s", + request[KEY_REAL_IP], + request.path, + auth_type, ) - ): - authenticated = True - - elif ( - support_legacy - and DATA_API_PASSWORD in request.query - and await async_validate_legacy_api_password( - request, request.query[DATA_API_PASSWORD] - ) - ): - authenticated = True request[KEY_AUTHENTICATED] = authenticated return await handler(request) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index bd821335542..0e6b9f9439a 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -3,7 +3,7 @@ import aiohttp_cors from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION -from homeassistant.const import HTTP_HEADER_HA_AUTH, HTTP_HEADER_X_REQUESTED_WITH +from homeassistant.const import HTTP_HEADER_X_REQUESTED_WITH from homeassistant.core import callback @@ -14,7 +14,6 @@ ALLOWED_CORS_HEADERS = [ ACCEPT, HTTP_HEADER_X_REQUESTED_WITH, CONTENT_TYPE, - HTTP_HEADER_HA_AUTH, AUTHORIZATION, ] VALID_CORS_TYPES = (Resource, ResourceRoute, StaticResource) diff --git a/homeassistant/components/http/view.py b/homeassistant/components/http/view.py index 66864eba55e..804c90d4f96 100644 --- a/homeassistant/components/http/view.py +++ b/homeassistant/components/http/view.py @@ -17,7 +17,6 @@ from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import Context, is_callback from homeassistant.helpers.json import JSONEncoder -from .ban import process_success_login from .const import KEY_AUTHENTICATED, KEY_HASS, KEY_REAL_IP _LOGGER = logging.getLogger(__name__) @@ -106,13 +105,8 @@ def request_handler_factory(view, handler): authenticated = request.get(KEY_AUTHENTICATED, False) - if view.requires_auth: - if authenticated: - if "deprecate_warning_message" in request: - _LOGGER.warning(request["deprecate_warning_message"]) - await process_success_login(request) - else: - raise HTTPUnauthorized() + if view.requires_auth and not authenticated: + raise HTTPUnauthorized() _LOGGER.debug( "Serving %s to %s (auth: %s)", diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 716b20f4ca4..3971d39ee73 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -3,7 +3,6 @@ import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.auth.models import RefreshToken, User -from homeassistant.auth.providers import legacy_api_password from homeassistant.components.http.ban import process_wrong_login, process_success_login from homeassistant.const import __version__ @@ -74,19 +73,6 @@ class AuthPhase: if refresh_token is not None: return await self._async_finish_auth(refresh_token.user, refresh_token) - elif self._hass.auth.support_legacy and "api_password" in msg: - self._logger.info( - "Received api_password, it is going to deprecate, please use" - " access_token instead. For instructions, see https://" - "developers.home-assistant.io/docs/en/external_api_websocket" - ".html#authentication-phase" - ) - user = await legacy_api_password.async_validate_password( - self._hass, msg["api_password"] - ) - if user is not None: - return await self._async_finish_auth(user, None) - self._send_message(auth_invalid_message("Invalid access token or password")) await process_wrong_login(self._request) raise Disconnect diff --git a/homeassistant/config.py b/homeassistant/config.py index 97c996d9e59..27137c08f1a 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -468,12 +468,7 @@ def _format_config_error(ex: Exception, domain: str, config: Dict) -> str: return message -async def async_process_ha_core_config( - hass: HomeAssistant, - config: Dict, - api_password: Optional[str] = None, - trusted_networks: Optional[Any] = None, -) -> None: +async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> None: """Process the [homeassistant] section from the configuration. This method is a coroutine. @@ -486,14 +481,6 @@ async def async_process_ha_core_config( if auth_conf is None: auth_conf = [{"type": "homeassistant"}] - if api_password: - auth_conf.append( - {"type": "legacy_api_password", "api_password": api_password} - ) - if trusted_networks: - auth_conf.append( - {"type": "trusted_networks", "trusted_networks": trusted_networks} - ) mfa_conf = config.get( CONF_AUTH_MFA_MODULES, diff --git a/homeassistant/const.py b/homeassistant/const.py index e0f90834d94..592f6b60bc6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -451,7 +451,6 @@ HTTP_SERVICE_UNAVAILABLE = 503 HTTP_BASIC_AUTHENTICATION = "basic" HTTP_DIGEST_AUTHENTICATION = "digest" -HTTP_HEADER_HA_AUTH = "X-HA-access" HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With" CONTENT_TYPE_JSON = "application/json" diff --git a/tests/components/auth/__init__.py b/tests/components/auth/__init__.py index e79d8a67845..5114e18889b 100644 --- a/tests/components/auth/__init__.py +++ b/tests/components/auth/__init__.py @@ -30,7 +30,7 @@ async def async_setup_auth( hass, provider_configs, module_configs ) ensure_auth_manager_loaded(hass.auth) - await async_setup_component(hass, "auth", {"http": {"api_password": "bla"}}) + await async_setup_component(hass, "auth", {}) if setup_api: await async_setup_component(hass, "api", {}) return await aiohttp_client(hass.http.app) diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 80527c2636b..de91613b74b 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -103,7 +103,7 @@ def test_auth_code_store_expiration(): async def test_ws_current_user(hass, hass_ws_client, hass_access_token): """Test the current user command with homeassistant creds.""" - assert await async_setup_component(hass, "auth", {"http": {"api_password": "bla"}}) + assert await async_setup_component(hass, "auth", {}) refresh_token = await hass.auth.async_validate_access_token(hass_access_token) user = refresh_token.user diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 8c2515939f3..4f1f3e64e02 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -40,7 +40,9 @@ def hass_ws_client(aiohttp_client, hass_access_token): assert auth_resp["type"] == TYPE_AUTH_REQUIRED if access_token is None: - await websocket.send_json({"type": TYPE_AUTH, "api_password": "bla"}) + await websocket.send_json( + {"type": TYPE_AUTH, "access_token": "incorrect"} + ) else: await websocket.send_json( {"type": TYPE_AUTH, "access_token": access_token} diff --git a/tests/components/google_assistant/test_google_assistant.py b/tests/components/google_assistant/test_google_assistant.py index 6473e8964b8..b43e913ab27 100644 --- a/tests/components/google_assistant/test_google_assistant.py +++ b/tests/components/google_assistant/test_google_assistant.py @@ -3,7 +3,7 @@ import asyncio import json -from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION +from aiohttp.hdrs import AUTHORIZATION import pytest from homeassistant import core, const, setup @@ -24,11 +24,6 @@ from . import DEMO_DEVICES API_PASSWORD = "test1234" -HA_HEADERS = { - const.HTTP_HEADER_HA_AUTH: API_PASSWORD, - CONTENT_TYPE: const.CONTENT_TYPE_JSON, -} - PROJECT_ID = "hasstest-1234" CLIENT_ID = "helloworld" ACCESS_TOKEN = "superdoublesecret" diff --git a/tests/components/hassio/__init__.py b/tests/components/hassio/__init__.py index 8e2b6db777d..767ec59f366 100644 --- a/tests/components/hassio/__init__.py +++ b/tests/components/hassio/__init__.py @@ -1,4 +1,3 @@ """Tests for Hassio component.""" -API_PASSWORD = "pass1234" HASSIO_TOKEN = "123456" diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index d7d50b97eb8..0e246cf1b46 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -9,7 +9,7 @@ from homeassistant.setup import async_setup_component from homeassistant.components.hassio.handler import HassIO, HassioAPIError from tests.common import mock_coro -from . import API_PASSWORD, HASSIO_TOKEN +from . import HASSIO_TOKEN @pytest.fixture @@ -39,23 +39,19 @@ def hassio_stubs(hassio_env, hass, hass_client, aioclient_mock): side_effect=HassioAPIError(), ): hass.state = CoreState.starting - hass.loop.run_until_complete( - async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) - ) + hass.loop.run_until_complete(async_setup_component(hass, "hassio", {})) @pytest.fixture def hassio_client(hassio_stubs, hass, hass_client): """Return a Hass.io HTTP client.""" - yield hass.loop.run_until_complete(hass_client()) + return hass.loop.run_until_complete(hass_client()) @pytest.fixture def hassio_noauth_client(hassio_stubs, hass, aiohttp_client): """Return a Hass.io HTTP client without auth.""" - yield hass.loop.run_until_complete(aiohttp_client(hass.http.app)) + return hass.loop.run_until_complete(aiohttp_client(hass.http.app)) @pytest.fixture diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index 114935df3fc..480df508968 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -4,10 +4,8 @@ from unittest.mock import patch, Mock import pytest from homeassistant.setup import async_setup_component -from homeassistant.const import HTTP_HEADER_HA_AUTH from tests.common import mock_coro -from . import API_PASSWORD @pytest.fixture(autouse=True) @@ -53,9 +51,7 @@ async def test_hassio_addon_panel_startup(hass, aioclient_mock, hassio_env): "homeassistant.components.hassio.addon_panel._register_panel", Mock(return_value=mock_coro()), ) as mock_panel: - await async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) + await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() assert aioclient_mock.call_count == 3 @@ -98,9 +94,7 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli "homeassistant.components.hassio.addon_panel._register_panel", Mock(return_value=mock_coro()), ) as mock_panel: - await async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) + await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() assert aioclient_mock.call_count == 3 @@ -113,14 +107,10 @@ async def test_hassio_addon_panel_api(hass, aioclient_mock, hassio_env, hass_cli hass_client = await hass_client() - resp = await hass_client.post( - "/api/hassio_push/panel/test2", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = await hass_client.post("/api/hassio_push/panel/test2") assert resp.status == 400 - resp = await hass_client.post( - "/api/hassio_push/panel/test1", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = await hass_client.post("/api/hassio_push/panel/test1") assert resp.status == 200 assert mock_panel.call_count == 2 diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index a2839b297b8..1fb6d32ccf7 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,11 +1,9 @@ """The tests for the hassio component.""" from unittest.mock import patch, Mock -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.exceptions import HomeAssistantError from tests.common import mock_coro -from . import API_PASSWORD async def test_login_success(hass, hassio_client): @@ -18,7 +16,6 @@ async def test_login_success(hass, hassio_client): resp = await hassio_client.post( "/api/hassio_auth", json={"username": "test", "password": "123456", "addon": "samba"}, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, ) # Check we got right response @@ -36,7 +33,6 @@ async def test_login_error(hass, hassio_client): resp = await hassio_client.post( "/api/hassio_auth", json={"username": "test", "password": "123456", "addon": "samba"}, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, ) # Check we got right response @@ -51,9 +47,7 @@ async def test_login_no_data(hass, hassio_client): "HassAuthProvider.async_validate_login", Mock(side_effect=HomeAssistantError()), ) as mock_login: - resp = await hassio_client.post( - "/api/hassio_auth", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = await hassio_client.post("/api/hassio_auth") # Check we got right response assert resp.status == 400 @@ -68,9 +62,7 @@ async def test_login_no_username(hass, hassio_client): Mock(side_effect=HomeAssistantError()), ) as mock_login: resp = await hassio_client.post( - "/api/hassio_auth", - json={"password": "123456", "addon": "samba"}, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, + "/api/hassio_auth", json={"password": "123456", "addon": "samba"} ) # Check we got right response @@ -93,7 +85,6 @@ async def test_login_success_extra(hass, hassio_client): "addon": "samba", "path": "/share", }, - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, ) # Check we got right response diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 89f1483ffab..a1b4ae2e900 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -3,10 +3,9 @@ from unittest.mock import patch, Mock from homeassistant.setup import async_setup_component from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.const import EVENT_HOMEASSISTANT_START, HTTP_HEADER_HA_AUTH +from homeassistant.const import EVENT_HOMEASSISTANT_START from tests.common import mock_coro -from . import API_PASSWORD async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): @@ -101,9 +100,7 @@ async def test_hassio_discovery_startup_done(hass, aioclient_mock, hassio_client Mock(return_value=mock_coro({"type": "abort"})), ) as mock_mqtt: await hass.async_start() - await async_setup_component( - hass, "hassio", {"http": {"api_password": API_PASSWORD}} - ) + await async_setup_component(hass, "hassio", {}) await hass.async_block_till_done() assert aioclient_mock.call_count == 2 @@ -151,7 +148,6 @@ async def test_hassio_discovery_webhook(hass, aioclient_mock, hassio_client): ) as mock_mqtt: resp = await hassio_client.post( "/api/hassio_push/discovery/testuuid", - headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}, json={"addon": "mosquitto", "service": "mqtt", "uuid": "testuuid"}, ) await hass.async_block_till_done() diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 8f77d6b6234..96d53f93c3a 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -4,19 +4,13 @@ from unittest.mock import patch import pytest -from homeassistant.const import HTTP_HEADER_HA_AUTH - -from . import API_PASSWORD - @asyncio.coroutine def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" aioclient_mock.post("http://127.0.0.1/beer", text="response") - resp = yield from hassio_client.post( - "/api/hassio/beer", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = yield from hassio_client.post("/api/hassio/beer") # Check we got right response assert resp.status == 200 @@ -87,9 +81,7 @@ def test_forward_log_request(hassio_client, aioclient_mock): """Test fetching normal log path doesn't remove ANSI color escape codes.""" aioclient_mock.get("http://127.0.0.1/beer/logs", text="\033[32mresponse\033[0m") - resp = yield from hassio_client.get( - "/api/hassio/beer/logs", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = yield from hassio_client.get("/api/hassio/beer/logs") # Check we got right response assert resp.status == 200 @@ -107,9 +99,7 @@ def test_bad_gateway_when_cannot_find_supervisor(hassio_client): "homeassistant.components.hassio.http.async_timeout.timeout", side_effect=asyncio.TimeoutError, ): - resp = yield from hassio_client.get( - "/api/hassio/addons/test/info", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD} - ) + resp = yield from hassio_client.get("/api/hassio/addons/test/info") assert resp.status == 502 diff --git a/tests/components/http/__init__.py b/tests/components/http/__init__.py index c4f73fd15a6..db5e1ea5c7a 100644 --- a/tests/components/http/__init__.py +++ b/tests/components/http/__init__.py @@ -6,6 +6,10 @@ from aiohttp import web from homeassistant.components.http.const import KEY_REAL_IP +# Relic from the past. Kept here so we can run negative tests. +HTTP_HEADER_HA_AUTH = "X-HA-access" + + def mock_real_ip(app): """Inject middleware to mock real IP. diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 842201beace..499ceab1556 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -11,10 +11,8 @@ from homeassistant.auth.providers import trusted_networks from homeassistant.components.http.auth import setup_auth, async_sign_path from homeassistant.components.http.const import KEY_AUTHENTICATED from homeassistant.components.http.real_ip import setup_real_ip -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.setup import async_setup_component -from . import mock_real_ip - +from . import mock_real_ip, HTTP_HEADER_HA_AUTH API_PASSWORD = "test-password" @@ -87,29 +85,29 @@ async def test_auth_middleware_loaded_by_default(hass): assert len(mock_setup.mock_calls) == 1 -async def test_access_with_password_in_header(app, aiohttp_client, legacy_auth, hass): +async def test_cant_access_with_password_in_header( + app, aiohttp_client, legacy_auth, hass +): """Test access with password in header.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: "wrong-pass"}) assert req.status == 401 -async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, hass): +async def test_cant_access_with_password_in_query( + app, aiohttp_client, legacy_auth, hass +): """Test access with password in URL.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) resp = await client.get("/", params={"api_password": API_PASSWORD}) - assert resp.status == 200 - assert await resp.json() == {"user_id": user.id} + assert resp.status == 401 resp = await client.get("/") assert resp.status == 401 @@ -118,15 +116,13 @@ async def test_access_with_password_in_query(app, aiohttp_client, legacy_auth, h assert resp.status == 401 -async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth): +async def test_basic_auth_does_not_work(app, aiohttp_client, hass, legacy_auth): """Test access with basic authentication.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD)) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 req = await client.get("/", auth=BasicAuth("wrong_username", API_PASSWORD)) assert req.status == 401 @@ -138,7 +134,7 @@ async def test_basic_auth_works(app, aiohttp_client, hass, legacy_auth): assert req.status == 401 -async def test_access_with_trusted_ip( +async def test_cannot_access_with_trusted_ip( hass, app2, trusted_networks_auth, aiohttp_client, hass_owner_user ): """Test access with an untrusted ip address.""" @@ -155,8 +151,7 @@ async def test_access_with_trusted_ip( for remote_addr in TRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert resp.status == 200, "{} should be trusted".format(remote_addr) - assert await resp.json() == {"user_id": hass_owner_user.id} + assert resp.status == 401, "{} shouldn't be trusted".format(remote_addr) async def test_auth_active_access_with_access_token_in_header( @@ -209,29 +204,24 @@ async def test_auth_active_access_with_trusted_ip( for remote_addr in TRUSTED_ADDRESSES: set_mock_ip(remote_addr) resp = await client.get("/") - assert resp.status == 200, "{} should be trusted".format(remote_addr) - assert await resp.json() == {"user_id": hass_owner_user.id} + assert resp.status == 401, "{} shouldn't be trusted".format(remote_addr) -async def test_auth_legacy_support_api_password_access( +async def test_auth_legacy_support_api_password_cannot_access( app, aiohttp_client, legacy_auth, hass ): """Test access using api_password if auth.support_legacy.""" setup_auth(hass, app) client = await aiohttp_client(app) - user = await get_legacy_user(hass.auth) req = await client.get("/", headers={HTTP_HEADER_HA_AUTH: API_PASSWORD}) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 resp = await client.get("/", params={"api_password": API_PASSWORD}) - assert resp.status == 200 - assert await resp.json() == {"user_id": user.id} + assert resp.status == 401 req = await client.get("/", auth=BasicAuth("homeassistant", API_PASSWORD)) - assert req.status == 200 - assert await req.json() == {"user_id": user.id} + assert req.status == 401 async def test_auth_access_signed_path(hass, app, aiohttp_client, hass_access_token): diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index fc31de4b950..f50afcef8a8 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -148,6 +148,8 @@ async def test_failed_login_attempts_counter(hass, aiohttp_client): assert resp.status == 200 assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2 + # This used to check that with trusted networks we reset login attempts + # We no longer support trusted networks. resp = await client.get("/auth_true") assert resp.status == 200 - assert remote_ip not in app[KEY_FAILED_LOGIN_ATTEMPTS] + assert app[KEY_FAILED_LOGIN_ATTEMPTS][remote_ip] == 2 diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 99b9e0b6e9a..1cea900d971 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -13,11 +13,12 @@ from aiohttp.hdrs import ( ) import pytest -from homeassistant.const import HTTP_HEADER_HA_AUTH from homeassistant.setup import async_setup_component from homeassistant.components.http.cors import setup_cors from homeassistant.components.http.view import HomeAssistantView +from . import HTTP_HEADER_HA_AUTH + TRUSTED_ORIGIN = "https://home-assistant.io" @@ -91,13 +92,13 @@ async def test_cors_preflight_allowed(client): headers={ ORIGIN: TRUSTED_ORIGIN, ACCESS_CONTROL_REQUEST_METHOD: "GET", - ACCESS_CONTROL_REQUEST_HEADERS: "x-ha-access", + ACCESS_CONTROL_REQUEST_HEADERS: "x-requested-with", }, ) assert req.status == 200 assert req.headers[ACCESS_CONTROL_ALLOW_ORIGIN] == TRUSTED_ORIGIN - assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == HTTP_HEADER_HA_AUTH.upper() + assert req.headers[ACCESS_CONTROL_ALLOW_HEADERS] == "X-REQUESTED-WITH" async def test_cors_middleware_with_cors_allowed_view(hass): diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index d8e613df6df..ad8e3ac10fd 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -133,7 +133,7 @@ async def test_not_log_password(hass, aiohttp_client, caplog, legacy_auth): resp = await client.get("/api/", params={"api_password": "test-password"}) - assert resp.status == 200 + assert resp.status == 401 logs = caplog.text # Ensure we don't log API passwords diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index c210d773faf..3627c95040e 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -57,12 +57,7 @@ class TestMQTT: self.hass.config.api = MagicMock(api_password="api_password") assert setup_component( - self.hass, - mqtt.DOMAIN, - { - "http": {"api_password": "http_secret"}, - mqtt.DOMAIN: {CONF_PASSWORD: password}, - }, + self.hass, mqtt.DOMAIN, {mqtt.DOMAIN: {CONF_PASSWORD: password}} ) self.hass.block_till_done() assert mock_mqtt.called diff --git a/tests/components/websocket_api/__init__.py b/tests/components/websocket_api/__init__.py index 56def1b7fd9..4904270cc72 100644 --- a/tests/components/websocket_api/__init__.py +++ b/tests/components/websocket_api/__init__.py @@ -1,2 +1 @@ """Tests for the websocket API.""" -API_PASSWORD = "test-password" diff --git a/tests/components/websocket_api/conftest.py b/tests/components/websocket_api/conftest.py index 2ee28c0cb20..382de3142e8 100644 --- a/tests/components/websocket_api/conftest.py +++ b/tests/components/websocket_api/conftest.py @@ -5,8 +5,6 @@ from homeassistant.setup import async_setup_component from homeassistant.components.websocket_api.http import URL from homeassistant.components.websocket_api.auth import TYPE_AUTH_REQUIRED -from . import API_PASSWORD - @pytest.fixture def websocket_client(hass, hass_ws_client, hass_access_token): @@ -17,11 +15,7 @@ def websocket_client(hass, hass_ws_client, hass_access_token): @pytest.fixture def no_auth_websocket_client(hass, loop, aiohttp_client): """Websocket connection that requires authentication.""" - assert loop.run_until_complete( - async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) - ) + assert loop.run_until_complete(async_setup_component(hass, "websocket_api", {})) client = loop.run_until_complete(aiohttp_client(hass.http.app)) ws = loop.run_until_complete(client.ws_connect(URL)) diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 19b9cbb2196..00387506020 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -17,21 +17,10 @@ from homeassistant.setup import async_setup_component from tests.common import mock_coro -from . import API_PASSWORD - -async def test_auth_via_msg(no_auth_websocket_client, legacy_auth): - """Test authenticating.""" - await no_auth_websocket_client.send_json( - {"type": TYPE_AUTH, "api_password": API_PASSWORD} - ) - - msg = await no_auth_websocket_client.receive_json() - - assert msg["type"] == TYPE_AUTH_OK - - -async def test_auth_events(hass, no_auth_websocket_client, legacy_auth): +async def test_auth_events( + hass, no_auth_websocket_client, legacy_auth, hass_access_token +): """Test authenticating.""" connected_evt = [] hass.helpers.dispatcher.async_dispatcher_connect( @@ -42,7 +31,7 @@ async def test_auth_events(hass, no_auth_websocket_client, legacy_auth): SIGNAL_WEBSOCKET_DISCONNECTED, lambda: disconnected_evt.append(1) ) - await test_auth_via_msg(no_auth_websocket_client, legacy_auth) + await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token) assert len(connected_evt) == 1 assert not disconnected_evt @@ -60,7 +49,7 @@ async def test_auth_via_msg_incorrect_pass(no_auth_websocket_client): return_value=mock_coro(), ) as mock_process_wrong_login: await no_auth_websocket_client.send_json( - {"type": TYPE_AUTH, "api_password": API_PASSWORD + "wrong"} + {"type": TYPE_AUTH, "api_password": "wrong"} ) msg = await no_auth_websocket_client.receive_json() @@ -110,31 +99,25 @@ async def test_pre_auth_only_auth_allowed(no_auth_websocket_client): assert msg["message"].startswith("Auth message incorrectly formatted") -async def test_auth_active_with_token(hass, aiohttp_client, hass_access_token): +async def test_auth_active_with_token( + hass, no_auth_websocket_client, hass_access_token +): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} + assert await async_setup_component(hass, "websocket_api", {}) + + await no_auth_websocket_client.send_json( + {"type": TYPE_AUTH, "access_token": hass_access_token} ) - client = await aiohttp_client(hass.http.app) - - async with client.ws_connect(URL) as ws: - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_REQUIRED - - await ws.send_json({"type": TYPE_AUTH, "access_token": hass_access_token}) - - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_OK + auth_msg = await no_auth_websocket_client.receive_json() + assert auth_msg["type"] == TYPE_AUTH_OK async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token): """Test authenticating with a token.""" refresh_token = await hass.auth.async_validate_access_token(hass_access_token) refresh_token.user.is_active = False - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) @@ -150,9 +133,7 @@ async def test_auth_active_user_inactive(hass, aiohttp_client, hass_access_token async def test_auth_active_with_password_not_allow(hass, aiohttp_client): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) @@ -160,7 +141,7 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client): auth_msg = await ws.receive_json() assert auth_msg["type"] == TYPE_AUTH_REQUIRED - await ws.send_json({"type": TYPE_AUTH, "api_password": API_PASSWORD}) + await ws.send_json({"type": TYPE_AUTH, "api_password": "some-password"}) auth_msg = await ws.receive_json() assert auth_msg["type"] == TYPE_AUTH_INVALID @@ -168,28 +149,23 @@ async def test_auth_active_with_password_not_allow(hass, aiohttp_client): async def test_auth_legacy_support_with_password(hass, aiohttp_client, legacy_auth): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) async with client.ws_connect(URL) as ws: - with patch("homeassistant.auth.AuthManager.support_legacy", return_value=True): - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_REQUIRED + auth_msg = await ws.receive_json() + assert auth_msg["type"] == TYPE_AUTH_REQUIRED - await ws.send_json({"type": TYPE_AUTH, "api_password": API_PASSWORD}) + await ws.send_json({"type": TYPE_AUTH, "api_password": "some-password"}) - auth_msg = await ws.receive_json() - assert auth_msg["type"] == TYPE_AUTH_OK + auth_msg = await ws.receive_json() + assert auth_msg["type"] == TYPE_AUTH_INVALID async def test_auth_with_invalid_token(hass, aiohttp_client): """Test authenticating with a token.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) client = await aiohttp_client(hass.http.app) diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index a39a0a0e7a6..1de5b8bb2c1 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -14,8 +14,6 @@ from homeassistant.setup import async_setup_component from tests.common import async_mock_service -from . import API_PASSWORD - async def test_call_service(hass, websocket_client): """Test call service command.""" @@ -250,9 +248,7 @@ async def test_ping(websocket_client): async def test_call_service_context_with_user(hass, aiohttp_client, hass_access_token): """Test that the user is set in the service call context.""" - assert await async_setup_component( - hass, "websocket_api", {"http": {"api_password": API_PASSWORD}} - ) + assert await async_setup_component(hass, "websocket_api", {}) calls = async_mock_service(hass, "domain_test", "test_service") client = await aiohttp_client(hass.http.app) diff --git a/tests/components/websocket_api/test_sensor.py b/tests/components/websocket_api/test_sensor.py index 873b9e7269c..84b73060698 100644 --- a/tests/components/websocket_api/test_sensor.py +++ b/tests/components/websocket_api/test_sensor.py @@ -3,10 +3,12 @@ from homeassistant.bootstrap import async_setup_component from tests.common import assert_setup_component -from .test_auth import test_auth_via_msg +from .test_auth import test_auth_active_with_token -async def test_websocket_api(hass, no_auth_websocket_client, legacy_auth): +async def test_websocket_api( + hass, no_auth_websocket_client, hass_access_token, legacy_auth +): """Test API streams.""" with assert_setup_component(1): await async_setup_component( @@ -16,7 +18,7 @@ async def test_websocket_api(hass, no_auth_websocket_client, legacy_auth): state = hass.states.get("sensor.connected_clients") assert state.state == "0" - await test_auth_via_msg(no_auth_websocket_client, legacy_auth) + await test_auth_active_with_token(hass, no_auth_websocket_client, hass_access_token) state = hass.states.get("sensor.connected_clients") assert state.state == "1" diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 18143c088be..5199f01807f 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -92,8 +92,8 @@ def test_secrets(isfile_patch, loop): files = { get_test_config_dir(YAML_CONFIG_FILE): BASE_CONFIG - + ("http:\n" " api_password: !secret http_pw"), - secrets_path: ("logger: debug\n" "http_pw: abc123"), + + ("http:\n" " cors_allowed_origins: !secret http_pw"), + secrets_path: ("logger: debug\n" "http_pw: http://google.com"), } with patch_yaml_files(files): @@ -103,17 +103,15 @@ def test_secrets(isfile_patch, loop): assert res["except"] == {} assert res["components"].keys() == {"homeassistant", "http"} assert res["components"]["http"] == { - "api_password": "abc123", - "cors_allowed_origins": ["https://cast.home-assistant.io"], + "cors_allowed_origins": ["http://google.com"], "ip_ban_enabled": True, "login_attempts_threshold": -1, "server_host": "0.0.0.0", "server_port": 8123, - "trusted_networks": [], "ssl_profile": "modern", } - assert res["secret_cache"] == {secrets_path: {"http_pw": "abc123"}} - assert res["secrets"] == {"http_pw": "abc123"} + assert res["secret_cache"] == {secrets_path: {"http_pw": "http://google.com"}} + assert res["secrets"] == {"http_pw": "http://google.com"} assert normalize_yaml_files(res) == [ ".../configuration.yaml", ".../secrets.yaml", diff --git a/tests/test_config.py b/tests/test_config.py index a67cd345797..362608e7af2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,7 +5,6 @@ import copy import os import unittest.mock as mock from collections import OrderedDict -from ipaddress import ip_network import asynctest import pytest @@ -876,48 +875,6 @@ async def test_auth_provider_config_default(hass): assert hass.auth.auth_mfa_modules[0].id == "totp" -async def test_auth_provider_config_default_api_password(hass): - """Test loading default auth provider config with api password.""" - core_config = { - "latitude": 60, - "longitude": 50, - "elevation": 25, - "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, - "time_zone": "GMT", - } - if hasattr(hass, "auth"): - del hass.auth - await config_util.async_process_ha_core_config(hass, core_config, "pass") - - assert len(hass.auth.auth_providers) == 2 - assert hass.auth.auth_providers[0].type == "homeassistant" - assert hass.auth.auth_providers[1].type == "legacy_api_password" - assert hass.auth.auth_providers[1].api_password == "pass" - - -async def test_auth_provider_config_default_trusted_networks(hass): - """Test loading default auth provider config with trusted networks.""" - core_config = { - "latitude": 60, - "longitude": 50, - "elevation": 25, - "name": "Huis", - CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_IMPERIAL, - "time_zone": "GMT", - } - if hasattr(hass, "auth"): - del hass.auth - await config_util.async_process_ha_core_config( - hass, core_config, trusted_networks=["192.168.0.1"] - ) - - assert len(hass.auth.auth_providers) == 2 - assert hass.auth.auth_providers[0].type == "homeassistant" - assert hass.auth.auth_providers[1].type == "trusted_networks" - assert hass.auth.auth_providers[1].trusted_networks[0] == ip_network("192.168.0.1") - - async def test_disallowed_auth_provider_config(hass): """Test loading insecure example auth provider is disallowed.""" core_config = { From 3cb844f22c46bdcaf177ab2fcdc4852f3568c98d Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Mon, 14 Oct 2019 18:53:59 -0400 Subject: [PATCH 292/639] Add Apprise notification integration (#26868) * Added apprise notification component * flake-8 fixes; black formatting + import merged to 1 line * pylint issues resolved * added github name to manifest.json * import moved to top as per code review request * manifest formatting to avoid failing ci * .coveragerc updated to include apprise * removed block for written tests * more test coverage * formatting as per code review * tests converted to async style as per code review * increased coverage * bumped version of apprise to 0.8.1 * test that mocked entries are called * added tests for hass.service loading * support tags for those who identify the TARGET option * renamed variable as per code review * 'assert not' used instead of 'is False' * added period (in case linter isn't happy) --- CODEOWNERS | 1 + homeassistant/components/apprise/__init__.py | 1 + .../components/apprise/manifest.json | 12 ++ homeassistant/components/apprise/notify.py | 73 +++++++++ requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/apprise/__init__.py | 1 + tests/components/apprise/test_notify.py | 148 ++++++++++++++++++ 8 files changed, 242 insertions(+) create mode 100644 homeassistant/components/apprise/__init__.py create mode 100644 homeassistant/components/apprise/manifest.json create mode 100644 homeassistant/components/apprise/notify.py create mode 100644 tests/components/apprise/__init__.py create mode 100644 tests/components/apprise/test_notify.py diff --git a/CODEOWNERS b/CODEOWNERS index ea50d24095c..8e52210cec7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -26,6 +26,7 @@ homeassistant/components/ambient_station/* @bachya homeassistant/components/androidtv/* @JeffLIrion homeassistant/components/apache_kafka/* @bachya homeassistant/components/api/* @home-assistant/core +homeassistant/components/apprise/* @caronc homeassistant/components/aprs/* @PhilRW homeassistant/components/arcam_fmj/* @elupus homeassistant/components/arduino/* @fabaff diff --git a/homeassistant/components/apprise/__init__.py b/homeassistant/components/apprise/__init__.py new file mode 100644 index 00000000000..6ffdaf690d9 --- /dev/null +++ b/homeassistant/components/apprise/__init__.py @@ -0,0 +1 @@ +"""The apprise component.""" diff --git a/homeassistant/components/apprise/manifest.json b/homeassistant/components/apprise/manifest.json new file mode 100644 index 00000000000..3e971a96e7e --- /dev/null +++ b/homeassistant/components/apprise/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "apprise", + "name": "Apprise", + "documentation": "https://www.home-assistant.io/components/apprise", + "requirements": [ + "apprise==0.8.1" + ], + "dependencies": [], + "codeowners": [ + "@caronc" + ] +} diff --git a/homeassistant/components/apprise/notify.py b/homeassistant/components/apprise/notify.py new file mode 100644 index 00000000000..662cc9c1ab6 --- /dev/null +++ b/homeassistant/components/apprise/notify.py @@ -0,0 +1,73 @@ +"""Apprise platform for notify component.""" +import logging + +import voluptuous as vol + +import apprise + +import homeassistant.helpers.config_validation as cv + +from homeassistant.components.notify import ( + ATTR_TARGET, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) + +_LOGGER = logging.getLogger(__name__) + +CONF_FILE = "config" +CONF_URL = "url" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_URL): vol.All(cv.ensure_list, [str]), + vol.Optional(CONF_FILE): cv.string, + } +) + + +def get_service(hass, config, discovery_info=None): + """Get the Apprise notification service.""" + + # Create our object + a_obj = apprise.Apprise() + + if config.get(CONF_FILE): + # Sourced from a Configuration File + a_config = apprise.AppriseConfig() + if not a_config.add(config[CONF_FILE]): + _LOGGER.error("Invalid Apprise config url provided") + return None + + if not a_obj.add(a_config): + _LOGGER.error("Invalid Apprise config url provided") + return None + + if config.get(CONF_URL): + # Ordered list of URLs + if not a_obj.add(config[CONF_URL]): + _LOGGER.error("Invalid Apprise URL(s) supplied") + return None + + return AppriseNotificationService(a_obj) + + +class AppriseNotificationService(BaseNotificationService): + """Implement the notification service for Apprise.""" + + def __init__(self, a_obj): + """Initialize the service.""" + self.apprise = a_obj + + def send_message(self, message="", **kwargs): + """Send a message to a specified target. + + If no target/tags are specified, then services are notified as is + However, if any tags are specified, then they will be applied + to the notification causing filtering (if set up that way). + """ + targets = kwargs.get(ATTR_TARGET) + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + self.apprise.notify(body=message, title=title, tag=targets) diff --git a/requirements_all.txt b/requirements_all.txt index dbc94de9a1c..ef07a3f44b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -217,6 +217,9 @@ apcaccess==0.0.13 # homeassistant.components.apns apns2==0.3.0 +# homeassistant.components.apprise +apprise==0.8.1 + # homeassistant.components.aprs aprslib==0.6.46 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18970fcbac0..967943894fb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -103,6 +103,9 @@ androidtv==0.0.30 # homeassistant.components.apns apns2==0.3.0 +# homeassistant.components.apprise +apprise==0.8.1 + # homeassistant.components.aprs aprslib==0.6.46 diff --git a/tests/components/apprise/__init__.py b/tests/components/apprise/__init__.py new file mode 100644 index 00000000000..ffebc35b4e1 --- /dev/null +++ b/tests/components/apprise/__init__.py @@ -0,0 +1 @@ +"""Tests for the apprise component.""" diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py new file mode 100644 index 00000000000..237f99de676 --- /dev/null +++ b/tests/components/apprise/test_notify.py @@ -0,0 +1,148 @@ +"""The tests for the apprise notification platform.""" +from unittest.mock import patch +from unittest.mock import MagicMock + +from homeassistant.setup import async_setup_component + +BASE_COMPONENT = "notify" + + +async def test_apprise_config_load_fail01(hass): + """Test apprise configuration failures 1.""" + + config = { + BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"} + } + + with patch("apprise.AppriseConfig.add", return_value=False): + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test that our service failed to load + assert not hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_config_load_fail02(hass): + """Test apprise configuration failures 2.""" + + config = { + BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": "/path/"} + } + + with patch("apprise.Apprise.add", return_value=False): + with patch("apprise.AppriseConfig.add", return_value=True): + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test that our service failed to load + assert not hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_config_load_okay(hass, tmp_path): + """Test apprise configuration failures.""" + + # Test cases where our URL is invalid + d = tmp_path / "apprise-config" + d.mkdir() + f = d / "apprise" + f.write_text("mailto://user:pass@example.com/") + + config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}} + + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Valid configuration was loaded; our service is good + assert hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_url_load_fail(hass): + """Test apprise url failure.""" + + config = { + BASE_COMPONENT: { + "name": "test", + "platform": "apprise", + "url": "mailto://user:pass@example.com", + } + } + with patch("apprise.Apprise.add", return_value=False): + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test that our service failed to load + assert not hass.services.has_service(BASE_COMPONENT, "test") + + +async def test_apprise_notification(hass): + """Test apprise notification.""" + + config = { + BASE_COMPONENT: { + "name": "test", + "platform": "apprise", + "url": "mailto://user:pass@example.com", + } + } + + # Our Message + data = {"title": "Test Title", "message": "Test Message"} + + with patch("apprise.Apprise") as mock_apprise: + obj = MagicMock() + obj.add.return_value = True + obj.notify.return_value = True + mock_apprise.return_value = obj + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test the existance of our service + assert hass.services.has_service(BASE_COMPONENT, "test") + + # Test the call to our underlining notify() call + await hass.services.async_call(BASE_COMPONENT, "test", data) + await hass.async_block_till_done() + + # Validate calls were made under the hood correctly + obj.add.assert_called_once_with([config[BASE_COMPONENT]["url"]]) + obj.notify.assert_called_once_with( + **{"body": data["message"], "title": data["title"], "tag": None} + ) + + +async def test_apprise_notification_with_target(hass, tmp_path): + """Test apprise notification with a target.""" + + # Test cases where our URL is invalid + d = tmp_path / "apprise-config" + d.mkdir() + f = d / "apprise" + + # Write 2 config entries each assigned to different tags + f.write_text("devops=mailto://user:pass@example.com/\r\n") + f.write_text("system,alert=syslog://\r\n") + + config = {BASE_COMPONENT: {"name": "test", "platform": "apprise", "config": str(f)}} + + # Our Message, only notify the services tagged with "devops" + data = {"title": "Test Title", "message": "Test Message", "target": ["devops"]} + + with patch("apprise.Apprise") as mock_apprise: + apprise_obj = MagicMock() + apprise_obj.add.return_value = True + apprise_obj.notify.return_value = True + mock_apprise.return_value = apprise_obj + assert await async_setup_component(hass, BASE_COMPONENT, config) + await hass.async_block_till_done() + + # Test the existance of our service + assert hass.services.has_service(BASE_COMPONENT, "test") + + # Test the call to our underlining notify() call + await hass.services.async_call(BASE_COMPONENT, "test", data) + await hass.async_block_till_done() + + # Validate calls were made under the hood correctly + apprise_obj.notify.assert_called_once_with( + **{"body": data["message"], "title": data["title"], "tag": data["target"]} + ) From d8e325560369c8e8e0d1d42496a5d1d13f11dc85 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 15 Oct 2019 00:31:44 +0000 Subject: [PATCH 293/639] [ci skip] Translation update --- .../components/abode/.translations/ca.json | 22 +++++++++++++++++++ .../components/abode/.translations/da.json | 22 +++++++++++++++++++ .../components/abode/.translations/es.json | 5 +++++ .../components/abode/.translations/lb.json | 22 +++++++++++++++++++ .../components/abode/.translations/nn.json | 5 +++++ .../components/abode/.translations/ru.json | 22 +++++++++++++++++++ .../components/abode/.translations/sl.json | 22 +++++++++++++++++++ .../abode/.translations/zh-Hant.json | 22 +++++++++++++++++++ .../components/adguard/.translations/nn.json | 3 ++- .../ambiclimate/.translations/nn.json | 5 +++++ .../ambient_station/.translations/nn.json | 5 +++++ .../ambient_station/.translations/ru.json | 4 ++-- .../components/axis/.translations/ru.json | 10 ++++----- .../cert_expiry/.translations/ru.json | 14 ++++++------ .../components/cover/.translations/ca.json | 10 +++++++++ .../components/cover/.translations/lb.json | 10 +++++++++ .../components/cover/.translations/sl.json | 10 +++++++++ .../components/deconz/.translations/no.json | 1 + .../components/deconz/.translations/ru.json | 12 +++++----- .../components/esphome/.translations/ru.json | 2 +- .../components/hangouts/.translations/ru.json | 2 +- .../components/heos/.translations/ru.json | 2 +- .../homekit_controller/.translations/ru.json | 4 ++-- .../homematicip_cloud/.translations/ru.json | 6 ++--- .../components/hue/.translations/ru.json | 16 +++++++------- .../iaqualink/.translations/ru.json | 2 +- .../components/ipma/.translations/ru.json | 2 +- .../components/iqvia/.translations/ru.json | 2 +- .../components/life360/.translations/ru.json | 8 +++---- .../components/linky/.translations/ru.json | 10 ++++----- .../components/locative/.translations/hu.json | 2 +- .../components/lock/.translations/ca.json | 8 +++++++ .../components/lock/.translations/lb.json | 8 +++++++ .../components/lock/.translations/sl.json | 8 +++++++ .../luftdaten/.translations/nn.json | 5 +++++ .../luftdaten/.translations/ru.json | 4 ++-- .../components/mailgun/.translations/ru.json | 2 +- .../components/met/.translations/nn.json | 3 ++- .../components/met/.translations/ru.json | 4 ++-- .../components/mqtt/.translations/ru.json | 2 +- .../components/neato/.translations/ru.json | 4 ++-- .../components/nest/.translations/ru.json | 6 ++--- .../components/notion/.translations/nn.json | 11 ++++++++++ .../components/notion/.translations/ru.json | 6 ++--- .../components/openuv/.translations/ru.json | 2 +- .../owntracks/.translations/ru.json | 2 +- .../components/plaato/.translations/ru.json | 2 +- .../components/plex/.translations/ru.json | 16 +++++++------- .../rainmachine/.translations/ru.json | 2 +- .../components/sensor/.translations/no.json | 2 +- .../simplisafe/.translations/ru.json | 2 +- .../smartthings/.translations/nn.json | 5 +++++ .../smartthings/.translations/ru.json | 2 +- .../components/smhi/.translations/ru.json | 2 +- .../solaredge/.translations/ru.json | 4 ++-- .../tellduslive/.translations/ru.json | 8 +++---- .../components/toon/.translations/ru.json | 2 +- .../components/tradfri/.translations/ru.json | 2 +- .../transmission/.translations/ru.json | 4 ++-- .../components/twilio/.translations/ru.json | 2 +- .../components/unifi/.translations/ru.json | 6 ++--- .../components/upnp/.translations/nn.json | 8 +++++++ .../components/upnp/.translations/ru.json | 6 ++--- .../components/velbus/.translations/ru.json | 2 +- .../components/vesync/.translations/ru.json | 2 +- .../components/zha/.translations/ru.json | 2 +- .../components/zwave/.translations/ru.json | 4 ++-- 67 files changed, 341 insertions(+), 103 deletions(-) create mode 100644 homeassistant/components/abode/.translations/ca.json create mode 100644 homeassistant/components/abode/.translations/da.json create mode 100644 homeassistant/components/abode/.translations/es.json create mode 100644 homeassistant/components/abode/.translations/lb.json create mode 100644 homeassistant/components/abode/.translations/nn.json create mode 100644 homeassistant/components/abode/.translations/ru.json create mode 100644 homeassistant/components/abode/.translations/sl.json create mode 100644 homeassistant/components/abode/.translations/zh-Hant.json create mode 100644 homeassistant/components/ambiclimate/.translations/nn.json create mode 100644 homeassistant/components/ambient_station/.translations/nn.json create mode 100644 homeassistant/components/cover/.translations/ca.json create mode 100644 homeassistant/components/cover/.translations/lb.json create mode 100644 homeassistant/components/cover/.translations/sl.json create mode 100644 homeassistant/components/lock/.translations/ca.json create mode 100644 homeassistant/components/lock/.translations/lb.json create mode 100644 homeassistant/components/lock/.translations/sl.json create mode 100644 homeassistant/components/luftdaten/.translations/nn.json create mode 100644 homeassistant/components/notion/.translations/nn.json create mode 100644 homeassistant/components/smartthings/.translations/nn.json diff --git a/homeassistant/components/abode/.translations/ca.json b/homeassistant/components/abode/.translations/ca.json new file mode 100644 index 00000000000..2424fd9b5f0 --- /dev/null +++ b/homeassistant/components/abode/.translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'Abode." + }, + "error": { + "connection_error": "No es pot connectar amb Abode.", + "identifier_exists": "Compte ja registrat.", + "invalid_credentials": "Credencials inv\u00e0lides." + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + }, + "title": "Introdueix la teva informaci\u00f3 d'inici de sessi\u00f3 a Abode." + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/da.json b/homeassistant/components/abode/.translations/da.json new file mode 100644 index 00000000000..3f094cb93bd --- /dev/null +++ b/homeassistant/components/abode/.translations/da.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Det er kun n\u00f8dvendigt med en ops\u00e6tning af Abode." + }, + "error": { + "connection_error": "Kunne ikke oprette forbindelse til Abode.", + "identifier_exists": "Konto er allerede registreret.", + "invalid_credentials": "Ugyldige legitimationsoplysninger." + }, + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Email adresse" + }, + "title": "Udfyld dine Abode-loginoplysninger" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/es.json b/homeassistant/components/abode/.translations/es.json new file mode 100644 index 00000000000..e0c1b6d6a7d --- /dev/null +++ b/homeassistant/components/abode/.translations/es.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/lb.json b/homeassistant/components/abode/.translations/lb.json new file mode 100644 index 00000000000..ed65a5df7c5 --- /dev/null +++ b/homeassistant/components/abode/.translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun ZHA ass erlaabt." + }, + "error": { + "connection_error": "Kann sech net mat Abode verbannen.", + "identifier_exists": "Konto ass scho registr\u00e9iert", + "invalid_credentials": "Ong\u00eblteg Login Informatioune" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail Adress" + }, + "title": "F\u00ebllt \u00e4r Abode Login Informatiounen aus." + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/nn.json b/homeassistant/components/abode/.translations/nn.json new file mode 100644 index 00000000000..e0c1b6d6a7d --- /dev/null +++ b/homeassistant/components/abode/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/ru.json b/homeassistant/components/abode/.translations/ru.json new file mode 100644 index 00000000000..f39e6b1443b --- /dev/null +++ b/homeassistant/components/abode/.translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a Abode.", + "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + }, + "title": "Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/sl.json b/homeassistant/components/abode/.translations/sl.json new file mode 100644 index 00000000000..b840913b7be --- /dev/null +++ b/homeassistant/components/abode/.translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Dovoljena je samo ena konfiguracija Abode." + }, + "error": { + "connection_error": "Ni mogo\u010de vzpostaviti povezave z Abode.", + "identifier_exists": "Ra\u010dun je \u017ee registriran.", + "invalid_credentials": "Neveljavne poverilnice." + }, + "step": { + "user": { + "data": { + "password": "Geslo", + "username": "E-po\u0161tni naslov" + }, + "title": "Izpolnite svoje podatke za prijavo v Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/zh-Hant.json b/homeassistant/components/abode/.translations/zh-Hant.json new file mode 100644 index 00000000000..5bc9efc3696 --- /dev/null +++ b/homeassistant/components/abode/.translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 Abode\u3002" + }, + "error": { + "connection_error": "\u7121\u6cd5\u9023\u7dda\u81f3 Abode\u3002", + "identifier_exists": "\u5e33\u865f\u5df2\u8a3b\u518a\u3002", + "invalid_credentials": "\u6191\u8b49\u7121\u6548\u3002" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" + }, + "title": "\u586b\u5beb Abode \u767b\u5165\u8cc7\u8a0a" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/nn.json b/homeassistant/components/adguard/.translations/nn.json index 7c129cba3af..0e2e82437e8 100644 --- a/homeassistant/components/adguard/.translations/nn.json +++ b/homeassistant/components/adguard/.translations/nn.json @@ -6,6 +6,7 @@ "username": "Brukarnamn" } } - } + }, + "title": "AdGuard Home" } } \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/nn.json b/homeassistant/components/ambiclimate/.translations/nn.json new file mode 100644 index 00000000000..ce8a3ed9db6 --- /dev/null +++ b/homeassistant/components/ambiclimate/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Ambiclimate" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/nn.json b/homeassistant/components/ambient_station/.translations/nn.json new file mode 100644 index 00000000000..0f878b363c9 --- /dev/null +++ b/homeassistant/components/ambient_station/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Ambient PWS" + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index 2d7964f18eb..3a7c405ea4c 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -2,8 +2,8 @@ "config": { "error": { "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", - "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f", - "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b" + "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", + "no_devices": "\u0412 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." }, "step": { "user": { diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 951263d53f9..ae5f0851c44 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -2,15 +2,15 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438", - "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f", - "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis" + "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", + "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis." }, "error": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", - "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e", - "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.", + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json index d962c793121..f9f9e2063be 100644 --- a/homeassistant/components/cert_expiry/.translations/ru.json +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -4,19 +4,19 @@ "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." }, "error": { - "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430", - "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443", + "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430.", + "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442" + "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442." }, "step": { "user": { "data": { - "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430", - "port": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" + "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "port": "\u041f\u043e\u0440\u0442" }, - "title": "C\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f" + "title": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" } }, "title": "\u0421\u0440\u043e\u043a \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u0430" diff --git a/homeassistant/components/cover/.translations/ca.json b/homeassistant/components/cover/.translations/ca.json new file mode 100644 index 00000000000..ffa9ca1a927 --- /dev/null +++ b/homeassistant/components/cover/.translations/ca.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est\u00e0 tancat/da", + "is_closing": "{entity_name} est\u00e0 tancan't-se", + "is_open": "{entity_name} est\u00e0 obert/a", + "is_opening": "{entity_name} s'est\u00e0 obrint" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/lb.json b/homeassistant/components/cover/.translations/lb.json new file mode 100644 index 00000000000..b0c9e1d0d4c --- /dev/null +++ b/homeassistant/components/cover/.translations/lb.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} ass zou", + "is_closing": "{entity_name} g\u00ebtt zougemaach", + "is_open": "{entity_name} ass op", + "is_opening": "{entity_name} g\u00ebtt opgemaach" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/sl.json b/homeassistant/components/cover/.translations/sl.json new file mode 100644 index 00000000000..cb5109b5cb0 --- /dev/null +++ b/homeassistant/components/cover/.translations/sl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} je/so zaprt/a", + "is_closing": "{entity_name} se zapira/jo", + "is_open": "{entity_name} je odprt/a/o", + "is_opening": "{entity_name} se odpira/jo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 71fba6043f7..7db8f3f118d 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -11,6 +11,7 @@ "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index a7200a0cbb4..f342f3145b9 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -3,13 +3,13 @@ "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", - "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", - "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ", - "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ", - "updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d" + "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440 deCONZ.", + "updated_instance": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 deCONZ \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." }, "error": { - "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API" + "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." }, "step": { "hassio_confirm": { @@ -28,7 +28,7 @@ "title": "deCONZ" }, "link": { - "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb", + "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.", "title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ" }, "options": { diff --git a/homeassistant/components/esphome/.translations/ru.json b/homeassistant/components/esphome/.translations/ru.json index 62d24662ab6..27d223012c0 100644 --- a/homeassistant/components/esphome/.translations/ru.json +++ b/homeassistant/components/esphome/.translations/ru.json @@ -6,7 +6,7 @@ "error": { "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a ESP. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0412\u0430\u0448 YAML-\u0444\u0430\u0439\u043b \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0441\u0442\u0440\u043e\u043a\u0443 'api:'.", "invalid_password": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0430\u0440\u043e\u043b\u044c!", - "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "resolve_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c \u0430\u0434\u0440\u0435\u0441 ESP. \u0415\u0441\u043b\u0438 \u044d\u0442\u0430 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438\u0439 IP-\u0430\u0434\u0440\u0435\u0441: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips." }, "flow_title": "ESPHome: {name}", "step": { diff --git a/homeassistant/components/hangouts/.translations/ru.json b/homeassistant/components/hangouts/.translations/ru.json index 6942f683fa6..15d90a672de 100644 --- a/homeassistant/components/hangouts/.translations/ru.json +++ b/homeassistant/components/hangouts/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "invalid_2fa": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", diff --git a/homeassistant/components/heos/.translations/ru.json b/homeassistant/components/heos/.translations/ru.json index f19b5e52064..8aacc8e165d 100644 --- a/homeassistant/components/heos/.translations/ru.json +++ b/homeassistant/components/heos/.translations/ru.json @@ -4,7 +4,7 @@ "already_setup": "\u041d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0435, \u043f\u043e\u0441\u043a\u043e\u043b\u044c\u043a\u0443 \u043e\u043d\u043e \u0431\u0443\u0434\u0435\u0442 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 HEOS \u0432 \u0441\u0435\u0442\u0438." }, "error": { - "connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0445\u043e\u0441\u0442\u0443" + "connection_failure": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c\u0443 \u0445\u043e\u0441\u0442\u0443." }, "step": { "user": { diff --git a/homeassistant/components/homekit_controller/.translations/ru.json b/homeassistant/components/homekit_controller/.translations/ru.json index c7770c6a064..44a57a1eb25 100644 --- a/homeassistant/components/homekit_controller/.translations/ru.json +++ b/homeassistant/components/homekit_controller/.translations/ru.json @@ -24,14 +24,14 @@ "data": { "pairing_code": "\u041a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u0434 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 XXX-XX-XXX), \u0447\u0442\u043e\u0431\u044b \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u044d\u0442\u043e\u0442 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit" }, "user": { "data": { "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e, \u0441 \u043a\u043e\u0442\u043e\u0440\u044b\u043c \u043d\u0443\u0436\u043d\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u043e\u043c HomeKit" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 5155a42c4c3..3170f4bf6cc 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP", + "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP.", "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { "invalid_pin": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 PIN-\u043a\u043e\u0434, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "press_the_button": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443.", - "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430", - "timeout_button": "\u0412\u044b \u043d\u0435 \u043d\u0430\u0436\u0430\u043b\u0438 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430" + "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", + "timeout_button": "\u0412\u044b \u043d\u0435 \u043d\u0430\u0436\u0430\u043b\u0438 \u0441\u0438\u043d\u044e\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u0445 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0432\u0440\u0435\u043c\u0435\u043d\u0438, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430." }, "step": { "init": { diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index 79a46e1861b..08fda906ea9 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -4,15 +4,15 @@ "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", - "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d", - "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b", - "not_hue_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c Hue", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", + "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d.", + "no_bridges": "\u0428\u043b\u044e\u0437\u044b Philips Hue \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", + "not_hue_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c Hue.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "linking": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f", - "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430" + "linking": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", + "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430." }, "step": { "init": { @@ -23,7 +23,7 @@ }, "link": { "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435 \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 Philips Hue \u0432 Home Assistant.\n\n![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435](/static/images/config_philips_hue.jpg)", - "title": "\u0421\u0432\u044f\u0437\u044c \u0441 \u0445\u0430\u0431\u043e\u043c" + "title": "Philips Hue" } }, "title": "Philips Hue" diff --git a/homeassistant/components/iaqualink/.translations/ru.json b/homeassistant/components/iaqualink/.translations/ru.json index 35444dd422b..9a93c19ef20 100644 --- a/homeassistant/components/iaqualink/.translations/ru.json +++ b/homeassistant/components/iaqualink/.translations/ru.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 iAqualink.", "title": "Jandy iAqualink" diff --git a/homeassistant/components/ipma/.translations/ru.json b/homeassistant/components/ipma/.translations/ru.json index a302572ed12..0db504c629c 100644 --- a/homeassistant/components/ipma/.translations/ru.json +++ b/homeassistant/components/ipma/.translations/ru.json @@ -10,7 +10,7 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0438 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u044b", + "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0438 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u044b.", "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" } }, diff --git a/homeassistant/components/iqvia/.translations/ru.json b/homeassistant/components/iqvia/.translations/ru.json index 0c3afc88c94..336877fda13 100644 --- a/homeassistant/components/iqvia/.translations/ru.json +++ b/homeassistant/components/iqvia/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "\u041f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", - "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441" + "invalid_zip_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u043e\u0447\u0442\u043e\u0432\u044b\u0439 \u0438\u043d\u0434\u0435\u043a\u0441." }, "step": { "user": { diff --git a/homeassistant/components/life360/.translations/ru.json b/homeassistant/components/life360/.translations/ru.json index d033da4bae7..eba3a47ead8 100644 --- a/homeassistant/components/life360/.translations/ru.json +++ b/homeassistant/components/life360/.translations/ru.json @@ -1,16 +1,16 @@ { "config": { "abort": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "create_entry": { "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "error": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", - "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d", - "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "invalid_username": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d.", + "unexpected": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u043c Life360.", "user_already_configured": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "step": { diff --git a/homeassistant/components/linky/.translations/ru.json b/homeassistant/components/linky/.translations/ru.json index b569cce9239..463343490a7 100644 --- a/homeassistant/components/linky/.translations/ru.json +++ b/homeassistant/components/linky/.translations/ru.json @@ -4,11 +4,11 @@ "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." }, "error": { - "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443", - "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00)", + "access": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a Enedis.fr, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0418\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443.", + "enedis": "Enedis.fr \u043e\u0442\u043f\u0440\u0430\u0432\u0438\u043b \u043e\u0442\u0432\u0435\u0442 \u0441 \u043e\u0448\u0438\u0431\u043a\u043e\u0439: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435 (\u043d\u0435 \u0432 \u043f\u0440\u043e\u043c\u0435\u0436\u0443\u0442\u043a\u0435 \u0441 23:00 \u043f\u043e 2:00).", "username_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", - "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + "wrong_login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { "user": { @@ -16,7 +16,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "title": "Linky" } }, diff --git a/homeassistant/components/locative/.translations/hu.json b/homeassistant/components/locative/.translations/hu.json index e90910c29a2..3528f1c1e45 100644 --- a/homeassistant/components/locative/.translations/hu.json +++ b/homeassistant/components/locative/.translations/hu.json @@ -6,7 +6,7 @@ }, "step": { "user": { - "description": "Biztosan be szeretn\u00e9d be\u00e1ll\u00edtani a Locative Webhookot?", + "description": "Biztosan be szeretn\u00e9d \u00e1ll\u00edtani a Locative Webhook-ot?", "title": "Locative Webhook be\u00e1ll\u00edt\u00e1sa" } }, diff --git a/homeassistant/components/lock/.translations/ca.json b/homeassistant/components/lock/.translations/ca.json new file mode 100644 index 00000000000..0e05d512bf4 --- /dev/null +++ b/homeassistant/components/lock/.translations/ca.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} est\u00e0 bloquejat/ada", + "is_unlocked": "{entity_name} est\u00e0 desbloquejat/ada" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/lb.json b/homeassistant/components/lock/.translations/lb.json new file mode 100644 index 00000000000..4526b8fb674 --- /dev/null +++ b/homeassistant/components/lock/.translations/lb.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} ass gespaart", + "is_unlocked": "{entity_name} ass entspaart" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/sl.json b/homeassistant/components/lock/.translations/sl.json new file mode 100644 index 00000000000..3c3fd5defbc --- /dev/null +++ b/homeassistant/components/lock/.translations/sl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} je/so zaklenjen/a", + "is_unlocked": "{entity_name} je/so odklenjen/a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/nn.json b/homeassistant/components/luftdaten/.translations/nn.json new file mode 100644 index 00000000000..52b1ec33166 --- /dev/null +++ b/homeassistant/components/luftdaten/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Luftdaten" + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/.translations/ru.json b/homeassistant/components/luftdaten/.translations/ru.json index 7ae83b550e3..1a05137f82d 100644 --- a/homeassistant/components/luftdaten/.translations/ru.json +++ b/homeassistant/components/luftdaten/.translations/ru.json @@ -1,8 +1,8 @@ { "config": { "error": { - "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten", - "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d", + "communication_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a API Luftdaten.", + "invalid_sensor": "\u0414\u0430\u0442\u0447\u0438\u043a \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d \u0438\u043b\u0438 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", "sensor_exists": "\u0414\u0430\u0442\u0447\u0438\u043a \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d." }, "step": { diff --git a/homeassistant/components/mailgun/.translations/ru.json b/homeassistant/components/mailgun/.translations/ru.json index 39503154b6c..094940e6f90 100644 --- a/homeassistant/components/mailgun/.translations/ru.json +++ b/homeassistant/components/mailgun/.translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Mailgun?", - "title": "Mailgun Webhook" + "title": "Mailgun" } }, "title": "Mailgun" diff --git a/homeassistant/components/met/.translations/nn.json b/homeassistant/components/met/.translations/nn.json index 0e024a0e1eb..6daa5b2657a 100644 --- a/homeassistant/components/met/.translations/nn.json +++ b/homeassistant/components/met/.translations/nn.json @@ -6,6 +6,7 @@ "name": "Namn" } } - } + }, + "title": "Met.no" } } \ No newline at end of file diff --git a/homeassistant/components/met/.translations/ru.json b/homeassistant/components/met/.translations/ru.json index 559382cf209..768152084aa 100644 --- a/homeassistant/components/met/.translations/ru.json +++ b/homeassistant/components/met/.translations/ru.json @@ -11,10 +11,10 @@ "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u041c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442", + "description": "\u041d\u043e\u0440\u0432\u0435\u0436\u0441\u043a\u0438\u0439 \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439 \u0438\u043d\u0441\u0442\u0438\u0442\u0443\u0442.", "title": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435" } }, - "title": "Met.no" + "title": "\u041c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0430\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u041d\u043e\u0440\u0432\u0435\u0433\u0438\u0438 (Met.no)" } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/ru.json b/homeassistant/components/mqtt/.translations/ru.json index ac27652cbdd..925b8cf5ab4 100644 --- a/homeassistant/components/mqtt/.translations/ru.json +++ b/homeassistant/components/mqtt/.translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443" + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443." }, "step": { "broker": { diff --git a/homeassistant/components/neato/.translations/ru.json b/homeassistant/components/neato/.translations/ru.json index 1a206258e24..999e45880cf 100644 --- a/homeassistant/components/neato/.translations/ru.json +++ b/homeassistant/components/neato/.translations/ru.json @@ -2,13 +2,13 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "create_entry": { "default": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u043d\u043e\u0439 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, "error": { - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "unexpected_error": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "step": { diff --git a/homeassistant/components/nest/.translations/ru.json b/homeassistant/components/nest/.translations/ru.json index ac88ed224ed..ba49b788b9a 100644 --- a/homeassistant/components/nest/.translations/ru.json +++ b/homeassistant/components/nest/.translations/ru.json @@ -7,10 +7,10 @@ "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Nest \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/nest/)." }, "error": { - "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430", - "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434", + "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u044f\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", + "invalid_code": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043e\u0434.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430.", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430" + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430." }, "step": { "init": { diff --git a/homeassistant/components/notion/.translations/nn.json b/homeassistant/components/notion/.translations/nn.json new file mode 100644 index 00000000000..6d373424c28 --- /dev/null +++ b/homeassistant/components/notion/.translations/nn.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Notion" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index 7345cf46295..6c1d5f5d8d7 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -2,14 +2,14 @@ "config": { "error": { "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c", - "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e" + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e." }, "step": { "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + "username": "\u041b\u043e\u0433\u0438\u043d / \u0410\u0434\u0440\u0435\u0441 \u044d\u043b. \u043f\u043e\u0447\u0442\u044b" }, "title": "Notion" } diff --git a/homeassistant/components/openuv/.translations/ru.json b/homeassistant/components/openuv/.translations/ru.json index 58d57b28056..27d2921a7d4 100644 --- a/homeassistant/components/openuv/.translations/ru.json +++ b/homeassistant/components/openuv/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b.", - "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API" + "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." }, "step": { "user": { diff --git a/homeassistant/components/owntracks/.translations/ru.json b/homeassistant/components/owntracks/.translations/ru.json index 6ebaa31cacf..31c3e77279d 100644 --- a/homeassistant/components/owntracks/.translations/ru.json +++ b/homeassistant/components/owntracks/.translations/ru.json @@ -4,7 +4,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u0435\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435 Android, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({android_url}), \u0437\u0430\u0442\u0435\u043c preferences -> connection. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ``\n - Device ID: ``\n\n\u0415\u0441\u043b\u0438 \u0412\u0430\u0448\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442 \u043d\u0430 iOS, \u043e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 [OwnTracks]({ios_url}), \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0435\u0432\u043e\u043c \u0432\u0435\u0440\u0445\u043d\u0435\u043c \u0443\u0433\u043b\u0443 -> settings. \u0418\u0437\u043c\u0435\u043d\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0442\u0430\u043a, \u043a\u0430\u043a \u0443\u043a\u0430\u0437\u0430\u043d\u043e \u043d\u0438\u0436\u0435:\n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/plaato/.translations/ru.json b/homeassistant/components/plaato/.translations/ru.json index 59964fdedd6..dc06e3ddab0 100644 --- a/homeassistant/components/plaato/.translations/ru.json +++ b/homeassistant/components/plaato/.translations/ru.json @@ -5,7 +5,7 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "create_entry": { - "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Plaato Airlock\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." + "default": "\u0414\u043b\u044f \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0432 Home Assistant \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c webhooks \u0434\u043b\u044f Plaato Airlock.\n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e:\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index fe773f72be9..bce55d35baa 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -5,15 +5,15 @@ "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", "discovery_no_file": "\u0421\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", - "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430", - "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430", - "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435" + "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430.", + "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430.", + "unknown": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e\u0439 \u043f\u0440\u0438\u0447\u0438\u043d\u0435." }, "error": { - "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438", - "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e", - "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443", - "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d" + "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.", + "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443.", + "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d." }, "step": { "manual_setup": { @@ -34,7 +34,7 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 Plex" }, "start_website_auth": { - "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", + "description": "\u041f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", "title": "Plex" }, "user": { diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index 6248890389d..df9adf2d989 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/sensor/.translations/no.json b/homeassistant/components/sensor/.translations/no.json index 5f5eeaacd11..6709e4eb28c 100644 --- a/homeassistant/components/sensor/.translations/no.json +++ b/homeassistant/components/sensor/.translations/no.json @@ -4,7 +4,7 @@ "is_battery_level": "{entity_name} batteriniv\u00e5", "is_humidity": "{entity_name} fuktighet", "is_illuminance": "{entity_name} belysningsstyrke", - "is_power": "{entity_name} str\u00f8m", + "is_power": "{entity_name} effekt", "is_pressure": "{entity_name} trykk", "is_signal_strength": "{entity_name} signalstyrke", "is_temperature": "{entity_name} temperatur", diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index e82172f92f8..721ba69d67e 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "identifier_exists": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", - "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435" + "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, "step": { "user": { diff --git a/homeassistant/components/smartthings/.translations/nn.json b/homeassistant/components/smartthings/.translations/nn.json new file mode 100644 index 00000000000..929e95dc2ff --- /dev/null +++ b/homeassistant/components/smartthings/.translations/nn.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "SmartThings" + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/ru.json b/homeassistant/components/smartthings/.translations/ru.json index 575c593d5a4..f07586c16e3 100644 --- a/homeassistant/components/smartthings/.translations/ru.json +++ b/homeassistant/components/smartthings/.translations/ru.json @@ -6,7 +6,7 @@ "base_url_not_https": "\u0412 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0435 `http` \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 `base_url`, \u043d\u0430\u0447\u0438\u043d\u0430\u044e\u0449\u0438\u0439\u0441\u044f \u0441 `https://`.", "token_already_setup": "\u0422\u043e\u043a\u0435\u043d \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "token_forbidden": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f OAuth.", - "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UID / GUID", + "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 UID / GUID.", "token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d \u0438\u043b\u0438 \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d.", "webhook_error": "SmartThings \u043d\u0435 \u043c\u043e\u0436\u0435\u0442 \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043a\u043e\u043d\u0435\u0447\u043d\u0443\u044e \u0442\u043e\u0447\u043a\u0443, \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u0443\u044e \u0432 `base_url`. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0435\u0439 \u043a \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443." }, diff --git a/homeassistant/components/smhi/.translations/ru.json b/homeassistant/components/smhi/.translations/ru.json index 03b17b3ba8b..f3ba34adac3 100644 --- a/homeassistant/components/smhi/.translations/ru.json +++ b/homeassistant/components/smhi/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "error": { "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", - "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438" + "wrong_location": "\u0422\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0438\u0438." }, "step": { "user": { diff --git a/homeassistant/components/solaredge/.translations/ru.json b/homeassistant/components/solaredge/.translations/ru.json index d8622cdd2c1..e6e7094648d 100644 --- a/homeassistant/components/solaredge/.translations/ru.json +++ b/homeassistant/components/solaredge/.translations/ru.json @@ -9,9 +9,9 @@ "step": { "user": { "data": { - "api_key": "\u041a\u043b\u044e\u0447 API \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u0430\u0439\u0442\u0430", + "api_key": "\u041a\u043b\u044e\u0447 API", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "site_id": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0441\u0430\u0439\u0442\u0430 SolarEdge" + "site_id": "site-id" }, "title": "SolarEdge" } diff --git a/homeassistant/components/tellduslive/.translations/ru.json b/homeassistant/components/tellduslive/.translations/ru.json index 9d3c97ad902..41dc39146e8 100644 --- a/homeassistant/components/tellduslive/.translations/ru.json +++ b/homeassistant/components/tellduslive/.translations/ru.json @@ -4,15 +4,15 @@ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "authorize_url_fail": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430" + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "auth_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443" + "auth_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438, \u043f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443." }, "step": { "auth": { - "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0430\u043a\u043a\u0430\u0443\u043d\u0442 TelldusLive:\n 1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u043d\u0438\u0436\u0435\n 2. \u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Telldus Live\n 3. Authorize **{app_name}** (\u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Yes**).\n 4. \u0412\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.\n\n [\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 TelldusLive]({auth_url})", - "title": "\u0412\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0432\u0445\u043e\u0434 \u0432 TelldusLive" + "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u0438\u0432\u044f\u0437\u0430\u0442\u044c \u0430\u043a\u043a\u0430\u0443\u043d\u0442 Telldus Live:\n 1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0439 \u043d\u0438\u0436\u0435\n 2. \u0412\u043e\u0439\u0434\u0438\u0442\u0435 \u0432 Telldus Live\n 3. Authorize **{app_name}** (\u043d\u0430\u0436\u043c\u0438\u0442\u0435 **Yes**).\n 4. \u0412\u0435\u0440\u043d\u0438\u0442\u0435\u0441\u044c \u0441\u044e\u0434\u0430 \u0438 \u043d\u0430\u0436\u043c\u0438\u0442\u0435 **\u041f\u041e\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u042c**.\n\n [\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 Telldus Live]({auth_url})", + "title": "Telldus Live" }, "user": { "data": { diff --git a/homeassistant/components/toon/.translations/ru.json b/homeassistant/components/toon/.translations/ru.json index 0eddbe2a151..58e6f53986c 100644 --- a/homeassistant/components/toon/.translations/ru.json +++ b/homeassistant/components/toon/.translations/ru.json @@ -8,7 +8,7 @@ "unknown_auth_fail": "\u0412\u043e \u0432\u0440\u0435\u043c\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", + "credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "display_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0434\u0438\u0441\u043f\u043b\u0435\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { diff --git a/homeassistant/components/tradfri/.translations/ru.json b/homeassistant/components/tradfri/.translations/ru.json index 99844dc91ca..c9121862caf 100644 --- a/homeassistant/components/tradfri/.translations/ru.json +++ b/homeassistant/components/tradfri/.translations/ru.json @@ -5,7 +5,7 @@ "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", "invalid_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0441 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u043c \u043a\u043b\u044e\u0447\u043e\u043c. \u0415\u0441\u043b\u0438 \u044d\u0442\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u0448\u043b\u044e\u0437.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043a\u043e\u0434\u0430." }, diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json index e7a438cae11..23f1ceaaa94 100644 --- a/homeassistant/components/transmission/.translations/ru.json +++ b/homeassistant/components/transmission/.translations/ru.json @@ -4,8 +4,8 @@ "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443", - "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", + "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { "options": { diff --git a/homeassistant/components/twilio/.translations/ru.json b/homeassistant/components/twilio/.translations/ru.json index b8d6f11f7ef..1c4e0653496 100644 --- a/homeassistant/components/twilio/.translations/ru.json +++ b/homeassistant/components/twilio/.translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c Twilio?", - "title": "Twilio Webhook" + "title": "Twilio" } }, "title": "Twilio" diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index d7451bd81a0..dbb6efd8343 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c" + "user_privilege": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u043e\u043c." }, "error": { - "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435", - "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430" + "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430." }, "step": { "user": { diff --git a/homeassistant/components/upnp/.translations/nn.json b/homeassistant/components/upnp/.translations/nn.json index cfbedd994af..8e173e4297f 100644 --- a/homeassistant/components/upnp/.translations/nn.json +++ b/homeassistant/components/upnp/.translations/nn.json @@ -8,8 +8,16 @@ "other": "Andre" }, "step": { + "confirm": { + "title": "UPnP/IGD" + }, "init": { "title": "UPnP / IGD" + }, + "user": { + "data": { + "igd": "UPnP/IGD" + } } }, "title": "UPnP / IGD" diff --git a/homeassistant/components/upnp/.translations/ru.json b/homeassistant/components/upnp/.translations/ru.json index 3351f0d5d8a..9599832799f 100644 --- a/homeassistant/components/upnp/.translations/ru.json +++ b/homeassistant/components/upnp/.translations/ru.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP", - "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD", + "incomplete_device": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043d\u0435\u043f\u043e\u043b\u043d\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP.", + "no_devices_discovered": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e UPnP / IGD.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 UPnP / IGD \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", - "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432", + "no_sensors_or_port_mapping": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438\u043b\u0438 \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043f\u043e\u0440\u0442\u043e\u0432.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { diff --git a/homeassistant/components/velbus/.translations/ru.json b/homeassistant/components/velbus/.translations/ru.json index 3434c584221..10ae06ffa7c 100644 --- a/homeassistant/components/velbus/.translations/ru.json +++ b/homeassistant/components/velbus/.translations/ru.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f Velbus", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "port": "\u0421\u0442\u0440\u043e\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" }, "title": "Velbus" diff --git a/homeassistant/components/vesync/.translations/ru.json b/homeassistant/components/vesync/.translations/ru.json index 38b86e9e29f..23cb6fdfac7 100644 --- a/homeassistant/components/vesync/.translations/ru.json +++ b/homeassistant/components/vesync/.translations/ru.json @@ -4,7 +4,7 @@ "already_setup": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c" + "invalid_login": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { "user": { diff --git a/homeassistant/components/zha/.translations/ru.json b/homeassistant/components/zha/.translations/ru.json index 291d760dbc8..1779ed613fc 100644 --- a/homeassistant/components/zha/.translations/ru.json +++ b/homeassistant/components/zha/.translations/ru.json @@ -4,7 +4,7 @@ "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, "step": { "user": { diff --git a/homeassistant/components/zwave/.translations/ru.json b/homeassistant/components/zwave/.translations/ru.json index ed2e20f3527..4243f583082 100644 --- a/homeassistant/components/zwave/.translations/ru.json +++ b/homeassistant/components/zwave/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave" + "one_instance_only": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0436\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043e\u0434\u043d\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u043e\u043c Z-Wave." }, "error": { "option_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 Z-Wave. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." @@ -13,7 +13,7 @@ "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", "title": "Z-Wave" } }, From 68a3c97464a3c80a05c65361d9a15d9dc4fe2883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 15 Oct 2019 11:04:58 +0300 Subject: [PATCH 294/639] Deprecate Python 3.6 support, 3.8.0 is out (#27680) --- homeassistant/bootstrap.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 422eab8ed4a..e399205ec70 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -93,6 +93,17 @@ async def async_from_config_dict( stop = time() _LOGGER.info("Home Assistant initialized in %.2fs", stop - start) + if sys.version_info[:3] < (3, 7, 0): + msg = ( + "Python 3.6 support is deprecated and will " + "be removed in the first release after December 15, 2019. Please " + "upgrade Python to 3.7.0 or higher." + ) + _LOGGER.warning(msg) + hass.components.persistent_notification.async_create( + msg, "Python version", "python_version" + ) + return hass From 5938f5a3a1e6a2947e24269964660d5726314f6a Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 10:06:29 +0200 Subject: [PATCH 295/639] moved imports to top level (#27682) --- homeassistant/components/discord/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index 17ff0a192d0..f35cf5b0ce9 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -2,6 +2,7 @@ import logging import os.path +import discord import voluptuous as vol from homeassistant.const import CONF_TOKEN @@ -44,7 +45,6 @@ class DiscordNotificationService(BaseNotificationService): async def async_send_message(self, message, **kwargs): """Login to Discord, send message to channel(s) and log out.""" - import discord discord.VoiceClient.warn_nacl = False discord_bot = discord.Client() From 502c65b5fd841ecadc0f249315806aee472537b6 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 10:06:56 +0200 Subject: [PATCH 296/639] moved imports to top level (#27678) --- homeassistant/components/digitalloggers/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/digitalloggers/switch.py b/homeassistant/components/digitalloggers/switch.py index 9983ccc93fa..10c8ce73a47 100644 --- a/homeassistant/components/digitalloggers/switch.py +++ b/homeassistant/components/digitalloggers/switch.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import dlipower import voluptuous as vol from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA @@ -45,7 +46,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Find and return DIN III Relay switch.""" - import dlipower host = config.get(CONF_HOST) controller_name = config.get(CONF_NAME) From ecc276de3866af52ed15fcc5b4847a3dedf0e0f0 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 10:07:37 +0200 Subject: [PATCH 297/639] moved imports to top level (#27675) --- homeassistant/components/denonavr/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/denonavr/media_player.py b/homeassistant/components/denonavr/media_player.py index 51fc890c873..1725b2d105c 100644 --- a/homeassistant/components/denonavr/media_player.py +++ b/homeassistant/components/denonavr/media_player.py @@ -3,9 +3,10 @@ from collections import namedtuple import logging +import denonavr import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MUSIC, @@ -88,7 +89,6 @@ NewHost = namedtuple("NewHost", ["host", "name"]) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Denon platform.""" - import denonavr # Initialize list with receivers to be started receivers = [] From 5b410ff3a523732efc05f52b28b29df88755fc04 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 11:33:22 +0200 Subject: [PATCH 298/639] moved imports to top level (#27677) --- homeassistant/components/digital_ocean/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/digital_ocean/__init__.py b/homeassistant/components/digital_ocean/__init__.py index 18dfb49365a..bdb0c348803 100644 --- a/homeassistant/components/digital_ocean/__init__.py +++ b/homeassistant/components/digital_ocean/__init__.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import digitalocean import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN @@ -38,7 +39,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Digital Ocean component.""" - import digitalocean conf = config[DOMAIN] access_token = conf.get(CONF_ACCESS_TOKEN) @@ -63,7 +63,6 @@ class DigitalOcean: def __init__(self, access_token): """Initialize the Digital Ocean connection.""" - import digitalocean self._access_token = access_token self.data = None From 57b8d1889acf82af74305f66ef774feb6a107e8f Mon Sep 17 00:00:00 2001 From: "Brett T. Warden" Date: Tue, 15 Oct 2019 02:53:13 -0700 Subject: [PATCH 299/639] Handle marker attrs that may not exist (#27519) marker-high-levels and marker-low-levels may not exist in printer attributes returned by CUPS, so we'll use .get() to avoid this and default to None: KeyError: 'marker-high-levels' Fixes #27518 --- homeassistant/components/cups/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cups/sensor.py b/homeassistant/components/cups/sensor.py index f6a5133d8a9..4af51e911a1 100644 --- a/homeassistant/components/cups/sensor.py +++ b/homeassistant/components/cups/sensor.py @@ -276,11 +276,11 @@ class MarkerSensor(Entity): if self._attributes is None: return None - high_level = self._attributes[self._printer]["marker-high-levels"] + high_level = self._attributes[self._printer].get("marker-high-levels") if isinstance(high_level, list): high_level = high_level[self._index] - low_level = self._attributes[self._printer]["marker-low-levels"] + low_level = self._attributes[self._printer].get("marker-low-levels") if isinstance(low_level, list): low_level = low_level[self._index] From 3d7860391a4be9cba357e7de2544ebe7a8c01b52 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Tue, 15 Oct 2019 12:12:58 +0200 Subject: [PATCH 300/639] Improve code coverage for HomematicIP Cloud (#27606) * Improve tests for HomematicIP Cloud * create fixtures remove decorators * removed further decorators * remove last decorator * improve exception handling * removed not required coroutine * use the correct place for mock --- .coveragerc | 1 - .../components/homematicip_cloud/__init__.py | 4 +- .../components/homematicip_cloud/hap.py | 2 +- .../components/homematicip_cloud/light.py | 8 +- .../components/homematicip_cloud/conftest.py | 86 ++++++++++---- tests/components/homematicip_cloud/helper.py | 9 +- .../test_alarm_control_panel.py | 18 +++ .../homematicip_cloud/test_binary_sensor.py | 16 +++ .../homematicip_cloud/test_climate.py | 36 ++++++ .../homematicip_cloud/test_cover.py | 14 +++ .../homematicip_cloud/test_device.py | 21 ++++ .../components/homematicip_cloud/test_hap.py | 108 +++++++++++++++++- .../homematicip_cloud/test_light.py | 29 ++++- .../homematicip_cloud/test_sensor.py | 26 +++++ .../homematicip_cloud/test_switch.py | 19 ++- .../homematicip_cloud/test_weather.py | 14 +++ 16 files changed, 372 insertions(+), 39 deletions(-) diff --git a/.coveragerc b/.coveragerc index 145350b6b19..69ce7f8322c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -290,7 +290,6 @@ omit = homeassistant/components/homematic/climate.py homeassistant/components/homematic/cover.py homeassistant/components/homematic/notify.py - homeassistant/components/homematicip_cloud/* homeassistant/components/homeworks/* homeassistant/components/honeywell/climate.py homeassistant/components/hook/switch.py diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index c8fb31998ef..139565bf249 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -213,9 +213,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: def _get_home(hapid: str): """Return a HmIP home.""" - hap = hass.data[DOMAIN][hapid] + hap = hass.data[DOMAIN].get(hapid) if hap: return hap.home + + _LOGGER.info("No matching access point found for access point id %s", hapid) return None return True diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index f6727f91c7e..64fbd4fd079 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -53,7 +53,7 @@ class HomematicipAuth: except HmipConnectionError: return False - async def get_auth(self, hass, hapid, pin): + async def get_auth(self, hass: HomeAssistant, hapid, pin): """Create a HomematicIP access point object.""" auth = AsyncAuth(hass.loop, async_get_clientsession(hass)) try: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 80ee4cc5743..bc704e2ef06 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -119,9 +119,7 @@ class HomematicipDimmer(HomematicipGenericDevice, Light): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - if self._device.dimLevel: - return int(self._device.dimLevel * 255) - return 0 + return int((self._device.dimLevel or 0.0) * 255) @property def supported_features(self) -> int: @@ -176,9 +174,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): @property def brightness(self) -> int: """Return the brightness of this light between 0..255.""" - if self._func_channel.dimLevel: - return int(self._func_channel.dimLevel * 255) - return 0 + return int((self._func_channel.dimLevel or 0.0) * 255) @property def hs_color(self) -> tuple: diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 2c2b020f3a0..b2fc53a28ec 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -1,22 +1,20 @@ """Initializer helpers for HomematicIP fake server.""" -from unittest.mock import MagicMock, patch - +from asynctest import MagicMock, Mock, patch +from homematicip.aio.auth import AsyncAuth from homematicip.aio.connection import AsyncConnection +from homematicip.aio.home import AsyncHome import pytest from homeassistant import config_entries from homeassistant.components.homematicip_cloud import ( - CONF_ACCESSPOINT, - CONF_AUTHTOKEN, DOMAIN as HMIPC_DOMAIN, async_setup as hmip_async_setup, const as hmipc, hap as hmip_hap, ) -from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from .helper import AUTH_TOKEN, HAPID, HomeTemplate +from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeTemplate from tests.common import MockConfigEntry, mock_coro @@ -31,16 +29,11 @@ def mock_connection_fixture(): connection._restCall.side_effect = _rest_call_side_effect # pylint: disable=W0212 connection.api_call.return_value = mock_coro(True) + connection.init.side_effect = mock_coro(True) return connection -@pytest.fixture(name="default_mock_home") -def default_mock_home_fixture(mock_connection): - """Create a fake homematic async home.""" - return HomeTemplate(connection=mock_connection).init_home().get_async_home_mock() - - @pytest.fixture(name="hmip_config_entry") def hmip_config_entry_fixture(): """Create a mock config entriy for homematic ip cloud.""" @@ -48,6 +41,7 @@ def hmip_config_entry_fixture(): hmipc.HMIPC_HAPID: HAPID, hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, hmipc.HMIPC_NAME: "", + hmipc.HMIPC_PIN: HAPPIN, } config_entry = MockConfigEntry( version=1, @@ -62,17 +56,34 @@ def hmip_config_entry_fixture(): return config_entry +@pytest.fixture(name="default_mock_home") +def default_mock_home_fixture(mock_connection): + """Create a fake homematic async home.""" + return HomeTemplate(connection=mock_connection).init_home().get_async_home_mock() + + @pytest.fixture(name="default_mock_hap") async def default_mock_hap_fixture( - hass: HomeAssistant, default_mock_home, hmip_config_entry + hass: HomeAssistant, mock_connection, hmip_config_entry ): - """Create a fake homematic access point.""" + """Create a mocked homematic access point.""" + return await get_mock_hap(hass, mock_connection, hmip_config_entry) + + +async def get_mock_hap(hass: HomeAssistant, mock_connection, hmip_config_entry): + """Create a mocked homematic access point.""" hass.config.components.add(HMIPC_DOMAIN) hap = hmip_hap.HomematicipHAP(hass, hmip_config_entry) - with patch.object(hap, "get_hap", return_value=mock_coro(default_mock_home)): + home_name = hmip_config_entry.data["name"] + mock_home = ( + HomeTemplate(connection=mock_connection, home_name=home_name) + .init_home() + .get_async_home_mock() + ) + with patch.object(hap, "get_hap", return_value=mock_coro(mock_home)): assert await hap.async_setup() is True - default_mock_home.on_update(hap.async_update) - default_mock_home.on_create(hap.async_create_entity) + mock_home.on_update(hap.async_update) + mock_home.on_create(hap.async_create_entity) hass.data[HMIPC_DOMAIN] = {HAPID: hap} @@ -85,18 +96,49 @@ async def default_mock_hap_fixture( def hmip_config_fixture(): """Create a config for homematic ip cloud.""" - entry_data = {CONF_ACCESSPOINT: HAPID, CONF_AUTHTOKEN: AUTH_TOKEN, CONF_NAME: ""} + entry_data = { + hmipc.HMIPC_HAPID: HAPID, + hmipc.HMIPC_AUTHTOKEN: AUTH_TOKEN, + hmipc.HMIPC_NAME: "", + hmipc.HMIPC_PIN: HAPPIN, + } - return {hmipc.DOMAIN: [entry_data]} + return {HMIPC_DOMAIN: [entry_data]} + + +@pytest.fixture(name="dummy_config") +def dummy_config_fixture(): + """Create a dummy config.""" + return {"blabla": None} @pytest.fixture(name="mock_hap_with_service") async def mock_hap_with_service_fixture( - hass: HomeAssistant, default_mock_hap, hmip_config + hass: HomeAssistant, default_mock_hap, dummy_config ): """Create a fake homematic access point with hass services.""" - - await hmip_async_setup(hass, hmip_config) + await hmip_async_setup(hass, dummy_config) await hass.async_block_till_done() hass.data[HMIPC_DOMAIN] = {HAPID: default_mock_hap} return default_mock_hap + + +@pytest.fixture(name="simple_mock_home") +def simple_mock_home_fixture(): + """Return a simple AsyncHome Mock.""" + return Mock( + spec=AsyncHome, + devices=[], + groups=[], + location=Mock(), + weather=Mock(create=True), + id=42, + dutyCycle=88, + connected=True, + ) + + +@pytest.fixture(name="simple_mock_auth") +def simple_mock_auth_fixture(): + """Return a simple AsyncAuth Mock.""" + return Mock(spec=AsyncAuth, pin=HAPPIN, create=True) diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index e5c5c4569d7..78c78ec0ab9 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -1,7 +1,7 @@ """Helper for HomematicIP Cloud Tests.""" import json -from asynctest import Mock +from asynctest import Mock from homematicip.aio.class_maps import ( TYPE_CLASS_MAP, TYPE_GROUP_MAP, @@ -20,6 +20,7 @@ from homeassistant.components.homematicip_cloud.device import ( from tests.common import load_fixture HAPID = "3014F7110000000000000001" +HAPPIN = "5678" AUTH_TOKEN = "1234" HOME_JSON = "homematicip_cloud.json" @@ -81,10 +82,11 @@ class HomeTemplate(Home): _typeGroupMap = TYPE_GROUP_MAP _typeSecurityEventMap = TYPE_SECURITY_EVENT_MAP - def __init__(self, connection=None): + def __init__(self, connection=None, home_name=""): """Init template with connection.""" super().__init__(connection=connection) self.label = "Access Point" + self.name = home_name self.model_type = "HmIP-HAP" self.init_json_state = None @@ -121,13 +123,12 @@ class HomeTemplate(Home): Create Mock for Async_Home. based on template to be used for testing. It adds collections of mocked devices and groups to the home objects, - and sets reuired attributes. + and sets required attributes. """ mock_home = Mock( spec=AsyncHome, wraps=self, label="Access Point", modelType="HmIP-HAP" ) mock_home.__dict__.update(self.__dict__) - mock_home.name = "" return mock_home diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 0a68ac6d509..2798a0879b7 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -2,12 +2,17 @@ from homematicip.base.enums import WindowState from homematicip.group import SecurityZoneGroup +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, +) +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.setup import async_setup_component from .helper import get_and_check_entity_basics @@ -38,6 +43,19 @@ async def _async_manipulate_security_zones( await hass.async_block_till_done() +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, + ALARM_CONTROL_PANEL_DOMAIN, + {ALARM_CONTROL_PANEL_DOMAIN: {"platform": HMIPC_DOMAIN}}, + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_alarm_control_panel(hass, default_mock_hap): """Test HomematicipAlarmControlPanel.""" entity_id = "alarm_control_panel.hmip_alarm_control_panel" diff --git a/tests/components/homematicip_cloud/test_binary_sensor.py b/tests/components/homematicip_cloud/test_binary_sensor.py index 0de2101d287..0760518171e 100644 --- a/tests/components/homematicip_cloud/test_binary_sensor.py +++ b/tests/components/homematicip_cloud/test_binary_sensor.py @@ -1,6 +1,8 @@ """Tests for HomematicIP Cloud binary sensor.""" from homematicip.base.enums import SmokeDetectorAlarmType, WindowState +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.binary_sensor import ( ATTR_ACCELERATION_SENSOR_MODE, ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION, @@ -10,10 +12,24 @@ from homeassistant.components.homematicip_cloud.binary_sensor import ( ATTR_MOTION_DETECTED, ) from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, + BINARY_SENSOR_DOMAIN, + {BINARY_SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}}, + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_acceleration_sensor(hass, default_mock_hap): """Test HomematicipAccelerationSensor.""" entity_id = "binary_sensor.garagentor" diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 8f8a681fad8..bdfd26319e6 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -4,6 +4,7 @@ import datetime from homematicip.base.enums import AbsenceType from homematicip.functionalHomes import IndoorClimateHome +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate.const import ( ATTR_CURRENT_TEMPERATURE, ATTR_PRESET_MODE, @@ -15,10 +16,23 @@ from homeassistant.components.climate.const import ( PRESET_ECO, PRESET_NONE, ) +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN +from homeassistant.setup import async_setup_component from .helper import HAPID, async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, CLIMATE_DOMAIN, {CLIMATE_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_heating_group(hass, default_mock_hap): """Test HomematicipHeatingGroup.""" entity_id = "climate.badezimmer" @@ -153,6 +167,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) + assert len(home._connection.mock_calls) == 1 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -162,6 +177,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_duration" assert home.mock_calls[-1][1] == (60,) + assert len(home._connection.mock_calls) == 2 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -171,6 +187,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) + assert len(home._connection.mock_calls) == 3 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -180,6 +197,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_absence_with_period" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0),) + assert len(home._connection.mock_calls) == 4 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -189,6 +207,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) + assert len(home._connection.mock_calls) == 5 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -198,6 +217,7 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "activate_vacation" assert home.mock_calls[-1][1] == (datetime.datetime(2019, 2, 17, 14, 0), 18.5) + assert len(home._connection.mock_calls) == 6 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -207,12 +227,14 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () + assert len(home._connection.mock_calls) == 7 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", "deactivate_eco_mode", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_absence" assert home.mock_calls[-1][1] == () + assert len(home._connection.mock_calls) == 8 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", @@ -222,9 +244,23 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () + assert len(home._connection.mock_calls) == 9 # pylint: disable=W0212 await hass.services.async_call( "homematicip_cloud", "deactivate_vacation", blocking=True ) assert home.mock_calls[-1][0] == "deactivate_vacation" assert home.mock_calls[-1][1] == () + assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 + + not_existing_hap_id = "5555F7110000000000000001" + await hass.services.async_call( + "homematicip_cloud", + "deactivate_vacation", + {"accesspoint_id": not_existing_hap_id}, + blocking=True, + ) + assert home.mock_calls[-1][0] == "deactivate_vacation" + assert home.mock_calls[-1][1] == () + # There is no further call on connection. + assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 diff --git a/tests/components/homematicip_cloud/test_cover.py b/tests/components/homematicip_cloud/test_cover.py index 7bfb842a0df..22922303f9e 100644 --- a/tests/components/homematicip_cloud/test_cover.py +++ b/tests/components/homematicip_cloud/test_cover.py @@ -2,12 +2,26 @@ from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, ATTR_CURRENT_TILT_POSITION, + DOMAIN as COVER_DOMAIN, ) +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.const import STATE_CLOSED, STATE_OPEN +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, COVER_DOMAIN, {COVER_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_cover_shutter(hass, default_mock_hap): """Test HomematicipCoverShutte.""" entity_id = "cover.sofa_links" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 81c35f8e2a9..812f32a3344 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -2,6 +2,7 @@ from homeassistant.const import STATE_ON, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er +from .conftest import get_mock_hap from .helper import async_manipulate_test_data, get_and_check_entity_basics @@ -109,3 +110,23 @@ async def test_hap_reconnected(hass, default_mock_hap): await hass.async_block_till_done() ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_ON + + +async def test_hap_with_name(hass, mock_connection, hmip_config_entry): + """Test hap with name.""" + home_name = "TestName" + entity_id = f"light.{home_name.lower()}_treppe" + entity_name = f"{home_name} Treppe" + device_model = "HmIP-BSL" + + hmip_config_entry.data["name"] = home_name + mock_hap = await get_mock_hap(hass, mock_connection, hmip_config_entry) + assert mock_hap + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap, entity_id, entity_name, device_model + ) + + assert hmip_device + assert ha_state.state == STATE_ON + assert ha_state.attributes["friendly_name"] == entity_name diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index cd8ead40c43..90f557b1f93 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -1,11 +1,24 @@ """Test HomematicIP Cloud accesspoint.""" -from unittest.mock import Mock, patch +from asynctest import Mock, patch +from homematicip.aio.auth import AsyncAuth +from homematicip.base.base_connection import HmipConnectionError import pytest -from homeassistant.components.homematicip_cloud import const, errors, hap as hmipc +from homeassistant.components.homematicip_cloud import ( + DOMAIN as HMIPC_DOMAIN, + const, + errors, + hap as hmipc, +) +from homeassistant.components.homematicip_cloud.hap import ( + HomematicipAuth, + HomematicipHAP, +) from homeassistant.exceptions import ConfigEntryNotReady +from .helper import HAPID, HAPPIN + from tests.common import mock_coro, mock_coro_func @@ -53,6 +66,22 @@ async def test_auth_auth_check_and_register(hass): assert await hap.async_register() == "ABC" +async def test_auth_auth_check_and_register_with_exception(hass): + """Test auth client registration.""" + config = { + const.HMIPC_HAPID: "ABC123", + const.HMIPC_PIN: "123", + const.HMIPC_NAME: "hmip", + } + hap = hmipc.HomematicipAuth(hass, config) + hap.auth = Mock(spec=AsyncAuth) + with patch.object( + hap.auth, "isRequestAcknowledged", side_effect=HmipConnectionError + ), patch.object(hap.auth, "requestAuthToken", side_effect=HmipConnectionError): + assert await hap.async_checkbutton() is False + assert await hap.async_register() is False + + async def test_hap_setup_works(aioclient_mock): """Test a successful setup of a accesspoint.""" hass = Mock() @@ -121,3 +150,78 @@ async def test_hap_reset_unloads_entry_if_setup(): await hap.async_reset() assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 8 + + +async def test_hap_create(hass, hmip_config_entry, simple_mock_home): + """Mock AsyncHome to execute get_hap.""" + hass.config.components.add(HMIPC_DOMAIN) + hap = HomematicipHAP(hass, hmip_config_entry) + assert hap + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome", + return_value=simple_mock_home, + ), patch.object(hap, "async_connect", return_value=mock_coro(None)): + assert await hap.async_setup() is True + + +async def test_hap_create_exception(hass, hmip_config_entry, simple_mock_home): + """Mock AsyncHome to execute get_hap.""" + hass.config.components.add(HMIPC_DOMAIN) + hap = HomematicipHAP(hass, hmip_config_entry) + assert hap + + with patch.object(hap, "get_hap", side_effect=HmipConnectionError), pytest.raises( + HmipConnectionError + ): + await hap.async_setup() + + simple_mock_home.init.side_effect = HmipConnectionError + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome", + return_value=simple_mock_home, + ), pytest.raises(ConfigEntryNotReady): + await hap.async_setup() + + +async def test_auth_create(hass, simple_mock_auth): + """Mock AsyncAuth to execute get_auth.""" + config = { + const.HMIPC_HAPID: HAPID, + const.HMIPC_PIN: HAPPIN, + const.HMIPC_NAME: "hmip", + } + hmip_auth = HomematicipAuth(hass, config) + assert hmip_auth + + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncAuth", + return_value=simple_mock_auth, + ): + assert await hmip_auth.async_setup() is True + await hass.async_block_till_done() + assert hmip_auth.auth.pin == HAPPIN + + +async def test_auth_create_exception(hass, simple_mock_auth): + """Mock AsyncAuth to execute get_auth.""" + config = { + const.HMIPC_HAPID: HAPID, + const.HMIPC_PIN: HAPPIN, + const.HMIPC_NAME: "hmip", + } + hmip_auth = HomematicipAuth(hass, config) + simple_mock_auth.connectionRequest.side_effect = HmipConnectionError + assert hmip_auth + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncAuth", + return_value=simple_mock_auth, + ): + assert await hmip_auth.async_setup() is True + await hass.async_block_till_done() + assert hmip_auth.auth is False + + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncAuth", + return_value=simple_mock_auth, + ): + assert await hmip_auth.get_auth(hass, HAPID, HAPPIN) is False diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index a8d4984520c..17e92d9d99d 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -1,16 +1,33 @@ """Tests for HomematicIP Cloud light.""" from homematicip.base.enums import RGBColorState +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.light import ( ATTR_ENERGY_COUNTER, ATTR_POWER_CONSUMPTION, ) -from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_COLOR_NAME +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_NAME, + DOMAIN as LIGHT_DOMAIN, +) from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_light(hass, default_mock_hap): """Test HomematicipLight.""" entity_id = "light.treppe" @@ -114,6 +131,11 @@ async def test_hmip_notification_light(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "dimLevel", None, 2) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_BRIGHTNESS) + async def test_hmip_dimmer(hass, default_mock_hap): """Test HomematicipDimmer.""" @@ -158,6 +180,11 @@ async def test_hmip_dimmer(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == STATE_OFF + await async_manipulate_test_data(hass, hmip_device, "dimLevel", None) + ha_state = hass.states.get(entity_id) + assert ha_state.state == STATE_OFF + assert not ha_state.attributes.get(ATTR_BRIGHTNESS) + async def test_hmip_light_measuring(hass, default_mock_hap): """Test HomematicipLightMeasuring.""" diff --git a/tests/components/homematicip_cloud/test_sensor.py b/tests/components/homematicip_cloud/test_sensor.py index d4307477975..8412cd19f4d 100644 --- a/tests/components/homematicip_cloud/test_sensor.py +++ b/tests/components/homematicip_cloud/test_sensor.py @@ -1,4 +1,7 @@ """Tests for HomematicIP Cloud sensor.""" +from homematicip.base.enums import ValveState + +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.sensor import ( ATTR_LEFT_COUNTER, ATTR_RIGHT_COUNTER, @@ -6,11 +9,24 @@ from homeassistant.components.homematicip_cloud.sensor import ( ATTR_WIND_DIRECTION, ATTR_WIND_DIRECTION_VARIATION, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, POWER_WATT, TEMP_CELSIUS +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, SENSOR_DOMAIN, {SENSOR_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_accesspoint_status(hass, default_mock_hap): """Test HomematicipSwitch.""" entity_id = "sensor.access_point" @@ -50,6 +66,16 @@ async def test_hmip_heating_thermostat(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.state == "nn" + await async_manipulate_test_data( + hass, hmip_device, "valveState", ValveState.ADAPTION_DONE + ) + ha_state = hass.states.get(entity_id) + assert ha_state.state == "37" + + await async_manipulate_test_data(hass, hmip_device, "lowBat", True) + ha_state = hass.states.get(entity_id) + assert ha_state.attributes["icon"] == "mdi:battery-outline" + async def test_hmip_humidity_sensor(hass, default_mock_hap): """Test HomematicipHumiditySensor.""" diff --git a/tests/components/homematicip_cloud/test_switch.py b/tests/components/homematicip_cloud/test_switch.py index 15eaf6da04c..9e33d1d9587 100644 --- a/tests/components/homematicip_cloud/test_switch.py +++ b/tests/components/homematicip_cloud/test_switch.py @@ -1,13 +1,30 @@ """Tests for HomematicIP Cloud switch.""" +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.homematicip_cloud.device import ( ATTR_GROUP_MEMBER_UNREACHABLE, ) -from homeassistant.components.switch import ATTR_CURRENT_POWER_W, ATTR_TODAY_ENERGY_KWH +from homeassistant.components.switch import ( + ATTR_CURRENT_POWER_W, + ATTR_TODAY_ENERGY_KWH, + DOMAIN as SWITCH_DOMAIN, +) from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_switch(hass, default_mock_hap): """Test HomematicipSwitch.""" entity_id = "switch.schrank" diff --git a/tests/components/homematicip_cloud/test_weather.py b/tests/components/homematicip_cloud/test_weather.py index 0b5d59215bb..9427a2d05bf 100644 --- a/tests/components/homematicip_cloud/test_weather.py +++ b/tests/components/homematicip_cloud/test_weather.py @@ -1,15 +1,29 @@ """Tests for HomematicIP Cloud weather.""" +from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN from homeassistant.components.weather import ( ATTR_WEATHER_ATTRIBUTION, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, + DOMAIN as WEATHER_DOMAIN, ) +from homeassistant.setup import async_setup_component from .helper import async_manipulate_test_data, get_and_check_entity_basics +async def test_manually_configured_platform(hass): + """Test that we do not set up an access point.""" + assert ( + await async_setup_component( + hass, WEATHER_DOMAIN, {WEATHER_DOMAIN: {"platform": HMIPC_DOMAIN}} + ) + is True + ) + assert not hass.data.get(HMIPC_DOMAIN) + + async def test_hmip_weather_sensor(hass, default_mock_hap): """Test HomematicipWeatherSensor.""" entity_id = "weather.weather_sensor_plus" From b4a73fa87e97be9d5b562554ea42c88b7c150644 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 12:26:50 +0200 Subject: [PATCH 301/639] Move imports in decora component (#27645) * moved imports to top level * replaced importlib with standard import * fix for Unable to import 'decora' error --- homeassistant/components/decora/light.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index cad98f9d8a4..4d2d10ccbd5 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -1,18 +1,19 @@ """Support for Decora dimmers.""" -import importlib -import logging from functools import wraps +import logging import time +from bluepy.btle import BTLEException # pylint: disable=import-error, no-member +import decora # pylint: disable=import-error, no-member import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -34,9 +35,6 @@ def retry(method): @wraps(method) def wrapper_retry(device, *args, **kwargs): """Try send command and retry on error.""" - # pylint: disable=import-error, no-member - import decora - import bluepy initial = time.monotonic() while True: @@ -44,7 +42,7 @@ def retry(method): return None try: return method(device, *args, **kwargs) - except (decora.decoraException, AttributeError, bluepy.btle.BTLEException): + except (decora.decoraException, AttributeError, BTLEException): _LOGGER.warning( "Decora connect error for device %s. " "Reconnecting...", device.name, @@ -74,8 +72,6 @@ class DecoraLight(Light): def __init__(self, device): """Initialize the light.""" - # pylint: disable=no-member - decora = importlib.import_module("decora") self._name = device["name"] self._address = device["address"] From 0463349f0284ea6d8e72d1727a9fbc952cdab2ba Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 12:28:24 +0200 Subject: [PATCH 302/639] moved imports to top level (#27683) --- .../dlib_face_detect/image_processing.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlib_face_detect/image_processing.py b/homeassistant/components/dlib_face_detect/image_processing.py index 749e536e2e8..cdd8bc101b5 100644 --- a/homeassistant/components/dlib_face_detect/image_processing.py +++ b/homeassistant/components/dlib_face_detect/image_processing.py @@ -1,17 +1,19 @@ """Component that will help set the Dlib face detect processing.""" -import logging import io +import logging -from homeassistant.core import split_entity_id +import face_recognition # pylint: disable=import-error + +from homeassistant.components.image_processing import ( + CONF_ENTITY_ID, + CONF_NAME, + CONF_SOURCE, + ImageProcessingFaceEntity, +) # pylint: disable=unused-import from homeassistant.components.image_processing import PLATFORM_SCHEMA # noqa -from homeassistant.components.image_processing import ( - ImageProcessingFaceEntity, - CONF_SOURCE, - CONF_ENTITY_ID, - CONF_NAME, -) +from homeassistant.core import split_entity_id _LOGGER = logging.getLogger(__name__) @@ -55,7 +57,6 @@ class DlibFaceDetectEntity(ImageProcessingFaceEntity): def process_image(self, image): """Process image.""" - import face_recognition # pylint: disable=import-error fak_file = io.BytesIO(image) fak_file.name = "snapshot.jpg" From 5b1f44ba197b27c70df091e47befc552a32b4e75 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 15 Oct 2019 13:37:40 +0200 Subject: [PATCH 303/639] Move imports in yeelight + yeelightsunflower component (#27388) * Move imports in yeelight + yeelightsunflower component * Fix pylint * Fix pylint (again) --- homeassistant/components/yeelight/light.py | 62 ++++++++----------- .../components/yeelightsunflower/light.py | 2 +- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index ab63e6fb319..772fb00977b 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -2,8 +2,16 @@ import logging import voluptuous as vol -from yeelight import RGBTransition, SleepTransition, Flow, BulbException +import yeelight +from yeelight import ( + RGBTransition, + SleepTransition, + Flow, + BulbException, + transitions as yee_transitions, +) from yeelight.enums import PowerMode, LightType, BulbType, SceneClass + from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.service import extract_entity_ids import homeassistant.helpers.config_validation as cv @@ -190,8 +198,6 @@ SERVICE_SCHEMA_SET_AUTO_DELAY_OFF = YEELIGHT_SERVICE_SCHEMA.extend( def _transitions_config_parser(transitions): """Parse transitions config into initialized objects.""" - import yeelight - transition_objects = [] for transition_config in transitions: transition, params = list(transition_config.items())[0] @@ -652,39 +658,23 @@ class YeelightGenericLight(Light): def set_effect(self, effect) -> None: """Activate effect.""" if effect: - from yeelight.transitions import ( - disco, - temp, - strobe, - pulse, - strobe_color, - alarm, - police, - police2, - christmas, - rgb, - randomloop, - lsd, - slowdown, - ) - if effect == EFFECT_STOP: self._bulb.stop_flow(light_type=self.light_type) return effects_map = { - EFFECT_DISCO: disco, - EFFECT_TEMP: temp, - EFFECT_STROBE: strobe, - EFFECT_STROBE_COLOR: strobe_color, - EFFECT_ALARM: alarm, - EFFECT_POLICE: police, - EFFECT_POLICE2: police2, - EFFECT_CHRISTMAS: christmas, - EFFECT_RGB: rgb, - EFFECT_RANDOM_LOOP: randomloop, - EFFECT_LSD: lsd, - EFFECT_SLOWDOWN: slowdown, + EFFECT_DISCO: yee_transitions.disco, + EFFECT_TEMP: yee_transitions.temp, + EFFECT_STROBE: yee_transitions.strobe, + EFFECT_STROBE_COLOR: yee_transitions.strobe_color, + EFFECT_ALARM: yee_transitions.alarm, + EFFECT_POLICE: yee_transitions.police, + EFFECT_POLICE2: yee_transitions.police2, + EFFECT_CHRISTMAS: yee_transitions.christmas, + EFFECT_RGB: yee_transitions.rgb, + EFFECT_RANDOM_LOOP: yee_transitions.randomloop, + EFFECT_LSD: yee_transitions.lsd, + EFFECT_SLOWDOWN: yee_transitions.slowdown, } if effect in self.custom_effects_names: @@ -692,13 +682,15 @@ class YeelightGenericLight(Light): elif effect in effects_map: flow = Flow(count=0, transitions=effects_map[effect]()) elif effect == EFFECT_FAST_RANDOM_LOOP: - flow = Flow(count=0, transitions=randomloop(duration=250)) + flow = Flow( + count=0, transitions=yee_transitions.randomloop(duration=250) + ) elif effect == EFFECT_WHATSAPP: - flow = Flow(count=2, transitions=pulse(37, 211, 102)) + flow = Flow(count=2, transitions=yee_transitions.pulse(37, 211, 102)) elif effect == EFFECT_FACEBOOK: - flow = Flow(count=2, transitions=pulse(59, 89, 152)) + flow = Flow(count=2, transitions=yee_transitions.pulse(59, 89, 152)) elif effect == EFFECT_TWITTER: - flow = Flow(count=2, transitions=pulse(0, 172, 237)) + flow = Flow(count=2, transitions=yee_transitions.pulse(0, 172, 237)) try: self._bulb.start_flow(flow, light_type=self.light_type) diff --git a/homeassistant/components/yeelightsunflower/light.py b/homeassistant/components/yeelightsunflower/light.py index fa836f2776f..3424014e8f4 100644 --- a/homeassistant/components/yeelightsunflower/light.py +++ b/homeassistant/components/yeelightsunflower/light.py @@ -1,6 +1,7 @@ """Support for Yeelight Sunflower color bulbs (not Yeelight Blue or WiFi).""" import logging +import yeelightsunflower import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -24,7 +25,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yeelight Sunflower Light platform.""" - import yeelightsunflower host = config.get(CONF_HOST) hub = yeelightsunflower.Hub(host) From 16c18d303faede4bf790b53b4e4dd0d7bfe3cdc0 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 13:39:51 +0200 Subject: [PATCH 304/639] Move imports in bme680 component (#27506) * moved imports to top level * fixed pylint error * moved imports to top level * fixed import error --- homeassistant/components/bme680/sensor.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bme680/sensor.py b/homeassistant/components/bme680/sensor.py index a36b35ea9d4..5a1e9fd120f 100644 --- a/homeassistant/components/bme680/sensor.py +++ b/homeassistant/components/bme680/sensor.py @@ -1,14 +1,15 @@ """Support for BME680 Sensor over SMBus.""" -import importlib import logging +import threading +from time import sleep, time -from time import time, sleep - +from smbus import SMBus # pylint: disable=import-error +import bme680 # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_NAME, TEMP_FAHRENHEIT import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_FAHRENHEIT, CONF_NAME, CONF_MONITORED_CONDITIONS from homeassistant.helpers.entity import Entity from homeassistant.util.temperature import celsius_to_fahrenheit @@ -121,9 +122,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _setup_bme680(config): """Set up and configure the BME680 sensor.""" - from smbus import SMBus # pylint: disable=import-error - - bme680 = importlib.import_module("bme680") sensor_handler = None sensor = None @@ -224,7 +222,6 @@ class BME680Handler: self._gas_baseline = None if gas_measurement: - import threading threading.Thread( target=self._run_gas_sensor, From 40fbc3bd412f0987e4bf8b8aa3af3c71aadc0222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiit=20R=C3=A4tsep?= Date: Tue, 15 Oct 2019 15:05:10 +0300 Subject: [PATCH 305/639] Fix missing strings in soma config flow (#27689) --- homeassistant/components/soma/.translations/en.json | 12 +++++++++++- homeassistant/components/soma/strings.json | 10 ++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json index 5dea73fcc22..aa2f92f0be6 100644 --- a/homeassistant/components/soma/.translations/en.json +++ b/homeassistant/components/soma/.translations/en.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Successfully authenticated with Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Please enter connection settings of your SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } -} \ No newline at end of file +} diff --git a/homeassistant/components/soma/strings.json b/homeassistant/components/soma/strings.json index eac817ce119..aa2f92f0be6 100644 --- a/homeassistant/components/soma/strings.json +++ b/homeassistant/components/soma/strings.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Successfully authenticated with Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Please enter connection settings of your SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } From b22eb223589402aa9134b33e2bdac5c850255c49 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 14:26:04 +0200 Subject: [PATCH 306/639] moved imports to top level (#27695) --- homeassistant/components/dnsip/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index 0053d5a95ea..fb57040f2c2 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import aiodns +from aiodns.error import DNSError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -58,7 +60,6 @@ class WanIpSensor(Entity): def __init__(self, hass, name, hostname, resolver, ipv6): """Initialize the DNS IP sensor.""" - import aiodns self.hass = hass self._name = name @@ -80,7 +81,6 @@ class WanIpSensor(Entity): async def async_update(self): """Get the current DNS IP address for hostname.""" - from aiodns.error import DNSError try: response = await self.resolver.query(self.hostname, self.querytype) From 3e26b49cc2ccb9cfa8c3a8d7d5b694bc21edb7b2 Mon Sep 17 00:00:00 2001 From: Luca Angemi Date: Tue, 15 Oct 2019 14:26:39 +0200 Subject: [PATCH 307/639] Add battery status in owntracks (#27686) * Add battery status in owntracks * Remove trailing whitespaces --- homeassistant/components/owntracks/messages.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 7ef31be1327..465d2762f74 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -79,6 +79,8 @@ def _parse_see_args(message, subscribe_topic): kwargs["attributes"]["address"] = message["addr"] if "cog" in message: kwargs["attributes"]["course"] = message["cog"] + if "bs" in message: + kwargs["attributes"]["battery_status"] = message["bs"] if "t" in message: if message["t"] in ("c", "u"): kwargs["source_type"] = SOURCE_TYPE_GPS From 0e5f24d60cbe65e241f51f64302bc078ef22f966 Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 14:27:02 +0200 Subject: [PATCH 308/639] moved imports to top level (#27693) --- .../components/dlib_face_identify/image_processing.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dlib_face_identify/image_processing.py b/homeassistant/components/dlib_face_identify/image_processing.py index d4851be28c8..d5b55b6a68c 100644 --- a/homeassistant/components/dlib_face_identify/image_processing.py +++ b/homeassistant/components/dlib_face_identify/image_processing.py @@ -2,6 +2,8 @@ import logging import io +# pylint: disable=import-error +import face_recognition import voluptuous as vol from homeassistant.core import split_entity_id @@ -49,8 +51,6 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): def __init__(self, camera_entity, faces, name, tolerance): """Initialize Dlib face identify entry.""" - # pylint: disable=import-error - import face_recognition super().__init__() @@ -83,8 +83,6 @@ class DlibFaceIdentifyEntity(ImageProcessingFaceEntity): def process_image(self, image): """Process image.""" - # pylint: disable=import-error - import face_recognition fak_file = io.BytesIO(image) fak_file.name = "snapshot.jpg" From d534f30042cf530e3307f64c12610f2e32ad512c Mon Sep 17 00:00:00 2001 From: AaronDavidSchneider Date: Tue, 15 Oct 2019 17:11:17 +0200 Subject: [PATCH 309/639] Update fritzconnection requirement to 0.8.4 (#27698) * update fritzconnection requirement * update requierements for other components and requierements_all --- homeassistant/components/fritz/manifest.json | 2 +- homeassistant/components/fritzbox_callmonitor/manifest.json | 2 +- homeassistant/components/fritzbox_netmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/fritz/manifest.json b/homeassistant/components/fritz/manifest.json index e6c1fee2c95..15a3406891f 100644 --- a/homeassistant/components/fritz/manifest.json +++ b/homeassistant/components/fritz/manifest.json @@ -3,7 +3,7 @@ "name": "Fritz", "documentation": "https://www.home-assistant.io/integrations/fritz", "requirements": [ - "fritzconnection==0.6.5" + "fritzconnection==0.8.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 35c27b7ca84..f85f16d6c0d 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Fritzbox callmonitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", "requirements": [ - "fritzconnection==0.6.5" + "fritzconnection==0.8.4" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/fritzbox_netmonitor/manifest.json b/homeassistant/components/fritzbox_netmonitor/manifest.json index 88a7ab5a338..9afaa71e699 100644 --- a/homeassistant/components/fritzbox_netmonitor/manifest.json +++ b/homeassistant/components/fritzbox_netmonitor/manifest.json @@ -3,7 +3,7 @@ "name": "Fritzbox netmonitor", "documentation": "https://www.home-assistant.io/integrations/fritzbox_netmonitor", "requirements": [ - "fritzconnection==0.6.5" + "fritzconnection==0.8.4" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index ef07a3f44b7..ea3c8e50f93 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -516,7 +516,7 @@ freesms==0.1.2 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor # homeassistant.components.fritzbox_netmonitor -# fritzconnection==0.6.5 +# fritzconnection==0.8.4 # homeassistant.components.fritzdect fritzhome==1.0.4 From 26d19f9e1c26f582bc7a835235af98e39d5143c1 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 15 Oct 2019 17:12:12 +0200 Subject: [PATCH 310/639] Moved imports to top-level in spotify integration (#27703) --- homeassistant/components/spotify/media_player.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 31fdc09af80..236c8b8db89 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -3,11 +3,14 @@ from datetime import timedelta import logging import random +import spotipy +import spotipy.oauth2 import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, SUPPORT_NEXT_TRACK, @@ -18,7 +21,6 @@ from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_SHUFFLE_SET, SUPPORT_VOLUME_SET, - ATTR_MEDIA_CONTENT_ID, ) from homeassistant.const import CONF_NAME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import callback @@ -97,7 +99,6 @@ def request_configuration(hass, config, add_entities, oauth): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Spotify platform.""" - import spotipy.oauth2 callback_url = f"{hass.config.api.base_url}{AUTH_CALLBACK_PATH}" cache = config.get(CONF_CACHE_PATH, hass.config.path(DEFAULT_CACHE_PATH)) @@ -181,7 +182,6 @@ class SpotifyMediaPlayer(MediaPlayerDevice): def refresh_spotify_instance(self): """Fetch a new spotify instance.""" - import spotipy token_refreshed = False need_token = self._token_info is None or self._oauth.is_token_expired( From 6f894d2dec988b6bee8f0df117efeea98a47629c Mon Sep 17 00:00:00 2001 From: bouni Date: Tue, 15 Oct 2019 17:12:32 +0200 Subject: [PATCH 311/639] moved imports to top level (#27679) --- homeassistant/components/discogs/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/discogs/sensor.py b/homeassistant/components/discogs/sensor.py index 64528f4ca5e..b3f29fbe75b 100644 --- a/homeassistant/components/discogs/sensor.py +++ b/homeassistant/components/discogs/sensor.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging import random +import discogs_client import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -65,19 +66,18 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Discogs sensor.""" - import discogs_client token = config[CONF_TOKEN] name = config[CONF_NAME] try: - discogs_client = discogs_client.Client(SERVER_SOFTWARE, user_token=token) + _discogs_client = discogs_client.Client(SERVER_SOFTWARE, user_token=token) discogs_data = { - "user": discogs_client.identity().name, - "folders": discogs_client.identity().collection_folders, - "collection_count": discogs_client.identity().num_collection, - "wantlist_count": discogs_client.identity().num_wantlist, + "user": _discogs_client.identity().name, + "folders": _discogs_client.identity().collection_folders, + "collection_count": _discogs_client.identity().num_collection, + "wantlist_count": _discogs_client.identity().num_wantlist, } except discogs_client.exceptions.HTTPError: _LOGGER.error("API token is not valid") From a591d78efe4bdbac966c301494b674c0f2ad7493 Mon Sep 17 00:00:00 2001 From: quthla Date: Tue, 15 Oct 2019 17:21:40 +0200 Subject: [PATCH 312/639] Bump PyMata to 2.20 (#27431) * Bump PyMata to 2.20 * Bump PyMata to 2.20 --- homeassistant/components/arduino/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/arduino/manifest.json b/homeassistant/components/arduino/manifest.json index 3567ce71cd1..a29f65700ff 100644 --- a/homeassistant/components/arduino/manifest.json +++ b/homeassistant/components/arduino/manifest.json @@ -3,7 +3,7 @@ "name": "Arduino", "documentation": "https://www.home-assistant.io/integrations/arduino", "requirements": [ - "PyMata==2.14" + "PyMata==2.20" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ea3c8e50f93..567acd712a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -56,7 +56,7 @@ PyISY==1.1.2 PyMVGLive==1.1.4 # homeassistant.components.arduino -PyMata==2.14 +PyMata==2.20 # homeassistant.components.mobile_app # homeassistant.components.owntracks From c700085490b23a4a73cb48243ea6c692b1f27a13 Mon Sep 17 00:00:00 2001 From: Rolf K Date: Tue, 15 Oct 2019 17:37:15 +0200 Subject: [PATCH 313/639] Add improved scene support to input_text (#27687) * Add improved scene support for input_text. * Add tests for reproducing input_text states. * Add some comments. --- .../components/input_text/reproduce_state.py | 46 +++++++++++++ .../input_text/test_reproduce_state.py | 65 +++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 homeassistant/components/input_text/reproduce_state.py create mode 100644 tests/components/input_text/test_reproduce_state.py diff --git a/homeassistant/components/input_text/reproduce_state.py b/homeassistant/components/input_text/reproduce_state.py new file mode 100644 index 00000000000..f64c5c019f6 --- /dev/null +++ b/homeassistant/components/input_text/reproduce_state.py @@ -0,0 +1,46 @@ +"""Reproduce an Input text state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN, SERVICE_SET_VALUE, ATTR_VALUE + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + # Return if we can't find the entity + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + # Call service + service = SERVICE_SET_VALUE + service_data = {ATTR_ENTITY_ID: state.entity_id, ATTR_VALUE: state.state} + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input text states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/input_text/test_reproduce_state.py b/tests/components/input_text/test_reproduce_state.py new file mode 100644 index 00000000000..fd75948d461 --- /dev/null +++ b/tests/components/input_text/test_reproduce_state.py @@ -0,0 +1,65 @@ +"""Test reproduce state for Input text.""" +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +VALID_TEXT1 = "Test text" +VALID_TEXT2 = "LoremIpsum" +INVALID_TEXT1 = "This text is too long!" +INVALID_TEXT2 = "Short" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input text states.""" + + # Setup entity for testing + assert await async_setup_component( + hass, + "input_text", + { + "input_text": { + "test_text": {"min": "6", "max": "10", "initial": VALID_TEXT1} + } + }, + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("input_text.test_text", VALID_TEXT1), + # Should not raise + State("input_text.non_existing", VALID_TEXT1), + ], + blocking=True, + ) + + # Test that entity is in desired state + assert hass.states.get("input_text.test_text").state == VALID_TEXT1 + + # Try reproducing with different state + await hass.helpers.state.async_reproduce_state( + [ + State("input_text.test_text", VALID_TEXT2), + # Should not raise + State("input_text.non_existing", VALID_TEXT2), + ], + blocking=True, + ) + + # Test that the state was changed + assert hass.states.get("input_text.test_text").state == VALID_TEXT2 + + # Test setting state to invalid state (length too long) + await hass.helpers.state.async_reproduce_state( + [State("input_text.test_text", INVALID_TEXT1)], blocking=True + ) + + # The entity state should be unchanged + assert hass.states.get("input_text.test_text").state == VALID_TEXT2 + + # Test setting state to invalid state (length too short) + await hass.helpers.state.async_reproduce_state( + [State("input_text.test_text", INVALID_TEXT2)], blocking=True + ) + + # The entity state should be unchanged + assert hass.states.get("input_text.test_text").state == VALID_TEXT2 From 93f9afcd21bcadc4d37655206eb5831af7f2c4d1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 15 Oct 2019 16:15:26 -0700 Subject: [PATCH 314/639] Fix config imports (#27669) * Fix config imports * Remove old migration * Remove migrate tests --- homeassistant/components/config/automation.py | 5 +- homeassistant/components/config/group.py | 10 ++- homeassistant/components/config/script.py | 5 +- homeassistant/config.py | 24 ++----- tests/test_config.py | 67 +------------------ 5 files changed, 21 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/config/automation.py b/homeassistant/components/config/automation.py index 97ddf1e0714..0e9b4053b7b 100644 --- a/homeassistant/components/config/automation.py +++ b/homeassistant/components/config/automation.py @@ -5,12 +5,11 @@ import uuid from homeassistant.components.automation import DOMAIN, PLATFORM_SCHEMA from homeassistant.components.automation.config import async_validate_config_item from homeassistant.const import CONF_ID, SERVICE_RELOAD +from homeassistant.config import AUTOMATION_CONFIG_PATH import homeassistant.helpers.config_validation as cv from . import EditIdBasedConfigView -CONFIG_PATH = "automations.yaml" - async def async_setup(hass): """Set up the Automation config API.""" @@ -23,7 +22,7 @@ async def async_setup(hass): EditAutomationConfigView( DOMAIN, "config", - CONFIG_PATH, + AUTOMATION_CONFIG_PATH, cv.string, PLATFORM_SCHEMA, post_write_hook=hook, diff --git a/homeassistant/components/config/group.py b/homeassistant/components/config/group.py index 371bd98cf08..d104cd2e1df 100644 --- a/homeassistant/components/config/group.py +++ b/homeassistant/components/config/group.py @@ -1,12 +1,11 @@ """Provide configuration end points for Groups.""" from homeassistant.components.group import DOMAIN, GROUP_SCHEMA from homeassistant.const import SERVICE_RELOAD +from homeassistant.config import GROUP_CONFIG_PATH import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView -CONFIG_PATH = "groups.yaml" - async def async_setup(hass): """Set up the Group config API.""" @@ -17,7 +16,12 @@ async def async_setup(hass): hass.http.register_view( EditKeyBasedConfigView( - "group", "config", CONFIG_PATH, cv.slug, GROUP_SCHEMA, post_write_hook=hook + "group", + "config", + GROUP_CONFIG_PATH, + cv.slug, + GROUP_SCHEMA, + post_write_hook=hook, ) ) return True diff --git a/homeassistant/components/config/script.py b/homeassistant/components/config/script.py index 8ce163745f1..e63651d8f2a 100644 --- a/homeassistant/components/config/script.py +++ b/homeassistant/components/config/script.py @@ -1,12 +1,11 @@ """Provide configuration end points for scripts.""" from homeassistant.components.script import DOMAIN, SCRIPT_ENTRY_SCHEMA from homeassistant.const import SERVICE_RELOAD +from homeassistant.config import SCRIPT_CONFIG_PATH import homeassistant.helpers.config_validation as cv from . import EditKeyBasedConfigView -CONFIG_PATH = "scripts.yaml" - async def async_setup(hass): """Set up the script config API.""" @@ -19,7 +18,7 @@ async def async_setup(hass): EditKeyBasedConfigView( "script", "config", - CONFIG_PATH, + SCRIPT_CONFIG_PATH, cv.slug, SCRIPT_ENTRY_SCHEMA, post_write_hook=hook, diff --git a/homeassistant/config.py b/homeassistant/config.py index 27137c08f1a..9f49889791e 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -66,9 +66,11 @@ VERSION_FILE = ".HA_VERSION" CONFIG_DIR_NAME = ".homeassistant" DATA_CUSTOMIZE = "hass_customize" -FILE_MIGRATION = (("ios.conf", ".ios.conf"),) +GROUP_CONFIG_PATH = "groups.yaml" +AUTOMATION_CONFIG_PATH = "automations.yaml" +SCRIPT_CONFIG_PATH = "scripts.yaml" -DEFAULT_CONFIG = """ +DEFAULT_CONFIG = f""" # Configure a default setup of Home Assistant (frontend, api, etc) default_config: @@ -80,9 +82,9 @@ default_config: tts: - platform: google_translate -group: !include groups.yaml -automation: !include automations.yaml -script: !include scripts.yaml +group: !include {GROUP_CONFIG_PATH} +automation: !include {AUTOMATION_CONFIG_PATH} +script: !include {SCRIPT_CONFIG_PATH} """ DEFAULT_SECRETS = """ # Use this file to store secrets like usernames and passwords. @@ -253,12 +255,6 @@ async def async_create_default_config( def _write_default_config(config_dir: str) -> Optional[str]: """Write the default config.""" - from homeassistant.components.config.group import CONFIG_PATH as GROUP_CONFIG_PATH - from homeassistant.components.config.automation import ( - CONFIG_PATH as AUTOMATION_CONFIG_PATH, - ) - from homeassistant.components.config.script import CONFIG_PATH as SCRIPT_CONFIG_PATH - config_path = os.path.join(config_dir, YAML_CONFIG_FILE) secret_path = os.path.join(config_dir, SECRET_YAML) version_path = os.path.join(config_dir, VERSION_FILE) @@ -407,12 +403,6 @@ def process_ha_config_upgrade(hass: HomeAssistant) -> None: with open(version_path, "wt") as outp: outp.write(__version__) - _LOGGER.debug("Migrating old system configuration files to new locations") - for oldf, newf in FILE_MIGRATION: - if os.path.isfile(hass.config.path(oldf)): - _LOGGER.info("Migrating %s to %s", oldf, newf) - os.rename(hass.config.path(oldf), hass.config.path(newf)) - @callback def async_log_exception( diff --git a/tests/test_config.py b/tests/test_config.py index 362608e7af2..dab51f59176 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -33,11 +33,6 @@ from homeassistant.const import ( from homeassistant.util import dt as dt_util from homeassistant.util.yaml import SECRET_YAML from homeassistant.helpers.entity import Entity -from homeassistant.components.config.group import CONFIG_PATH as GROUP_CONFIG_PATH -from homeassistant.components.config.automation import ( - CONFIG_PATH as AUTOMATIONS_CONFIG_PATH, -) -from homeassistant.components.config.script import CONFIG_PATH as SCRIPTS_CONFIG_PATH import homeassistant.helpers.check_config as check_config from tests.common import get_test_config_dir, patch_yaml_files @@ -46,9 +41,9 @@ CONFIG_DIR = get_test_config_dir() YAML_PATH = os.path.join(CONFIG_DIR, config_util.YAML_CONFIG_FILE) SECRET_PATH = os.path.join(CONFIG_DIR, SECRET_YAML) VERSION_PATH = os.path.join(CONFIG_DIR, config_util.VERSION_FILE) -GROUP_PATH = os.path.join(CONFIG_DIR, GROUP_CONFIG_PATH) -AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, AUTOMATIONS_CONFIG_PATH) -SCRIPTS_PATH = os.path.join(CONFIG_DIR, SCRIPTS_CONFIG_PATH) +GROUP_PATH = os.path.join(CONFIG_DIR, config_util.GROUP_CONFIG_PATH) +AUTOMATIONS_PATH = os.path.join(CONFIG_DIR, config_util.AUTOMATION_CONFIG_PATH) +SCRIPTS_PATH = os.path.join(CONFIG_DIR, config_util.SCRIPT_CONFIG_PATH) ORIG_TIMEZONE = dt_util.DEFAULT_TIME_ZONE @@ -345,62 +340,6 @@ def test_config_upgrade_no_file(hass): assert opened_file.write.call_args == mock.call(__version__) -@mock.patch("homeassistant.config.shutil") -@mock.patch("homeassistant.config.os") -@mock.patch("homeassistant.config.find_config_file", mock.Mock()) -def test_migrate_file_on_upgrade(mock_os, mock_shutil, hass): - """Test migrate of config files on upgrade.""" - ha_version = "0.7.0" - - mock_os.path.isdir = mock.Mock(return_value=True) - - mock_open = mock.mock_open() - - def _mock_isfile(filename): - return True - - with mock.patch("homeassistant.config.open", mock_open, create=True), mock.patch( - "homeassistant.config.os.path.isfile", _mock_isfile - ): - opened_file = mock_open.return_value - # pylint: disable=no-member - opened_file.readline.return_value = ha_version - - hass.config.path = mock.Mock() - - config_util.process_ha_config_upgrade(hass) - - assert mock_os.rename.call_count == 1 - - -@mock.patch("homeassistant.config.shutil") -@mock.patch("homeassistant.config.os") -@mock.patch("homeassistant.config.find_config_file", mock.Mock()) -def test_migrate_no_file_on_upgrade(mock_os, mock_shutil, hass): - """Test not migrating config files on upgrade.""" - ha_version = "0.7.0" - - mock_os.path.isdir = mock.Mock(return_value=True) - - mock_open = mock.mock_open() - - def _mock_isfile(filename): - return False - - with mock.patch("homeassistant.config.open", mock_open, create=True), mock.patch( - "homeassistant.config.os.path.isfile", _mock_isfile - ): - opened_file = mock_open.return_value - # pylint: disable=no-member - opened_file.readline.return_value = ha_version - - hass.config.path = mock.Mock() - - config_util.process_ha_config_upgrade(hass) - - assert mock_os.rename.call_count == 0 - - async def test_loading_configuration_from_storage(hass, hass_storage): """Test loading core config onto hass object.""" hass_storage["core.config"] = { From 8720ca38b58ec2739a1a5732e49299cb6c029cde Mon Sep 17 00:00:00 2001 From: Rolf K Date: Wed, 16 Oct 2019 01:15:42 +0200 Subject: [PATCH 315/639] Add improved scene support for input_select (#27697) * Add improved scene support for input_select * Add tests for reproducing input_select states. * Add some comments. * Add support for set_options Allows defining the options for an input_select in a scene. * Add tests for set_options in test_reproduce_state * Execute for real instead of mock execution. --- .../input_select/reproduce_state.py | 80 +++++++++++++++++++ .../input_select/test_reproduce_state.py | 72 +++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 homeassistant/components/input_select/reproduce_state.py create mode 100644 tests/components/input_select/test_reproduce_state.py diff --git a/homeassistant/components/input_select/reproduce_state.py b/homeassistant/components/input_select/reproduce_state.py new file mode 100644 index 00000000000..657f518cd3d --- /dev/null +++ b/homeassistant/components/input_select/reproduce_state.py @@ -0,0 +1,80 @@ +"""Reproduce an Input select state.""" +import asyncio +import logging +from types import MappingProxyType +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + DOMAIN, + SERVICE_SELECT_OPTION, + SERVICE_SET_OPTIONS, + ATTR_OPTION, + ATTR_OPTIONS, +) + +ATTR_GROUP = [ATTR_OPTION, ATTR_OPTIONS] + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + # Return if we can't find entity + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and all( + check_attr_equal(cur_state.attributes, state.attributes, attr) + for attr in ATTR_GROUP + ): + return + + # Set service data + service_data = {ATTR_ENTITY_ID: state.entity_id} + + # If options are specified, call SERVICE_SET_OPTIONS + if ATTR_OPTIONS in state.attributes: + service = SERVICE_SET_OPTIONS + service_data[ATTR_OPTIONS] = state.attributes[ATTR_OPTIONS] + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + # Remove ATTR_OPTIONS from service_data so we can reuse service_data in next call + del service_data[ATTR_OPTIONS] + + # Call SERVICE_SELECT_OPTION + service = SERVICE_SELECT_OPTION + service_data[ATTR_OPTION] = state.state + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input select states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) + + +def check_attr_equal( + attr1: MappingProxyType, attr2: MappingProxyType, attr_str: str +) -> bool: + """Return true if the given attributes are equal.""" + return attr1.get(attr_str) == attr2.get(attr_str) diff --git a/tests/components/input_select/test_reproduce_state.py b/tests/components/input_select/test_reproduce_state.py new file mode 100644 index 00000000000..469c258cb4b --- /dev/null +++ b/tests/components/input_select/test_reproduce_state.py @@ -0,0 +1,72 @@ +"""Test reproduce state for Input select.""" +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +VALID_OPTION1 = "Option A" +VALID_OPTION2 = "Option B" +VALID_OPTION3 = "Option C" +VALID_OPTION4 = "Option D" +VALID_OPTION5 = "Option E" +VALID_OPTION6 = "Option F" +INVALID_OPTION = "Option X" +VALID_OPTION_SET1 = [VALID_OPTION1, VALID_OPTION2, VALID_OPTION3] +VALID_OPTION_SET2 = [VALID_OPTION4, VALID_OPTION5, VALID_OPTION6] +ENTITY = "input_select.test_select" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input select states.""" + + # Setup entity + assert await async_setup_component( + hass, + "input_select", + { + "input_select": { + "test_select": {"options": VALID_OPTION_SET1, "initial": VALID_OPTION1} + } + }, + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State(ENTITY, VALID_OPTION1), + # Should not raise + State("input_select.non_existing", VALID_OPTION1), + ], + blocking=True, + ) + + # Test that entity is in desired state + assert hass.states.get(ENTITY).state == VALID_OPTION1 + + # Try reproducing with different state + await hass.helpers.state.async_reproduce_state( + [ + State(ENTITY, VALID_OPTION3), + # Should not raise + State("input_select.non_existing", VALID_OPTION3), + ], + blocking=True, + ) + + # Test that we got the desired result + assert hass.states.get(ENTITY).state == VALID_OPTION3 + + # Test setting state to invalid state + await hass.helpers.state.async_reproduce_state( + [State(ENTITY, INVALID_OPTION)], blocking=True + ) + + # The entity state should be unchanged + assert hass.states.get(ENTITY).state == VALID_OPTION3 + + # Test setting a different option set + await hass.helpers.state.async_reproduce_state( + [State(ENTITY, VALID_OPTION5, {"options": VALID_OPTION_SET2})], blocking=True + ) + + # These should fail if options weren't changed to VALID_OPTION_SET2 + assert hass.states.get(ENTITY).attributes == {"options": VALID_OPTION_SET2} + assert hass.states.get(ENTITY).state == VALID_OPTION5 From a58d2429091e2dc582f5623cb289b43ce9465622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Wed, 16 Oct 2019 01:17:09 +0200 Subject: [PATCH 316/639] move imports in sony_projector component (#27718) --- homeassistant/components/sony_projector/switch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sony_projector/switch.py b/homeassistant/components/sony_projector/switch.py index 43a4b7bc0fe..e68bed34cfa 100644 --- a/homeassistant/components/sony_projector/switch.py +++ b/homeassistant/components/sony_projector/switch.py @@ -1,10 +1,11 @@ """Support for Sony projectors via SDCP network control.""" import logging +import pysdcp import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA -from homeassistant.const import STATE_ON, STATE_OFF, CONF_NAME, CONF_HOST +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_OFF, STATE_ON import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -21,7 +22,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Connect to Sony projector using network.""" - import pysdcp host = config[CONF_HOST] name = config[CONF_NAME] From b2f6931bbed3821841aef5326e9b13fdadfe63e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Wed, 16 Oct 2019 01:20:59 +0200 Subject: [PATCH 317/639] move imports in speedtestdotnet component (#27716) --- homeassistant/components/speedtestdotnet/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 029356cb082..afccc71d285 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -1,15 +1,17 @@ """Support for testing internet speed via Speedtest.net.""" -import logging from datetime import timedelta +import logging +import speedtest import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval + from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -72,7 +74,6 @@ class SpeedtestData: def update(self, now=None): """Get the latest data from speedtest.net.""" - import speedtest _LOGGER.debug("Executing speedtest.net speed test") speed = speedtest.Speedtest() From d4692367c5e36b0f1f69155e5bd2b105be4d479a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Wed, 16 Oct 2019 01:21:19 +0200 Subject: [PATCH 318/639] move imports in spotcrime component (#27715) --- homeassistant/components/spotcrime/sensor.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/spotcrime/sensor.py b/homeassistant/components/spotcrime/sensor.py index fc3a7592af3..2edaa3cf933 100644 --- a/homeassistant/components/spotcrime/sensor.py +++ b/homeassistant/components/spotcrime/sensor.py @@ -1,27 +1,28 @@ """Sensor for Spot Crime.""" -from datetime import timedelta from collections import defaultdict +from datetime import timedelta import logging +import spotcrime import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_INCLUDE, - CONF_EXCLUDE, - CONF_NAME, - CONF_LATITUDE, - CONF_LONGITUDE, ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_API_KEY, + CONF_EXCLUDE, + CONF_INCLUDE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, CONF_RADIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -75,7 +76,6 @@ class SpotCrimeSensor(Entity): self, name, latitude, longitude, radius, include, exclude, api_key, days ): """Initialize the Spot Crime sensor.""" - import spotcrime self._name = name self._include = include From 2b92fd3422e339c5659c25db2537e9c8bf252bf0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 16 Oct 2019 01:22:42 +0200 Subject: [PATCH 319/639] Moved imports to top-level in fritzbox_callmonitor component (#27705) --- .../components/fritzbox_callmonitor/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 4dada44f4e5..b1d601ce382 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -1,24 +1,25 @@ """Sensor to monitor incoming/outgoing phone calls on a Fritz!Box router.""" +import datetime import logging +import re import socket import threading -import datetime import time -import re +import fritzconnection as fc # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, - CONF_PORT, CONF_NAME, CONF_PASSWORD, + CONF_PORT, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -248,8 +249,6 @@ class FritzBoxPhonebook: self.number_dict = None self.prefixes = prefixes or [] - import fritzconnection as fc # pylint: disable=import-error - # Establish a connection to the FRITZ!Box. self.fph = fc.FritzPhonebook( address=self.host, user=self.username, password=self.password From 04a5f19f6aaad6e6518666d707b0d5cc1ab9c18a Mon Sep 17 00:00:00 2001 From: bouni Date: Wed, 16 Oct 2019 01:24:18 +0200 Subject: [PATCH 320/639] moved imports to top level (#27696) --- homeassistant/components/dovado/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/dovado/__init__.py b/homeassistant/components/dovado/__init__.py index 29f0cc59392..a13c49cc61a 100644 --- a/homeassistant/components/dovado/__init__.py +++ b/homeassistant/components/dovado/__init__.py @@ -2,6 +2,7 @@ import logging from datetime import timedelta +import dovado import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -32,7 +33,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) def setup(hass, config): """Set up the Dovado component.""" - import dovado hass.data[DOMAIN] = DovadoData( dovado.Dovado( From b8e00925e7139c44ec8e3fcc832108e771ca29e7 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 16 Oct 2019 00:32:17 +0000 Subject: [PATCH 321/639] [ci skip] Translation update --- .../components/abode/.translations/de.json | 22 +++++ .../components/abode/.translations/fr.json | 12 +++ .../components/abode/.translations/nl.json | 22 +++++ .../components/airly/.translations/de.json | 6 +- .../binary_sensor/.translations/de.json | 89 ++++++++++++++++++- .../binary_sensor/.translations/nl.json | 58 ++++++++++++ .../components/cover/.translations/de.json | 10 +++ .../components/cover/.translations/nl.json | 10 +++ .../components/deconz/.translations/de.json | 21 +++++ .../components/deconz/.translations/lb.json | 1 + .../components/deconz/.translations/nl.json | 2 + .../components/deconz/.translations/ru.json | 1 + .../deconz/.translations/zh-Hant.json | 1 + .../components/ecobee/.translations/de.json | 18 +++- .../components/ecobee/.translations/nl.json | 15 ++++ .../iaqualink/.translations/de.json | 21 +++++ .../components/izone/.translations/de.json | 15 ++++ .../components/lock/.translations/de.json | 8 ++ .../components/lock/.translations/nl.json | 8 ++ .../components/met/.translations/nl.json | 2 +- .../components/neato/.translations/nl.json | 20 +++++ .../opentherm_gw/.translations/de.json | 10 ++- .../opentherm_gw/.translations/nl.json | 3 +- .../components/plex/.translations/de.json | 38 ++++++-- .../components/plex/.translations/nl.json | 35 ++++++++ .../components/sensor/.translations/de.json | 5 ++ .../components/sensor/.translations/nl.json | 17 ++++ .../solaredge/.translations/de.json | 21 +++++ .../components/soma/.translations/de.json | 6 +- .../components/soma/.translations/en.json | 2 +- .../components/soma/.translations/lb.json | 10 +++ .../components/soma/.translations/nl.json | 7 ++ .../components/soma/.translations/no.json | 10 +++ .../components/soma/.translations/ru.json | 10 +++ .../components/switch/.translations/de.json | 19 ++++ .../transmission/.translations/de.json | 9 +- .../transmission/.translations/nl.json | 16 ++++ .../components/unifi/.translations/nl.json | 5 ++ .../components/withings/.translations/de.json | 3 + .../components/zha/.translations/de.json | 37 ++++++++ .../components/zha/.translations/nl.json | 8 ++ 41 files changed, 614 insertions(+), 19 deletions(-) create mode 100644 homeassistant/components/abode/.translations/de.json create mode 100644 homeassistant/components/abode/.translations/fr.json create mode 100644 homeassistant/components/abode/.translations/nl.json create mode 100644 homeassistant/components/binary_sensor/.translations/nl.json create mode 100644 homeassistant/components/cover/.translations/de.json create mode 100644 homeassistant/components/cover/.translations/nl.json create mode 100644 homeassistant/components/ecobee/.translations/nl.json create mode 100644 homeassistant/components/iaqualink/.translations/de.json create mode 100644 homeassistant/components/izone/.translations/de.json create mode 100644 homeassistant/components/lock/.translations/de.json create mode 100644 homeassistant/components/lock/.translations/nl.json create mode 100644 homeassistant/components/neato/.translations/nl.json create mode 100644 homeassistant/components/plex/.translations/nl.json create mode 100644 homeassistant/components/sensor/.translations/nl.json create mode 100644 homeassistant/components/solaredge/.translations/de.json create mode 100644 homeassistant/components/soma/.translations/nl.json create mode 100644 homeassistant/components/switch/.translations/de.json create mode 100644 homeassistant/components/transmission/.translations/nl.json diff --git a/homeassistant/components/abode/.translations/de.json b/homeassistant/components/abode/.translations/de.json new file mode 100644 index 00000000000..ed5ec85a5d7 --- /dev/null +++ b/homeassistant/components/abode/.translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Abode erlaubt." + }, + "error": { + "connection_error": "Es kann keine Verbindung zu Abode hergestellt werden.", + "identifier_exists": "Das Konto ist bereits registriert.", + "invalid_credentials": "Ung\u00fcltige Anmeldeinformationen" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "E-Mail-Adresse" + }, + "title": "Gib deine Abode-Anmeldeinformationen ein" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/fr.json b/homeassistant/components/abode/.translations/fr.json new file mode 100644 index 00000000000..c2e4b241b90 --- /dev/null +++ b/homeassistant/components/abode/.translations/fr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Adresse e-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/nl.json b/homeassistant/components/abode/.translations/nl.json new file mode 100644 index 00000000000..89b5ae0c4a5 --- /dev/null +++ b/homeassistant/components/abode/.translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Slechts een enkele configuratie van Abode is toegestaan." + }, + "error": { + "connection_error": "Kan geen verbinding maken met Abode.", + "identifier_exists": "Account is al geregistreerd.", + "invalid_credentials": "Ongeldige inloggegevens." + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "username": "E-mailadres" + }, + "title": "Vul uw Abode-inloggegevens in" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/de.json b/homeassistant/components/airly/.translations/de.json index cb290dc46c0..83c23a90389 100644 --- a/homeassistant/components/airly/.translations/de.json +++ b/homeassistant/components/airly/.translations/de.json @@ -1,15 +1,19 @@ { "config": { "error": { - "name_exists": "Name existiert bereits" + "auth": "Der API-Schl\u00fcssel ist nicht korrekt.", + "name_exists": "Name existiert bereits", + "wrong_location": "Keine Airly Luftmessstation an diesem Ort" }, "step": { "user": { "data": { + "api_key": "Airly API-Schl\u00fcssel", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", "name": "Name der Integration" }, + "description": "Einrichtung der Airly-Luftqualit\u00e4t Integration. Um einen API-Schl\u00fcssel zu generieren, registriere dich auf https://developer.airly.eu/register", "title": "Airly" } }, diff --git a/homeassistant/components/binary_sensor/.translations/de.json b/homeassistant/components/binary_sensor/.translations/de.json index 25e8ea2f86b..e246198864b 100644 --- a/homeassistant/components/binary_sensor/.translations/de.json +++ b/homeassistant/components/binary_sensor/.translations/de.json @@ -1,7 +1,94 @@ { "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} Batterie ist schwach", + "is_cold": "{entity_name} ist kalt", + "is_connected": "{entity_name} ist verbunden", + "is_gas": "{entity_name} erkennt Gas", + "is_hot": "{entity_name} ist hei\u00df", + "is_light": "{entity_name} erkennt Licht", + "is_locked": "{entity_name} ist gesperrt", + "is_moist": "{entity_name} ist feucht", + "is_motion": "{entity_name} erkennt Bewegung", + "is_moving": "{entity_name} bewegt sich", + "is_no_gas": "{entity_name} erkennt kein Gas", + "is_no_light": "{entity_name} erkennt kein Licht", + "is_no_motion": "{entity_name} erkennt keine Bewegung", + "is_no_problem": "{entity_name} erkennt kein Problem", + "is_no_smoke": "{entity_name} erkennt keinen Rauch", + "is_no_sound": "{entity_name} erkennt keine Ger\u00e4usche", + "is_no_vibration": "{entity_name} erkennt keine Vibrationen", + "is_not_bat_low": "{entity_name} Batterie ist normal", + "is_not_cold": "{entity_name} ist nicht kalt", + "is_not_connected": "{entity_name} ist nicht verbunden", + "is_not_hot": "{entity_name} ist nicht hei\u00df", + "is_not_locked": "{entity_name} ist entsperrt", + "is_not_moist": "{entity_name} ist trocken", + "is_not_moving": "{entity_name} bewegt sich nicht", + "is_not_occupied": "{entity_name} ist nicht besch\u00e4ftigt / besetzt", + "is_not_open": "{entity_name} ist geschlossen", + "is_not_plugged_in": "{entity_name} ist nicht angeschlossen", + "is_not_powered": "{entity_name} wird nicht mit Strom versorgt", + "is_not_present": "{entity_name} ist nicht vorhanden", + "is_not_unsafe": "{entity_name} ist sicher", + "is_occupied": "{entity_name} ist besch\u00e4ftigt / besetzt", + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet", + "is_open": "{entity_name} ist offen", + "is_plugged_in": "{entity_name} ist eingesteckt", + "is_powered": "{entity_name} wird mit Strom versorgt", + "is_present": "{entity_name} ist vorhanden", + "is_problem": "{entity_name} hat ein Problem festgestellt", + "is_smoke": "{entity_name} hat Rauch detektiert", + "is_sound": "{entity_name} hat Ger\u00e4usche detektiert", + "is_unsafe": "{entity_name} ist unsicher", + "is_vibration": "{entity_name} erkennt Vibrationen." + }, "trigger_type": { - "plugged_in": "{entity_name} eingesteckt" + "bat_low": "{entity_name} Batterie schwach", + "closed": "{entity_name} geschlossen", + "cold": "{entity_name} wurde kalt", + "connected": "{entity_name} verbunden", + "gas": "{entity_name} hat Gas detektiert", + "hot": "{entity_name} wurde hei\u00df", + "light": "{entity_name} hat Licht detektiert", + "locked": "{entity_name} gesperrt", + "moist": "{entity_name} wurde feucht", + "moist\u00a7": "{entity_name} wurde feucht", + "motion": "{entity_name} hat Bewegungen detektiert", + "moving": "{entity_name} hat angefangen sich zu bewegen", + "no_gas": "{entity_name} hat kein Gas mehr erkannt", + "no_light": "{entity_name} hat kein Licht mehr erkannt", + "no_motion": "{entity_name} hat keine Bewegung mehr erkannt", + "no_problem": "{entity_name} hat kein Problem mehr erkannt", + "no_smoke": "{entity_name} hat keinen Rauch mehr erkannt", + "no_sound": "{entity_name} hat keine Ger\u00e4usche mehr erkannt", + "no_vibration": "{entity_name}hat keine Vibrationen mehr erkannt", + "not_bat_low": "{entity_name} Batterie normal", + "not_cold": "{entity_name} w\u00e4rmte auf", + "not_connected": "{entity_name} getrennt", + "not_hot": "{entity_name} k\u00fchlte ab", + "not_locked": "{entity_name} entsperrt", + "not_moist": "{entity_name} wurde trocken", + "not_moving": "{entity_name} bewegt sich nicht mehr", + "not_occupied": "{entity_name} wurde frei / inaktiv", + "not_opened": "{entity_name} geschlossen", + "not_plugged_in": "{entity_name} ist nicht angeschlossen", + "not_powered": "{entity_name} nicht mit Strom versorgt", + "not_present": "{entity_name} nicht anwesend", + "not_unsafe": "{entity_name} wurde sicher", + "occupied": "{entity_name} wurde besch\u00e4ftigt / besetzt", + "opened": "{entity_name} ge\u00f6ffnet", + "plugged_in": "{entity_name} eingesteckt", + "powered": "{entity_name} wird mit Strom versorgt", + "present": "{entity_name} anwesend", + "problem": "{entity_name} hat ein Problem festgestellt", + "smoke": "{entity_name} detektiert Rauch", + "sound": "{entity_name} detektiert Ger\u00e4usche", + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet", + "unsafe": "{entity_name} ist unsicher", + "vibration": "{entity_name} detektiert Vibrationen" } } } \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/nl.json b/homeassistant/components/binary_sensor/.translations/nl.json new file mode 100644 index 00000000000..92cadf79aa8 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/nl.json @@ -0,0 +1,58 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "{entity_name} batterij is bijna leeg", + "is_cold": "{entity_name} is koud", + "is_connected": "{entity_name} is verbonden", + "is_gas": "{entity_name} detecteert gas", + "is_hot": "{entity_name} is hot", + "is_no_gas": "{entity_name} detecteert geen gas", + "is_no_light": "{entity_name} detecteert geen licht", + "is_no_motion": "{entity_name} detecteert geen beweging", + "is_no_problem": "{entity_name} detecteert geen probleem", + "is_no_smoke": "{entity_name} detecteert geen rook", + "is_no_sound": "{entity_name} detecteert geen geluid", + "is_no_vibration": "{entity_name} detecteert geen trillingen", + "is_not_bat_low": "{entity_name} batterij is normaal", + "is_not_cold": "{entity_name} is niet koud", + "is_not_connected": "{entity_name} is niet verbonden", + "is_not_hot": "{entity_name} is niet heet", + "is_not_occupied": "{entity_name} is niet bezet", + "is_not_present": "{entity_name} is niet aanwezig", + "is_not_unsafe": "{entity_name} is veilig", + "is_occupied": "{entity_name} bezet is", + "is_present": "{entity_name} is aanwezig", + "is_problem": "{entity_name} detecteert een probleem", + "is_smoke": "{entity_name} detecteert rook", + "is_sound": "{entity_name} detecteert geluid", + "is_unsafe": "{entity_name} is onveilig", + "is_vibration": "{entity_name} detecteert trillingen" + }, + "trigger_type": { + "bat_low": "{entity_name} batterij bijna leeg", + "closed": "{entity_name} gesloten", + "connected": "{entity_name} verbonden", + "light": "{entity_name} begon licht te detecteren", + "locked": "{entity_name} vergrendeld", + "moist": "{entity_name} werd vochtig", + "motion": "{entity_name} begon beweging te detecteren", + "not_bat_low": "{entity_name} batterij normaal", + "not_connected": "{entity_name} verbroken", + "not_locked": "{entity_name} ontgrendeld", + "not_occupied": "{entity_name} werd niet bezet", + "not_opened": "{entity_name} gesloten", + "not_plugged_in": "{entity_name} niet verbonden", + "not_powered": "{entity_name} niet ingeschakeld", + "occupied": "{entity_name} werd bezet", + "opened": "{entity_name} geopend", + "plugged_in": "{entity_name} aangesloten", + "powered": "{entity_name} heeft vermogen", + "problem": "{entity_name} begonnen met het detecteren van een probleem", + "smoke": "{entity_name} begon rook te detecteren", + "sound": "{entity_name} begon geluid te detecteren", + "turned_off": "{entity_name} uitgeschakeld", + "turned_on": "{entity_name} ingeschakeld", + "vibration": "{entity_name} begon trillingen te detecteren" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/de.json b/homeassistant/components/cover/.translations/de.json new file mode 100644 index 00000000000..e9ed497ccc2 --- /dev/null +++ b/homeassistant/components/cover/.translations/de.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} ist geschlossen", + "is_closing": "{entity_name} wird geschlossen", + "is_open": "{entity_name} ist offen", + "is_opening": "{entity_name} wird ge\u00f6ffnet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/nl.json b/homeassistant/components/cover/.translations/nl.json new file mode 100644 index 00000000000..93015afbfdd --- /dev/null +++ b/homeassistant/components/cover/.translations/nl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} is gesloten", + "is_closing": "{entity_name} wordt gesloten", + "is_open": "{entity_name} is open", + "is_opening": "{entity_name} wordt geopend" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index 830ae0fd13f..2bf0667cadb 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -11,6 +11,7 @@ "error": { "no_key": "Es konnte kein API-Schl\u00fcssel abgerufen werden" }, + "flow_title": "deCONZ Zigbee Gateway", "step": { "hassio_confirm": { "data": { @@ -43,12 +44,32 @@ }, "device_automation": { "trigger_subtype": { + "both_buttons": "Beide Tasten", + "button_1": "Erste Taste", + "button_2": "Zweite Taste", + "button_3": "Dritte Taste", + "button_4": "Vierte Taste", "close": "Schlie\u00dfen", + "dim_down": "Dimmer runter", + "dim_up": "Dimmer hoch", "left": "Links", "open": "Offen", "right": "Rechts", "turn_off": "Ausschalten", "turn_on": "Einschalten" + }, + "trigger_type": { + "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt", + "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", + "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", + "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt", + "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt", + "remote_button_rotated": "Button gedreht \"{subtype}\".", + "remote_button_rotation_stopped": "Die Tastendrehung \"{subtype}\" wurde gestoppt", + "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" Taste losgelassen", + "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt", + "remote_gyro_activated": "Ger\u00e4t ersch\u00fcttert" } }, "options": { diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index f5f41a28a32..49394eb9773 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -11,6 +11,7 @@ "error": { "no_key": "Konnt keen API Schl\u00ebssel kr\u00e9ien" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 116f6254b37..7f690f11f1d 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -11,6 +11,7 @@ "error": { "no_key": "Kon geen API-sleutel ophalen" }, + "flow_title": "deCONZ Zigbee gateway ( {host} )", "step": { "hassio_confirm": { "data": { @@ -64,6 +65,7 @@ "remote_button_quadruple_press": "\" {subtype} \" knop viervoudig aangeklikt", "remote_button_quintuple_press": "\" {subtype} \" knop vijf keer aangeklikt", "remote_button_rotated": "Knop gedraaid \" {subtype} \"", + "remote_button_rotation_stopped": "Knoprotatie \" {subtype} \" gestopt", "remote_button_short_press": "\" {subtype} \" knop ingedrukt", "remote_button_short_release": "\"{subtype}\" knop losgelaten", "remote_button_triple_press": "\" {subtype} \" knop driemaal geklikt", diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index f342f3145b9..d3a8781bb4e 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -11,6 +11,7 @@ "error": { "no_key": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447 API." }, + "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 2ad613cde68..975600a5745 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -11,6 +11,7 @@ "error": { "no_key": "\u7121\u6cd5\u53d6\u5f97 API key" }, + "flow_title": "deCONZ Zigbee \u9598\u9053\u5668\uff08{host}\uff09", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/ecobee/.translations/de.json b/homeassistant/components/ecobee/.translations/de.json index 1959f769d3a..33d493f6db0 100644 --- a/homeassistant/components/ecobee/.translations/de.json +++ b/homeassistant/components/ecobee/.translations/de.json @@ -1,11 +1,25 @@ { "config": { + "abort": { + "one_instance_only": "Diese Integration unterst\u00fctzt derzeit nur eine Ecobee-Instanz." + }, + "error": { + "pin_request_failed": "Fehler beim Anfordern der PIN von ecobee; Bitte \u00fcberpr\u00fcfe, ob der API-Schl\u00fcssel korrekt ist.", + "token_request_failed": "Fehler beim Anfordern eines Token von ecobee; Bitte versuche es erneut." + }, "step": { + "authorize": { + "description": "Bitte autorisiere diese App unter https://www.ecobee.com/consumerportal/index.html mit Pincode:\n\n{pin}\n\nDr\u00fccke dann auf Senden.", + "title": "App auf ecobee.com autorisieren" + }, "user": { "data": { "api_key": "API Key" - } + }, + "description": "Bitte geben Sie den von ecobee.com erhaltenen API-Schl\u00fcssel ein.", + "title": "ecobee API-Schl\u00fcssel" } - } + }, + "title": "ecobee" } } \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/nl.json b/homeassistant/components/ecobee/.translations/nl.json new file mode 100644 index 00000000000..b2e3ce9cdd7 --- /dev/null +++ b/homeassistant/components/ecobee/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "one_instance_only": "Deze integratie ondersteunt momenteel slechts \u00e9\u00e9n ecobee-instantie." + }, + "error": { + "token_request_failed": "Fout bij het aanvragen van tokens bij ecobee; probeer het opnieuw." + }, + "step": { + "authorize": { + "description": "Autoriseer deze app op https://www.ecobee.com/consumerportal/index.html met pincode: \n\n {pin} \n \nDruk vervolgens op Submit." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/de.json b/homeassistant/components/iaqualink/.translations/de.json new file mode 100644 index 00000000000..d929022c905 --- /dev/null +++ b/homeassistant/components/iaqualink/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_setup": "Es kann nur eine einzige iAqualink-Verbindung konfiguriert werden." + }, + "error": { + "connection_failure": "Die Verbindung zu iAqualink ist nicht m\u00f6glich. Bitte \u00fcberpr\u00fcfe den Benutzernamen und das Passwort." + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername/E-Mail-Adresse" + }, + "description": "Bitte geben Sie den Benutzernamen und das Passwort f\u00fcr Ihr iAqualink-Konto ein.", + "title": "Mit iAqualink verbinden" + } + }, + "title": "Jandy iAqualink" + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/de.json b/homeassistant/components/izone/.translations/de.json new file mode 100644 index 00000000000..3c7ebfa937f --- /dev/null +++ b/homeassistant/components/izone/.translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Es wurden keine iZone-Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Es ist nur eine einzige Konfiguration von iZone erforderlich." + }, + "step": { + "confirm": { + "description": "M\u00f6chten Sie iZone einrichten?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/de.json b/homeassistant/components/lock/.translations/de.json new file mode 100644 index 00000000000..02c387ff487 --- /dev/null +++ b/homeassistant/components/lock/.translations/de.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} ist gesperrt", + "is_unlocked": "{entity_name} ist entsperrt" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/nl.json b/homeassistant/components/lock/.translations/nl.json new file mode 100644 index 00000000000..6a39f9cbf58 --- /dev/null +++ b/homeassistant/components/lock/.translations/nl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} is vergrendeld", + "is_unlocked": "{entity_name} is ontgrendeld" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/.translations/nl.json b/homeassistant/components/met/.translations/nl.json index 87f13084f7e..c8b120b855a 100644 --- a/homeassistant/components/met/.translations/nl.json +++ b/homeassistant/components/met/.translations/nl.json @@ -1,7 +1,7 @@ { "config": { "error": { - "name_exists": "Naam bestaat al" + "name_exists": "Locatie bestaat al." }, "step": { "user": { diff --git a/homeassistant/components/neato/.translations/nl.json b/homeassistant/components/neato/.translations/nl.json new file mode 100644 index 00000000000..a90009cb7be --- /dev/null +++ b/homeassistant/components/neato/.translations/nl.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "invalid_credentials": "Ongeldige gebruikersgegevens" + }, + "create_entry": { + "default": "Zie [Neato-documentatie] ({docs_url})." + }, + "error": { + "invalid_credentials": "Ongeldige inloggegevens", + "unexpected_error": "Onverwachte fout" + }, + "step": { + "user": { + "title": "Neato-account info" + } + }, + "title": "Neato" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json index 274dd46488b..0957e233116 100644 --- a/homeassistant/components/opentherm_gw/.translations/de.json +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -10,10 +10,14 @@ "init": { "data": { "device": "Pfad oder URL", + "floor_temperature": "Boden-Temperatur", "id": "ID", - "name": "Name" - } + "name": "Name", + "precision": "Genauigkeit der Temperatur" + }, + "title": "OpenTherm Gateway" } - } + }, + "title": "OpenTherm Gateway" } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json index 4fec1baba7b..81f4aa028f1 100644 --- a/homeassistant/components/opentherm_gw/.translations/nl.json +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -4,7 +4,8 @@ "init": { "data": { "device": "Pad of URL", - "id": "ID" + "id": "ID", + "precision": "Klimaattemperatuur precisie" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json index 56715e60a8c..4b24e6c78a6 100644 --- a/homeassistant/components/plex/.translations/de.json +++ b/homeassistant/components/plex/.translations/de.json @@ -1,32 +1,60 @@ { "config": { "abort": { - "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden" + "all_configured": "Alle verkn\u00fcpften Server sind bereits konfiguriert", + "already_configured": "Dieser Plex-Server ist bereits konfiguriert", + "already_in_progress": "Plex wird konfiguriert", + "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden", + "invalid_import": "Die importierte Konfiguration ist ung\u00fcltig", + "token_request_timeout": "Zeit\u00fcberschreitung beim Erhalt des Tokens", + "unknown": "Aus unbekanntem Grund fehlgeschlagen" + }, + "error": { + "faulty_credentials": "Autorisation fehlgeschlagen", + "no_servers": "Keine Server sind mit dem Konto verbunden", + "no_token": "Bereitstellen eines Tokens oder Ausw\u00e4hlen der manuellen Einrichtung", + "not_found": "Plex-Server nicht gefunden" }, "step": { "manual_setup": { "data": { "host": "Host", "port": "Port", - "ssl": "SSL verwenden" + "ssl": "SSL verwenden", + "token": "Token (falls erforderlich)", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "title": "Plex Server" }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Mehrere Server verf\u00fcgbar, w\u00e4hlen Sie einen aus:", + "title": "Plex-Server ausw\u00e4hlen" + }, "start_website_auth": { "description": "Weiter zur Autorisierung unter plex.tv.", "title": "Plex Server verbinden" }, "user": { "data": { - "manual_setup": "Manuelle Einrichtung" + "manual_setup": "Manuelle Einrichtung", + "token": "Plex Token" }, - "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell." + "description": "Fahren Sie mit der Autorisierung unter plex.tv fort oder konfigurieren Sie einen Server manuell.", + "title": "Plex Server verbinden" } - } + }, + "title": "Plex" }, "options": { "step": { "plex_mp_settings": { + "data": { + "show_all_controls": "Alle Steuerelemente anzeigen", + "use_episode_art": "Episode-Bilder verwenden" + }, "description": "Optionen f\u00fcr Plex-Media-Player" } } diff --git a/homeassistant/components/plex/.translations/nl.json b/homeassistant/components/plex/.translations/nl.json new file mode 100644 index 00000000000..413f4ad3207 --- /dev/null +++ b/homeassistant/components/plex/.translations/nl.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "invalid_import": "Ge\u00efmporteerde configuratie is ongeldig", + "token_request_timeout": "Time-out verkrijgen van token", + "unknown": "Mislukt om onbekende reden" + }, + "error": { + "faulty_credentials": "Autorisatie mislukt", + "no_servers": "Geen servers gekoppeld aan account", + "no_token": "Geef een token op of selecteer handmatige installatie", + "not_found": "Plex-server niet gevonden" + }, + "step": { + "manual_setup": { + "data": { + "host": "Host", + "port": "Poort", + "ssl": "Gebruik SSL" + }, + "title": "Plex server" + }, + "start_website_auth": { + "description": "Ga verder met autoriseren bij plex.tv.", + "title": "Verbind de Plex server" + }, + "user": { + "data": { + "manual_setup": "Handmatig setup" + }, + "description": "Ga verder met autoriseren bij plex.tv of configureer een server." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/de.json b/homeassistant/components/sensor/.translations/de.json index 1f248099df3..bf28653c0ce 100644 --- a/homeassistant/components/sensor/.translations/de.json +++ b/homeassistant/components/sensor/.translations/de.json @@ -1,7 +1,10 @@ { "device_automation": { "condition_type": { + "is_battery_level": "{entity_name} Batteriestand", "is_humidity": "{entity_name} Feuchtigkeit", + "is_illuminance": "{entity_name} Beleuchtungsst\u00e4rke", + "is_power": "{entity_name} Leistung", "is_pressure": "{entity_name} Druck", "is_signal_strength": "{entity_name} Signalst\u00e4rke", "is_temperature": "{entity_name} Temperatur", @@ -11,6 +14,8 @@ "trigger_type": { "battery_level": "{entity_name} Batteriestatus", "humidity": "{entity_name} Feuchtigkeit", + "illuminance": "{entity_name} Beleuchtungsst\u00e4rke", + "power": "{entity_name} Leistung", "pressure": "{entity_name} Druck", "signal_strength": "{entity_name} Signalst\u00e4rke", "temperature": "{entity_name} Temperatur", diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json new file mode 100644 index 00000000000..aca2306d90e --- /dev/null +++ b/homeassistant/components/sensor/.translations/nl.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "condition_type": { + "is_power": "{entity_name}\nvermogen" + }, + "trigger_type": { + "battery_level": "{entity_name} batterijniveau", + "humidity": "{entity_name} vochtigheidsgraad", + "illuminance": "{entity_name} verlichtingssterkte", + "power": "{entity_name} vermogen", + "pressure": "{entity_name} druk", + "signal_strength": "{entity_name} signaalsterkte", + "temperature": "{entity_name} temperatuur", + "timestamp": "{entity_name} tijdstip" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/de.json b/homeassistant/components/solaredge/.translations/de.json new file mode 100644 index 00000000000..cbe913e131c --- /dev/null +++ b/homeassistant/components/solaredge/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "site_exists": "Diese site_id ist bereits konfiguriert" + }, + "error": { + "site_exists": "Diese site_id ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "api_key": "Der API-Schl\u00fcssel f\u00fcr diese Site", + "name": "Der Name dieser Installation", + "site_id": "Die SolarEdge-Site-ID" + }, + "title": "Definiere die API-Parameter f\u00fcr diese Installation" + } + }, + "title": "SolarEdge" + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json index d93eec8aed7..838d46a6d42 100644 --- a/homeassistant/components/soma/.translations/de.json +++ b/homeassistant/components/soma/.translations/de.json @@ -4,6 +4,10 @@ "already_setup": "Du kannst nur ein einziges Soma-Konto konfigurieren.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "missing_configuration": "Die Soma-Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." - } + }, + "create_entry": { + "default": "Erfolgreich bei Soma authentifiziert." + }, + "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/en.json b/homeassistant/components/soma/.translations/en.json index aa2f92f0be6..42e09a8762c 100644 --- a/homeassistant/components/soma/.translations/en.json +++ b/homeassistant/components/soma/.translations/en.json @@ -20,4 +20,4 @@ }, "title": "Soma" } -} +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/lb.json b/homeassistant/components/soma/.translations/lb.json index d8aba082537..93e9a1e66c4 100644 --- a/homeassistant/components/soma/.translations/lb.json +++ b/homeassistant/components/soma/.translations/lb.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Erfollegr\u00e4ich mat Soma authentifiz\u00e9iert." }, + "step": { + "user": { + "data": { + "host": "Apparat", + "port": "Port" + }, + "description": "Gitt Verbindungs Informatioune vun \u00e4rem SOMA Connect an.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/nl.json b/homeassistant/components/soma/.translations/nl.json new file mode 100644 index 00000000000..0bf2836c5a1 --- /dev/null +++ b/homeassistant/components/soma/.translations/nl.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_setup": "U kunt slechts \u00e9\u00e9n Soma-account configureren." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json index c3d9d7e70d4..b2d80208b83 100644 --- a/homeassistant/components/soma/.translations/no.json +++ b/homeassistant/components/soma/.translations/no.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Vellykket autentisering med Somfy." }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "description": "Vennligst skriv tilkoblingsinnstillingene for din SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ru.json b/homeassistant/components/soma/.translations/ru.json index 5ab3af0ecf8..f7e6574b113 100644 --- a/homeassistant/components/soma/.translations/ru.json +++ b/homeassistant/components/soma/.translations/ru.json @@ -8,6 +8,16 @@ "create_entry": { "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a SOMA Connect.", + "title": "Soma" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/de.json b/homeassistant/components/switch/.translations/de.json new file mode 100644 index 00000000000..5396facadd7 --- /dev/null +++ b/homeassistant/components/switch/.translations/de.json @@ -0,0 +1,19 @@ +{ + "device_automation": { + "action_type": { + "toggle": "{entity_name} umschalten", + "turn_off": "Schalte {entity_name} aus.", + "turn_on": "Schalte {entity_name} ein." + }, + "condition_type": { + "is_off": "{entity_name} ist ausgeschaltet", + "is_on": "{entity_name} ist eingeschaltet", + "turn_off": "{entity_name} ausgeschaltet", + "turn_on": "{entity_name} eingeschaltet" + }, + "trigger_type": { + "turned_off": "{entity_name} ausgeschaltet", + "turned_on": "{entity_name} eingeschaltet" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json index ed0342b9430..1a2fa4a48c0 100644 --- a/homeassistant/components/transmission/.translations/de.json +++ b/homeassistant/components/transmission/.translations/de.json @@ -21,16 +21,19 @@ "password": "Passwort", "port": "Port", "username": "Benutzername" - } + }, + "title": "Transmission-Client einrichten" } - } + }, + "title": "Transmission" }, "options": { "step": { "init": { "data": { "scan_interval": "Aktualisierungsfrequenz" - } + }, + "description": "Konfigurieren von Optionen f\u00fcr Transmission" } } } diff --git a/homeassistant/components/transmission/.translations/nl.json b/homeassistant/components/transmission/.translations/nl.json new file mode 100644 index 00000000000..6d9d130f85c --- /dev/null +++ b/homeassistant/components/transmission/.translations/nl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Kan geen verbinding maken met host", + "wrong_credentials": "verkeerde gebruikersnaam of wachtwoord" + }, + "step": { + "user": { + "data": { + "username": "Gebruikersnaam" + }, + "title": "Verzendclient instellen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/nl.json b/homeassistant/components/unifi/.translations/nl.json index 518f0066534..36e21728f1d 100644 --- a/homeassistant/components/unifi/.translations/nl.json +++ b/homeassistant/components/unifi/.translations/nl.json @@ -38,6 +38,11 @@ "one": "Leeg", "other": "Leeg" } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Maak bandbreedtegebruiksensoren voor netwerkclients" + } } } } diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json index 15b6f4e3b01..dabf184d7ed 100644 --- a/homeassistant/components/withings/.translations/de.json +++ b/homeassistant/components/withings/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "no_flows": "Withings muss konfiguriert werden, bevor die Integration authentifiziert werden kann. Bitte lies die Dokumentation." + }, "create_entry": { "default": "Erfolgreiche Authentifizierung mit Withings f\u00fcr das ausgew\u00e4hlte Profil." }, diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 969c78e7b13..3329eafa1c6 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -18,13 +18,50 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Kreischen", + "warn": "Warnen" + }, "trigger_subtype": { + "both_buttons": "Beide Tasten", + "button_1": "Erste Taste", + "button_2": "Zweite Taste", + "button_3": "Dritte Taste", + "button_4": "Vierte Taste", + "button_5": "F\u00fcnfte Taste", + "button_6": "Sechste Taste", "close": "Schlie\u00dfen", + "dim_down": "Dimmer runter", + "dim_up": "Dimmer hoch", + "face_1": "mit Fl\u00e4che 1 aktiviert", + "face_2": "mit Fl\u00e4che 2 aktiviert", + "face_3": "mit Fl\u00e4che 3 aktiviert", + "face_4": "mit Fl\u00e4che 4 aktiviert", + "face_5": "mit Fl\u00e4che 5 aktiviert", + "face_6": "mit Fl\u00e4che 6 aktiviert", + "face_any": "Mit einer beliebigen/festgelegten Fl\u00e4che(n) aktiviert", "left": "Links", "open": "Offen", "right": "Rechts", "turn_off": "Ausschalten", "turn_on": "Einschalten" + }, + "trigger_type": { + "device_dropped": "Ger\u00e4t ist gefallen", + "device_flipped": "Ger\u00e4t umgedreht \"{subtype}\"", + "device_knocked": "Ger\u00e4t klopfte \"{subtype}\"", + "device_rotated": "Ger\u00e4t wurde gedreht \"{subtype}\"", + "device_shaken": "Ger\u00e4t ersch\u00fcttert", + "device_slid": "Ger\u00e4t gerutscht \"{subtype}\"", + "device_tilted": "Ger\u00e4t gekippt", + "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt", + "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", + "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", + "remote_button_quadruple_press": "\"{subtype}\" Taste vierfach geklickt", + "remote_button_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt", + "remote_button_short_press": "\"{subtype}\" Taste gedr\u00fcckt", + "remote_button_short_release": "\"{subtype}\" Taste losgelassen", + "remote_button_triple_press": "\"{subtype}\" Taste dreimal geklickt" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/nl.json b/homeassistant/components/zha/.translations/nl.json index 5e5c666b1a4..bfb47c9d7fc 100644 --- a/homeassistant/components/zha/.translations/nl.json +++ b/homeassistant/components/zha/.translations/nl.json @@ -18,7 +18,15 @@ "title": "ZHA" }, "device_automation": { + "action_type": { + "squawk": "Schreeuw", + "warn": "Waarschuwen" + }, "trigger_subtype": { + "close": "Sluiten", + "dim_down": "Dim omlaag", + "dim_up": "Dim omhoog", + "face_any": "Met elk/opgegeven gezicht (en) geactiveerd", "left": "Links", "open": "Open", "right": "Rechts", From c8f64840957a72aa826fdae6616d631736eea4c3 Mon Sep 17 00:00:00 2001 From: Bogdan Vlaicu Date: Wed, 16 Oct 2019 03:52:30 -0400 Subject: [PATCH 322/639] New sensor platform integration for Orange and Rockland Utility smart energy meter (#27571) * New sensor platform integration for Orange and Rockland Utility smart energy meter * New sensor platform integration for Orange and Rockland Utility smart energy meter * bumped the oru py version to 0.1.9 * Added PLATFORM_SCHEMA Adde unique_id property Changed logger level from info to debug when printing the updated sensor value Set the SCAN_INTERVAL to 15 mins Added exception handling durin init when creating the oru meter instance * Various fixes base on the PR review + Added SCAN_INTERVAL for 15 mins * fixed path to documentation --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/oru/__init__.py | 1 + homeassistant/components/oru/manifest.json | 8 ++ homeassistant/components/oru/sensor.py | 92 ++++++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 106 insertions(+) create mode 100644 homeassistant/components/oru/__init__.py create mode 100644 homeassistant/components/oru/manifest.json create mode 100644 homeassistant/components/oru/sensor.py diff --git a/.coveragerc b/.coveragerc index 69ce7f8322c..52cf74f384a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -485,6 +485,7 @@ omit = homeassistant/components/openweathermap/weather.py homeassistant/components/opple/light.py homeassistant/components/orangepi_gpio/* + homeassistant/components/oru/* homeassistant/components/orvibo/switch.py homeassistant/components/osramlightify/light.py homeassistant/components/otp/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 8e52210cec7..547fe504892 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -215,6 +215,7 @@ homeassistant/components/opentherm_gw/* @mvn23 homeassistant/components/openuv/* @bachya homeassistant/components/openweathermap/* @fabaff homeassistant/components/orangepi_gpio/* @pascallj +homeassistant/components/oru/* @bvlaicu homeassistant/components/owlet/* @oblogic7 homeassistant/components/panel_custom/* @home-assistant/frontend homeassistant/components/panel_iframe/* @home-assistant/frontend diff --git a/homeassistant/components/oru/__init__.py b/homeassistant/components/oru/__init__.py new file mode 100644 index 00000000000..d1517ab0bf1 --- /dev/null +++ b/homeassistant/components/oru/__init__.py @@ -0,0 +1 @@ +"""The Orange and Rockland Utility smart energy meter integration.""" diff --git a/homeassistant/components/oru/manifest.json b/homeassistant/components/oru/manifest.json new file mode 100644 index 00000000000..ff5e74fd260 --- /dev/null +++ b/homeassistant/components/oru/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "oru", + "name": "Orange and Rockland Utility Smart Energy Meter Sensor", + "documentation": "https://www.home-assistant.io/integrations/oru", + "dependencies": [], + "codeowners": ["@bvlaicu"], + "requirements": ["oru==0.1.9"] +} \ No newline at end of file diff --git a/homeassistant/components/oru/sensor.py b/homeassistant/components/oru/sensor.py new file mode 100644 index 00000000000..e68d8e1c45a --- /dev/null +++ b/homeassistant/components/oru/sensor.py @@ -0,0 +1,92 @@ +"""Platform for sensor integration.""" +from datetime import timedelta +import logging + +import voluptuous as vol + +from oru import Meter +from oru import MeterError + +from homeassistant.components.sensor import PLATFORM_SCHEMA +import homeassistant.helpers.config_validation as cv +from homeassistant.const import ENERGY_KILO_WATT_HOUR +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +CONF_METER_NUMBER = "meter_number" + +SCAN_INTERVAL = timedelta(minutes=15) + +SENSOR_NAME = "ORU Current Energy Usage" +SENSOR_ICON = "mdi:counter" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_METER_NUMBER): cv.string}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the sensor platform.""" + + meter_number = config[CONF_METER_NUMBER] + + try: + meter = Meter(meter_number) + + except MeterError: + _LOGGER.error("Unable to create Oru meter") + return + + add_entities([CurrentEnergyUsageSensor(meter)], True) + + _LOGGER.debug("Oru meter_number = %s", meter_number) + + +class CurrentEnergyUsageSensor(Entity): + """Representation of the sensor.""" + + def __init__(self, meter): + """Initialize the sensor.""" + self._state = None + self._available = None + self.meter = meter + + @property + def unique_id(self): + """Return a unique, HASS-friendly identifier for this entity.""" + return self.meter.meter_id + + @property + def name(self): + """Return the name of the sensor.""" + return SENSOR_NAME + + @property + def icon(self): + """Return the icon of the sensor.""" + return SENSOR_ICON + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return ENERGY_KILO_WATT_HOUR + + def update(self): + """Fetch new state data for the sensor.""" + try: + last_read = self.meter.last_read() + + self._state = last_read + self._available = True + + _LOGGER.debug( + "%s = %s %s", self.name, self._state, self.unit_of_measurement + ) + except MeterError as err: + self._available = False + + _LOGGER.error("Unexpected oru meter error: %s", err) diff --git a/requirements_all.txt b/requirements_all.txt index 567acd712a1..51b28bdf352 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -916,6 +916,9 @@ openwebifpy==3.1.1 # homeassistant.components.luci openwrt-luci-rpc==1.1.1 +# homeassistant.components.oru +oru==0.1.9 + # homeassistant.components.orvibo orvibo==1.1.1 From 5a35e52adf5630e9068a046d661c7808c1e3af1a Mon Sep 17 00:00:00 2001 From: bouni Date: Wed, 16 Oct 2019 10:25:37 +0200 Subject: [PATCH 323/639] Move imports in device_tracker component (#27676) * moved imports to top level * sorted imports using isort --- .../components/device_tracker/legacy.py | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 5c186cc12a1..ad7ff3fe3f5 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -1,11 +1,12 @@ """Legacy device tracker classes.""" import asyncio from datetime import timedelta +import hashlib from typing import Any, List, Sequence import voluptuous as vol -from homeassistant.core import callback +from homeassistant import util from homeassistant.components import zone from homeassistant.components.group import ( ATTR_ADD_ENTITIES, @@ -16,16 +17,7 @@ from homeassistant.components.group import ( SERVICE_SET, ) from homeassistant.components.zone import async_active_zone -from homeassistant.config import load_yaml_config_file, async_log_exception -from homeassistant.exceptions import HomeAssistantError -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_registry import async_get_registry -from homeassistant.helpers.restore_state import RestoreEntity -from homeassistant.helpers.typing import GPSType, HomeAssistantType -from homeassistant import util -import homeassistant.util.dt as dt_util -from homeassistant.util.yaml import dump - +from homeassistant.config import async_log_exception, load_yaml_config_file from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_GPS_ACCURACY, @@ -37,9 +29,17 @@ from homeassistant.const import ( CONF_MAC, CONF_NAME, DEVICE_DEFAULT_NAME, - STATE_NOT_HOME, STATE_HOME, + STATE_NOT_HOME, ) +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_registry import async_get_registry +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import GPSType, HomeAssistantType +import homeassistant.util.dt as dt_util +from homeassistant.util.yaml import dump from .const import ( ATTR_BATTERY, @@ -635,7 +635,6 @@ def get_gravatar_for_email(email: str): Async friendly. """ - import hashlib url = "https://www.gravatar.com/avatar/{}.jpg?s=80&d=wavatar" return url.format(hashlib.md5(email.encode("utf-8").lower()).hexdigest()) From 44b6258e48aa3cdb1bd90280d1271e695bde8f82 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Wed, 16 Oct 2019 10:32:25 +0100 Subject: [PATCH 324/639] Add evohome high_precision temperatures (#27513) * add high_precision (current) temperatures * bump client to use aiohttp for v1 client * token saving now event-driven rather than scheduled * protection against invalid tokens that cause issues * tweak error message --- homeassistant/components/evohome/__init__.py | 285 ++++++++++-------- homeassistant/components/evohome/climate.py | 8 +- .../components/evohome/manifest.json | 2 +- .../components/evohome/water_heater.py | 4 +- requirements_all.txt | 2 +- 5 files changed, 176 insertions(+), 125 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index e9254c373d9..a52780c8a0f 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -10,9 +10,9 @@ from typing import Any, Dict, Optional, Tuple import aiohttp.client_exceptions import voluptuous as vol import evohomeasync2 +import evohomeasync from homeassistant.const import ( - CONF_ACCESS_TOKEN, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME, @@ -32,10 +32,13 @@ from .const import DOMAIN, EVO_FOLLOW, STORAGE_VERSION, STORAGE_KEY, GWS, TCS _LOGGER = logging.getLogger(__name__) -CONF_ACCESS_TOKEN_EXPIRES = "access_token_expires" -CONF_REFRESH_TOKEN = "refresh_token" +ACCESS_TOKEN = "access_token" +ACCESS_TOKEN_EXPIRES = "access_token_expires" +REFRESH_TOKEN = "refresh_token" +USER_DATA = "user_data" CONF_LOCATION_IDX = "location_idx" + SCAN_INTERVAL_DEFAULT = timedelta(seconds=300) SCAN_INTERVAL_MINIMUM = timedelta(seconds=60) @@ -96,14 +99,15 @@ def convert_dict(dictionary: Dict[str, Any]) -> Dict[str, Any]: def _handle_exception(err) -> bool: + """Return False if the exception can't be ignored.""" try: raise err except evohomeasync2.AuthenticationError: _LOGGER.error( - "Failed to (re)authenticate with the vendor's server. " + "Failed to authenticate with the vendor's server. " "Check your network and the vendor's service status page. " - "Check that your username and password are correct. " + "Also check that your username and password are correct. " "Message is: %s", err, ) @@ -135,14 +139,77 @@ def _handle_exception(err) -> bool: ) return False - raise # we don't expect/handle any other ClientResponseError + raise # we don't expect/handle any other Exceptions async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Create a (EMEA/EU-based) Honeywell evohome system.""" - broker = EvoBroker(hass, config[DOMAIN]) - if not await broker.init_client(): + + async def load_auth_tokens(store) -> Tuple[Dict, Optional[Dict]]: + app_storage = await store.async_load() + tokens = dict(app_storage if app_storage else {}) + + if tokens.pop(CONF_USERNAME, None) != config[DOMAIN][CONF_USERNAME]: + # any tokens wont be valid, and store might be be corrupt + await store.async_save({}) + return ({}, None) + + # evohomeasync2 requires naive/local datetimes as strings + if tokens.get(ACCESS_TOKEN_EXPIRES) is not None: + tokens[ACCESS_TOKEN_EXPIRES] = _dt_to_local_naive( + dt_util.parse_datetime(tokens[ACCESS_TOKEN_EXPIRES]) + ) + + user_data = tokens.pop(USER_DATA, None) + return (tokens, user_data) + + store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) + tokens, user_data = await load_auth_tokens(store) + + client_v2 = evohomeasync2.EvohomeClient( + config[DOMAIN][CONF_USERNAME], + config[DOMAIN][CONF_PASSWORD], + **tokens, + session=async_get_clientsession(hass), + ) + + try: + await client_v2.login() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: + _handle_exception(err) return False + finally: + config[DOMAIN][CONF_PASSWORD] = "REDACTED" + + loc_idx = config[DOMAIN][CONF_LOCATION_IDX] + try: + loc_config = client_v2.installation_info[loc_idx][GWS][0][TCS][0] + except IndexError: + _LOGGER.error( + "Config error: '%s' = %s, but the valid range is 0-%s. " + "Unable to continue. Fix any configuration errors and restart HA.", + CONF_LOCATION_IDX, + loc_idx, + len(client_v2.installation_info) - 1, + ) + return False + + _LOGGER.debug("Config = %s", loc_config) + + client_v1 = evohomeasync.EvohomeClient( + client_v2.username, + client_v2.password, + user_data=user_data, + session=async_get_clientsession(hass), + ) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN]["broker"] = broker = EvoBroker( + hass, client_v2, client_v1, store, config[DOMAIN] + ) + + await broker.save_auth_tokens() + await broker.update() # get initial state hass.async_create_task(async_load_platform(hass, "climate", DOMAIN, {}, config)) if broker.tcs.hotwater: @@ -160,116 +227,100 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: class EvoBroker: """Container for evohome client and data.""" - def __init__(self, hass, params) -> None: + def __init__(self, hass, client, client_v1, store, params) -> None: """Initialize the evohome client and its data structure.""" self.hass = hass + self.client = client + self.client_v1 = client_v1 + self._store = store self.params = params - self.config = {} - - self.client = self.tcs = None - self._app_storage = {} - - hass.data[DOMAIN] = {} - hass.data[DOMAIN]["broker"] = self - - async def init_client(self) -> bool: - """Initialse the evohome data broker. - - Return True if this is successful, otherwise return False. - """ - refresh_token, access_token, access_token_expires = ( - await self._load_auth_tokens() - ) - - # evohomeasync2 uses naive/local datetimes - if access_token_expires is not None: - access_token_expires = _dt_to_local_naive(access_token_expires) - - client = self.client = evohomeasync2.EvohomeClient( - self.params[CONF_USERNAME], - self.params[CONF_PASSWORD], - refresh_token=refresh_token, - access_token=access_token, - access_token_expires=access_token_expires, - session=async_get_clientsession(self.hass), - ) - - try: - await client.login() - except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - if not _handle_exception(err): - return False - - finally: - self.params[CONF_PASSWORD] = "REDACTED" - - self.hass.add_job(self._save_auth_tokens()) - - loc_idx = self.params[CONF_LOCATION_IDX] - try: - self.config = client.installation_info[loc_idx][GWS][0][TCS][0] - - except IndexError: - _LOGGER.error( - "Config error: '%s' = %s, but its valid range is 0-%s. " - "Unable to continue. " - "Fix any configuration errors and restart HA.", - CONF_LOCATION_IDX, - loc_idx, - len(client.installation_info) - 1, - ) - return False + loc_idx = params[CONF_LOCATION_IDX] + self.config = client.installation_info[loc_idx][GWS][0][TCS][0] self.tcs = ( client.locations[loc_idx] # pylint: disable=protected-access ._gateways[0] ._control_systems[0] ) + self.temps = None - _LOGGER.debug("Config = %s", self.config) - if _LOGGER.isEnabledFor(logging.DEBUG): # don't do an I/O unless required - await self.update() # includes: _LOGGER.debug("Status = %s"... - - return True - - async def _load_auth_tokens( - self - ) -> Tuple[Optional[str], Optional[str], Optional[datetime]]: - store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - app_storage = self._app_storage = await store.async_load() - - if app_storage is None: - app_storage = self._app_storage = {} - - if app_storage.get(CONF_USERNAME) == self.params[CONF_USERNAME]: - refresh_token = app_storage.get(CONF_REFRESH_TOKEN) - access_token = app_storage.get(CONF_ACCESS_TOKEN) - at_expires_str = app_storage.get(CONF_ACCESS_TOKEN_EXPIRES) - if at_expires_str: - at_expires_dt = dt_util.parse_datetime(at_expires_str) - else: - at_expires_dt = None - - return (refresh_token, access_token, at_expires_dt) - - return (None, None, None) # account switched: so tokens wont be valid - - async def _save_auth_tokens(self, *args) -> None: + async def save_auth_tokens(self) -> None: + """Save access tokens and session IDs to the store for later use.""" # evohomeasync2 uses naive/local datetimes access_token_expires = _local_dt_to_aware(self.client.access_token_expires) - self._app_storage[CONF_USERNAME] = self.params[CONF_USERNAME] - self._app_storage[CONF_REFRESH_TOKEN] = self.client.refresh_token - self._app_storage[CONF_ACCESS_TOKEN] = self.client.access_token - self._app_storage[CONF_ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat() + app_storage = {CONF_USERNAME: self.client.username} + app_storage[REFRESH_TOKEN] = self.client.refresh_token + app_storage[ACCESS_TOKEN] = self.client.access_token + app_storage[ACCESS_TOKEN_EXPIRES] = access_token_expires.isoformat() - store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) - await store.async_save(self._app_storage) + if self.client_v1 and self.client_v1.user_data: + app_storage[USER_DATA] = { + "userInfo": {"userID": self.client_v1.user_data["userInfo"]["userID"]}, + "sessionId": self.client_v1.user_data["sessionId"], + } + else: + app_storage[USER_DATA] = None - self.hass.helpers.event.async_track_point_in_utc_time( - self._save_auth_tokens, - access_token_expires + self.params[CONF_SCAN_INTERVAL], - ) + await self._store.async_save(app_storage) + + async def _update_v1(self, *args, **kwargs) -> None: + """Get the latest high-precision temperatures of the default Location.""" + + def get_session_id(client_v1) -> Optional[str]: + user_data = client_v1.user_data if client_v1 else None + return user_data.get("sessionId") if user_data else None + + session_id = get_session_id(self.client_v1) + + try: + temps = list(await self.client_v1.temperatures(force_refresh=True)) + + except aiohttp.ClientError as err: + _LOGGER.warning( + "Unable to obtain the latest high-precision temperatures. " + "Check your network and the vendor's service status page. " + "Proceeding with low-precision temperatures. " + "Message is: %s", + err, + ) + self.temps = None # these are now stale, will fall back to v2 temps + + else: + if ( + str(self.client_v1.location_id) + != self.client.locations[self.params[CONF_LOCATION_IDX]].locationId + ): + _LOGGER.warning( + "The v2 API's configured location doesn't match " + "the v1 API's default location (there is more than one location), " + "so the high-precision feature will be disabled" + ) + self.client_v1 = self.temps = None + else: + self.temps = {str(i["id"]): i["temp"] for i in temps} + + _LOGGER.debug("Temperatures = %s", self.temps) + + if session_id != get_session_id(self.client_v1): + await self.save_auth_tokens() + + async def _update_v2(self, *args, **kwargs) -> None: + """Get the latest modes, temperatures, setpoints of a Location.""" + access_token = self.client.access_token + + loc_idx = self.params[CONF_LOCATION_IDX] + try: + status = await self.client.locations[loc_idx].status() + except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: + _handle_exception(err) + else: + self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) + + _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) + + if access_token != self.client.access_token: + await self.save_auth_tokens() async def update(self, *args, **kwargs) -> None: """Get the latest state data of an entire evohome Location. @@ -278,17 +329,13 @@ class EvoBroker: operating mode of the Controller and the current temp of its children (e.g. Zones, DHW controller). """ - loc_idx = self.params[CONF_LOCATION_IDX] + await self._update_v2() - try: - status = await self.client.locations[loc_idx].status() - except (aiohttp.ClientError, evohomeasync2.AuthenticationError) as err: - _handle_exception(err) - else: - # inform the evohome devices that state data has been updated - self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) + if self.client_v1: + await self._update_v1() - _LOGGER.debug("Status = %s", status[GWS][0][TCS][0]) + # inform the evohome devices that state data has been updated + self.hass.helpers.dispatcher.async_dispatcher_send(DOMAIN) class EvoDevice(Entity): @@ -305,10 +352,8 @@ class EvoDevice(Entity): self._evo_tcs = evo_broker.tcs self._unique_id = self._name = self._icon = self._precision = None - - self._device_state_attrs = {} - self._state_attributes = [] self._supported_features = None + self._device_state_attrs = {} @callback def _refresh(self) -> None: @@ -394,9 +439,13 @@ class EvoChild(EvoDevice): @property def current_temperature(self) -> Optional[float]: """Return the current temperature of a Zone.""" - if self._evo_device.temperatureStatus["isAvailable"]: - return self._evo_device.temperatureStatus["temperature"] - return None + if not self._evo_device.temperatureStatus["isAvailable"]: + return None + + if self._evo_broker.temps: + return self._evo_broker.temps[self._evo_device.zoneId] + + return self._evo_device.temperatureStatus["temperature"] @property def setpoints(self) -> Dict[str, Any]: diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 7df2db1b17e..eb7f3f7d7d8 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -72,14 +72,13 @@ async def async_setup_platform( return broker = hass.data[DOMAIN]["broker"] - loc_idx = broker.params[CONF_LOCATION_IDX] _LOGGER.debug( "Found the Location/Controller (%s), id=%s, name=%s (location_idx=%s)", broker.tcs.modelType, broker.tcs.systemId, broker.tcs.location.name, - loc_idx, + broker.params[CONF_LOCATION_IDX], ) # special case of RoundModulation/RoundWireless (is a single zone system) @@ -148,9 +147,12 @@ class EvoZone(EvoChild, EvoClimateDevice): self._name = evo_device.name self._icon = "mdi:radiator" - self._precision = self._evo_device.setpointCapabilities["valueResolution"] self._supported_features = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE self._preset_modes = list(HA_PRESET_TO_EVO) + if evo_broker.client_v1: + self._precision = PRECISION_TENTHS + else: + self._precision = self._evo_device.setpointCapabilities["valueResolution"] @property def hvac_mode(self) -> str: diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index 5633880be35..da942db7920 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -3,7 +3,7 @@ "name": "Evohome", "documentation": "https://www.home-assistant.io/integrations/evohome", "requirements": [ - "evohome-async==0.3.3b4" + "evohome-async==0.3.3b5" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 37bdcd82afc..e29dbb49af2 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -7,7 +7,7 @@ from homeassistant.components.water_heater import ( SUPPORT_OPERATION_MODE, WaterHeaterDevice, ) -from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON +from homeassistant.const import PRECISION_TENTHS, PRECISION_WHOLE, STATE_OFF, STATE_ON from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime @@ -55,7 +55,7 @@ class EvoDHW(EvoChild, WaterHeaterDevice): self._name = "DHW controller" self._icon = "mdi:thermometer-lines" - self._precision = PRECISION_WHOLE + self._precision = PRECISION_TENTHS if evo_broker.client_v1 else PRECISION_WHOLE self._supported_features = SUPPORT_AWAY_MODE | SUPPORT_OPERATION_MODE @property diff --git a/requirements_all.txt b/requirements_all.txt index 51b28bdf352..2b5b4f53e52 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -477,7 +477,7 @@ eternalegypt==0.0.10 # evdev==0.6.1 # homeassistant.components.evohome -evohome-async==0.3.3b4 +evohome-async==0.3.3b5 # homeassistant.components.dlib_face_detect # homeassistant.components.dlib_face_identify From cc93dd49286759b040786b80a635d9d359ab9e78 Mon Sep 17 00:00:00 2001 From: Paolo Tuninetto Date: Wed, 16 Oct 2019 12:05:05 +0200 Subject: [PATCH 325/639] Move imports in Kodi component (#27728) * Move imports for Kodi component * Removed empty line ad requested by review --- homeassistant/components/kodi/media_player.py | 17 ++++------------- homeassistant/components/kodi/notify.py | 6 ++---- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index 9f0aab6c00c..9b2ba01e90a 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -7,6 +7,10 @@ import socket import urllib import aiohttp +import jsonrpc_base +import jsonrpc_async +import jsonrpc_websocket + import voluptuous as vol from homeassistant.components.kodi import SERVICE_CALL_METHOD @@ -231,8 +235,6 @@ def cmd(func): @wraps(func) async def wrapper(obj, *args, **kwargs): """Wrap all command methods.""" - import jsonrpc_base - try: await func(obj, *args, **kwargs) except jsonrpc_base.jsonrpc.TransportError as exc: @@ -268,9 +270,6 @@ class KodiDevice(MediaPlayerDevice): unique_id=None, ): """Initialize the Kodi device.""" - import jsonrpc_async - import jsonrpc_websocket - self.hass = hass self._name = name self._unique_id = unique_id @@ -389,8 +388,6 @@ class KodiDevice(MediaPlayerDevice): async def _get_players(self): """Return the active player objects or None.""" - import jsonrpc_base - try: return await self.server.Player.GetActivePlayers() except jsonrpc_base.jsonrpc.TransportError: @@ -420,8 +417,6 @@ class KodiDevice(MediaPlayerDevice): async def async_ws_connect(self): """Connect to Kodi via websocket protocol.""" - import jsonrpc_base - try: ws_loop_future = await self._ws_server.ws_connect() except jsonrpc_base.jsonrpc.TransportError: @@ -801,8 +796,6 @@ class KodiDevice(MediaPlayerDevice): async def async_call_method(self, method, **kwargs): """Run Kodi JSONRPC API method with params.""" - import jsonrpc_base - _LOGGER.debug("Run API method %s, kwargs=%s", method, kwargs) result_ok = False try: @@ -850,8 +843,6 @@ class KodiDevice(MediaPlayerDevice): All the albums of an artist can be added with media_name="ALL" """ - import jsonrpc_base - params = {"playlistid": 0} if media_type == "SONG": if media_id is None: diff --git a/homeassistant/components/kodi/notify.py b/homeassistant/components/kodi/notify.py index 41dfc42b5de..1072cf1b732 100644 --- a/homeassistant/components/kodi/notify.py +++ b/homeassistant/components/kodi/notify.py @@ -2,6 +2,8 @@ import logging import aiohttp +import jsonrpc_async + import voluptuous as vol from homeassistant.const import ( @@ -77,8 +79,6 @@ class KodiNotificationService(BaseNotificationService): def __init__(self, hass, url, auth=None): """Initialize the service.""" - import jsonrpc_async - self._url = url kwargs = {"timeout": DEFAULT_TIMEOUT, "session": async_get_clientsession(hass)} @@ -90,8 +90,6 @@ class KodiNotificationService(BaseNotificationService): async def async_send_message(self, message="", **kwargs): """Send a message to Kodi.""" - import jsonrpc_async - try: data = kwargs.get(ATTR_DATA) or {} From ec788211619c10a0513eee3ea8f90db4a6d43fad Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 16 Oct 2019 12:06:52 +0200 Subject: [PATCH 326/639] Add sensor platform to Airly integration (#27717) * Add sesnor.py file * Move AirlyData to __init__ * Cleaning * Update .coveragerc file * Sort consts * Sort imports * Remove icons from sensors with device_class --- .coveragerc | 1 + homeassistant/components/airly/__init__.py | 93 +++++++++++ homeassistant/components/airly/air_quality.py | 106 +++--------- homeassistant/components/airly/const.py | 15 ++ homeassistant/components/airly/sensor.py | 154 ++++++++++++++++++ 5 files changed, 283 insertions(+), 86 deletions(-) create mode 100644 homeassistant/components/airly/sensor.py diff --git a/.coveragerc b/.coveragerc index 52cf74f384a..859e1c0f92c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -29,6 +29,7 @@ omit = homeassistant/components/aftership/sensor.py homeassistant/components/airly/__init__.py homeassistant/components/airly/air_quality.py + homeassistant/components/airly/sensor.py homeassistant/components/airly/const.py homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/cover.py diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index 56b3477ac89..dc2323ddd4e 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -1,5 +1,31 @@ """The Airly component.""" +import asyncio +import logging +from datetime import timedelta + +import async_timeout +from aiohttp.client_exceptions import ClientConnectorError +from airly import Airly +from airly.exceptions import AirlyError + +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import Config, HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import Throttle + +from .const import ( + ATTR_API_ADVICE, + ATTR_API_CAQI, + ATTR_API_CAQI_DESCRIPTION, + ATTR_API_CAQI_LEVEL, + DATA_CLIENT, + DOMAIN, + NO_AIRLY_SENSORS, +) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) async def async_setup(hass: HomeAssistant, config: Config) -> bool: @@ -9,13 +35,80 @@ async def async_setup(hass: HomeAssistant, config: Config) -> bool: async def async_setup_entry(hass, config_entry): """Set up Airly as config entry.""" + api_key = config_entry.data[CONF_API_KEY] + latitude = config_entry.data[CONF_LATITUDE] + longitude = config_entry.data[CONF_LONGITUDE] + + websession = async_get_clientsession(hass) + + airly = AirlyData(websession, api_key, latitude, longitude) + + await airly.async_update() + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly + hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "sensor") + ) return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" + hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality") + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") return True + + +class AirlyData: + """Define an object to hold Airly data.""" + + def __init__(self, session, api_key, latitude, longitude): + """Initialize.""" + self.latitude = latitude + self.longitude = longitude + self.airly = Airly(api_key, session) + self.data = {} + + @Throttle(DEFAULT_SCAN_INTERVAL) + async def async_update(self): + """Update Airly data.""" + + try: + with async_timeout.timeout(10): + measurements = self.airly.create_measurements_session_point( + self.latitude, self.longitude + ) + await measurements.update() + + values = measurements.current["values"] + index = measurements.current["indexes"][0] + standards = measurements.current["standards"] + + if index["description"] == NO_AIRLY_SENSORS: + _LOGGER.error("Can't retrieve data: no Airly sensors in this area") + return + for value in values: + self.data[value["name"]] = value["value"] + for standard in standards: + self.data[f"{standard['pollutant']}_LIMIT"] = standard["limit"] + self.data[f"{standard['pollutant']}_PERCENT"] = standard["percent"] + self.data[ATTR_API_CAQI] = index["value"] + self.data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ") + self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"] + self.data[ATTR_API_ADVICE] = index["advice"] + _LOGGER.debug("Data retrieved from Airly") + except ( + ValueError, + AirlyError, + asyncio.TimeoutError, + ClientConnectorError, + ) as error: + _LOGGER.error(error) + self.data = {} diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index f8500869509..082344c14e3 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -1,40 +1,29 @@ -"""Support for the Airly service.""" -import asyncio -import logging -from datetime import timedelta - -import async_timeout -from aiohttp.client_exceptions import ClientConnectorError -from airly import Airly -from airly.exceptions import AirlyError - -from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +"""Support for the Airly air_quality service.""" from homeassistant.components.air_quality import ( AirQualityEntity, ATTR_AQI, ATTR_PM_10, ATTR_PM_2_5, ) -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util import Throttle +from homeassistant.const import CONF_NAME -from .const import NO_AIRLY_SENSORS - -_LOGGER = logging.getLogger(__name__) +from .const import ( + ATTR_API_ADVICE, + ATTR_API_CAQI, + ATTR_API_CAQI_DESCRIPTION, + ATTR_API_CAQI_LEVEL, + ATTR_API_PM10, + ATTR_API_PM10_LIMIT, + ATTR_API_PM10_PERCENT, + ATTR_API_PM25, + ATTR_API_PM25_LIMIT, + ATTR_API_PM25_PERCENT, + DATA_CLIENT, + DOMAIN, +) ATTRIBUTION = "Data provided by Airly" -ATTR_API_ADVICE = "ADVICE" -ATTR_API_CAQI = "CAQI" -ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION" -ATTR_API_CAQI_LEVEL = "LEVEL" -ATTR_API_PM10 = "PM10" -ATTR_API_PM10_LIMIT = "PM10_LIMIT" -ATTR_API_PM10_PERCENT = "PM10_PERCENT" -ATTR_API_PM25 = "PM25" -ATTR_API_PM25_LIMIT = "PM25_LIMIT" -ATTR_API_PM25_PERCENT = "PM25_PERCENT" - LABEL_ADVICE = "advice" LABEL_AQI_LEVEL = f"{ATTR_AQI}_level" LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit" @@ -42,19 +31,12 @@ LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit" LABEL_PM_10_LIMIT = f"{ATTR_PM_10}_limit" LABEL_PM_10_PERCENT = f"{ATTR_PM_10}_percent_of_limit" -DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) - async def async_setup_entry(hass, config_entry, async_add_entities): - """Add a Airly entities from a config_entry.""" - api_key = config_entry.data[CONF_API_KEY] + """Set up Airly air_quality entity based on a config entry.""" name = config_entry.data[CONF_NAME] - latitude = config_entry.data[CONF_LATITUDE] - longitude = config_entry.data[CONF_LONGITUDE] - websession = async_get_clientsession(hass) - - data = AirlyData(websession, api_key, latitude, longitude) + data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] async_add_entities([AirlyAirQuality(data, name)], True) @@ -72,7 +54,7 @@ def round_state(func): class AirlyAirQuality(AirQualityEntity): - """Define an Airly air_quality.""" + """Define an Airly air quality.""" def __init__(self, airly, name): """Initialize.""" @@ -145,7 +127,7 @@ class AirlyAirQuality(AirQualityEntity): return self._attrs async def async_update(self): - """Get the data from Airly.""" + """Update the entity.""" await self.airly.async_update() if self.airly.data: @@ -154,51 +136,3 @@ class AirlyAirQuality(AirQualityEntity): self._pm_10 = self.data[ATTR_API_PM10] self._pm_2_5 = self.data[ATTR_API_PM25] self._aqi = self.data[ATTR_API_CAQI] - - -class AirlyData: - """Define an object to hold sensor data.""" - - def __init__(self, session, api_key, latitude, longitude): - """Initialize.""" - self.latitude = latitude - self.longitude = longitude - self.airly = Airly(api_key, session) - self.data = {} - - @Throttle(DEFAULT_SCAN_INTERVAL) - async def async_update(self): - """Update Airly data.""" - - try: - with async_timeout.timeout(10): - measurements = self.airly.create_measurements_session_point( - self.latitude, self.longitude - ) - await measurements.update() - - values = measurements.current["values"] - index = measurements.current["indexes"][0] - standards = measurements.current["standards"] - - if index["description"] == NO_AIRLY_SENSORS: - _LOGGER.error("Can't retrieve data: no Airly sensors in this area") - return - for value in values: - self.data[value["name"]] = value["value"] - for standard in standards: - self.data[f"{standard['pollutant']}_LIMIT"] = standard["limit"] - self.data[f"{standard['pollutant']}_PERCENT"] = standard["percent"] - self.data[ATTR_API_CAQI] = index["value"] - self.data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ") - self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"] - self.data[ATTR_API_ADVICE] = index["advice"] - _LOGGER.debug("Data retrieved from Airly") - except ( - ValueError, - AirlyError, - asyncio.TimeoutError, - ClientConnectorError, - ) as error: - _LOGGER.error(error) - self.data = {} diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 5313ba0e494..2040faea6b6 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -1,4 +1,19 @@ """Constants for Airly integration.""" +ATTR_API_ADVICE = "ADVICE" +ATTR_API_CAQI = "CAQI" +ATTR_API_CAQI_DESCRIPTION = "DESCRIPTION" +ATTR_API_CAQI_LEVEL = "LEVEL" +ATTR_API_HUMIDITY = "HUMIDITY" +ATTR_API_PM1 = "PM1" +ATTR_API_PM10 = "PM10" +ATTR_API_PM10_LIMIT = "PM10_LIMIT" +ATTR_API_PM10_PERCENT = "PM10_PERCENT" +ATTR_API_PM25 = "PM25" +ATTR_API_PM25_LIMIT = "PM25_LIMIT" +ATTR_API_PM25_PERCENT = "PM25_PERCENT" +ATTR_API_PRESSURE = "PRESSURE" +ATTR_API_TEMPERATURE = "TEMPERATURE" +DATA_CLIENT = "client" DEFAULT_NAME = "Airly" DOMAIN = "airly" NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py new file mode 100644 index 00000000000..03439d7d206 --- /dev/null +++ b/homeassistant/components/airly/sensor.py @@ -0,0 +1,154 @@ +"""Support for the Airly sensor service.""" +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_NAME, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PRESSURE_HPA, + TEMP_CELSIUS, +) +from homeassistant.helpers.entity import Entity + +from .const import ( + ATTR_API_HUMIDITY, + ATTR_API_PM1, + ATTR_API_PRESSURE, + ATTR_API_TEMPERATURE, + DATA_CLIENT, + DOMAIN, +) + +ATTRIBUTION = "Data provided by Airly" + +ATTR_ICON = "icon" +ATTR_LABEL = "label" +ATTR_UNIT = "unit" + +HUMI_PERCENT = "%" +VOLUME_MICROGRAMS_PER_CUBIC_METER = "µg/m³" + +SENSOR_TYPES = { + ATTR_API_PM1: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_PM1, + ATTR_UNIT: VOLUME_MICROGRAMS_PER_CUBIC_METER, + }, + ATTR_API_HUMIDITY: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_HUMIDITY.capitalize(), + ATTR_UNIT: HUMI_PERCENT, + }, + ATTR_API_PRESSURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_PRESSURE.capitalize(), + ATTR_UNIT: PRESSURE_HPA, + }, + ATTR_API_TEMPERATURE: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_ICON: None, + ATTR_LABEL: ATTR_API_TEMPERATURE.capitalize(), + ATTR_UNIT: TEMP_CELSIUS, + }, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Airly sensor entities based on a config entry.""" + name = config_entry.data[CONF_NAME] + + data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + + sensors = [] + for sensor in SENSOR_TYPES: + sensors.append(AirlySensor(data, name, sensor)) + async_add_entities(sensors, True) + + +def round_state(func): + """Round state.""" + + def _decorator(self): + res = func(self) + if isinstance(res, float): + return round(res) + return res + + return _decorator + + +class AirlySensor(Entity): + """Define an Airly sensor.""" + + def __init__(self, airly, name, kind): + """Initialize.""" + self.airly = airly + self.data = airly.data + self._name = name + self.kind = kind + self._device_class = None + self._state = None + self._icon = None + self._unit_of_measurement = None + self._attrs = {} + + @property + def name(self): + """Return the name.""" + return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + + @property + def state(self): + """Return the state.""" + self._state = self.data[self.kind] + if self.kind in [ATTR_API_PM1, ATTR_API_PRESSURE]: + self._state = round(self._state) + if self.kind in [ATTR_API_TEMPERATURE, ATTR_API_HUMIDITY]: + self._state = round(self._state, 1) + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attrs + + @property + def icon(self): + """Return the icon.""" + self._icon = SENSOR_TYPES[self.kind][ATTR_ICON] + return self._icon + + @property + def device_class(self): + """Return the device_class.""" + return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self.airly.latitude}-{self.airly.longitude}-{self.kind.lower()}" + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_TYPES[self.kind][ATTR_UNIT] + + @property + def attribution(self): + """Return the attribution.""" + return ATTRIBUTION + + @property + def available(self): + """Return True if entity is available.""" + return bool(self.airly.data) + + async def async_update(self): + """Update the sensor.""" + await self.airly.async_update() + + if self.airly.data: + self.data = self.airly.data From 14d3b9b8f932495779b7607e6f8078b255b32f00 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 16 Oct 2019 12:19:38 +0200 Subject: [PATCH 327/639] Bump pyatmo version to 2.3.2 (#27731) * Bump pyatmo version to 2.3.2 * Add reachable attribute * Add reachable attribute --- homeassistant/components/netatmo/manifest.json | 2 +- homeassistant/components/netatmo/sensor.py | 3 +++ requirements_all.txt | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index 83091368aff..efb2840216b 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -3,7 +3,7 @@ "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", "requirements": [ - "pyatmo==2.2.1" + "pyatmo==2.3.2" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 38e3753708e..70b6297388c 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -80,6 +80,7 @@ SENSOR_TYPES = { "gustangle": ["Gust Angle", "", "mdi:compass", None], "gustangle_value": ["Gust Angle Value", "º", "mdi:compass", None], "guststrength": ["Gust Strength", "km/h", "mdi:weather-windy", None], + "reachable": ["Reachability", "", "mdi:signal", None], "rf_status": ["Radio", "", "mdi:signal", None], "rf_status_lvl": ["Radio_lvl", "", "mdi:signal", None], "wifi_status": ["Wifi", "", "mdi:wifi", None], @@ -375,6 +376,8 @@ class NetatmoSensor(Entity): self._state = "N (%d\xb0)" % data["GustAngle"] elif self.type == "guststrength": self._state = data["GustStrength"] + elif self.type == "reachable": + self._state = data["reachable"] elif self.type == "rf_status_lvl": self._state = data["rf_status"] elif self.type == "rf_status": diff --git a/requirements_all.txt b/requirements_all.txt index 2b5b4f53e52..ac5415a4923 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1084,7 +1084,7 @@ pyalarmdotcom==0.3.2 pyarlo==0.2.3 # homeassistant.components.netatmo -pyatmo==2.2.1 +pyatmo==2.3.2 # homeassistant.components.atome pyatome==0.1.1 From a1b8f4d9c39d726fcaa64deaac3011d4f67c5ff9 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 16 Oct 2019 17:11:25 +0200 Subject: [PATCH 328/639] New cache on Azure (#27739) * New cache on Azure * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml * Update azure-pipelines-ci.yml --- azure-pipelines-ci.yml | 48 +++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 74e9ea107c5..5e42281bf7e 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -37,12 +37,14 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - displayName: 'Setup Env' + . venv/bin/activate + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate flake8 homeassistant tests script @@ -52,12 +54,14 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -e . - displayName: 'Setup Env' + . venv/bin/activate + pip install -e . - script: | . venv/bin/activate python -m script.hassfest validate @@ -71,12 +75,14 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - displayName: 'Setup Env' + . venv/bin/activate + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate ./script/check_format @@ -100,7 +106,7 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_test_all.txt, .cache, homeassistant/package_constraints.txt' + keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt' build: | set -e python -m venv venv @@ -111,6 +117,10 @@ stages: # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. # Find offending deps with `pipdeptree -r -p typing` pip uninstall -y typing + - script: | + . venv/bin/activate + pip install -e . + displayName: 'Install Home Assistant' - script: | set -e @@ -140,7 +150,7 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_all.txt, requirements_test.txt, .cache, homeassistant/package_constraints.txt' + keyfile: 'requirements_all.txt | requirements_test.txt | homeassistant/package_constraints.txt' build: | set -e python -m venv venv @@ -149,6 +159,10 @@ stages: pip install -U pip setuptools pip install -r requirements_all.txt -c homeassistant/package_constraints.txt pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + - script: | + . venv/bin/activate + pip install -e . + displayName: 'Install Home Assistant' - script: | . venv/bin/activate pylint homeassistant From 8a0f26e15547652c1ae7ff4050684a18762d375f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 16 Oct 2019 17:37:24 +0200 Subject: [PATCH 329/639] Add cache for mypy (#27745) * Add cache for mypy * Update ruamel_yaml.py --- azure-pipelines-ci.yml | 13 +++++++------ homeassistant/util/ruamel_yaml.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 5e42281bf7e..a566baf6561 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -172,13 +172,14 @@ stages: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - - script: | - python -m venv venv + - template: templates/azp-step-cache.yaml@azure + parameters: + keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' + build: | + python -m venv venv - . venv/bin/activate - pip install -e . - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - displayName: 'Setup Env' + . venv/bin/activate + pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate mypy homeassistant diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index b7e8927888c..6793784abae 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -90,7 +90,7 @@ def load_yaml(fname: str, round_trip: bool = False) -> JSON_TYPE: if round_trip: yaml = YAML(typ="rt") # type ignore: https://bitbucket.org/ruamel/yaml/pull-requests/42 - yaml.preserve_quotes = True # type: ignore + yaml.preserve_quotes = True else: if ExtSafeConstructor.name is None: ExtSafeConstructor.name = fname From bd95a89f45174f0430a6fdb140208c977d97242a Mon Sep 17 00:00:00 2001 From: Andrey Kupreychik Date: Thu, 17 Oct 2019 01:28:12 +0700 Subject: [PATCH 330/639] Bump ndms2-client to 0.0.10 (#27734) --- CODEOWNERS | 1 + homeassistant/components/keenetic_ndms2/manifest.json | 6 ++++-- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 547fe504892..40f1e93cfb9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -157,6 +157,7 @@ homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/kaiterra/* @Michsior14 homeassistant/components/keba/* @dannerph +homeassistant/components/keenetic_ndms2/* @foxel homeassistant/components/knx/* @Julius2342 homeassistant/components/kodi/* @armills homeassistant/components/konnected/* @heythisisnate diff --git a/homeassistant/components/keenetic_ndms2/manifest.json b/homeassistant/components/keenetic_ndms2/manifest.json index 41e45a9e578..4613d2d9608 100644 --- a/homeassistant/components/keenetic_ndms2/manifest.json +++ b/homeassistant/components/keenetic_ndms2/manifest.json @@ -3,8 +3,10 @@ "name": "Keenetic ndms2", "documentation": "https://www.home-assistant.io/integrations/keenetic_ndms2", "requirements": [ - "ndms2_client==0.0.9" + "ndms2_client==0.0.10" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@foxel" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index ac5415a4923..8a5733df4ec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -847,7 +847,7 @@ n26==0.2.7 nad_receiver==0.0.11 # homeassistant.components.keenetic_ndms2 -ndms2_client==0.0.9 +ndms2_client==0.0.10 # homeassistant.components.ness_alarm nessclient==0.9.15 From 0607a306125eb1482ad38a932c94c7a35a37104c Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Wed, 16 Oct 2019 20:28:59 +0200 Subject: [PATCH 331/639] Upgrade youtube_dl to 2019.10.16 (#27737) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 886535555d5..de3d4546ca0 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.09.28" + "youtube_dl==2019.10.16" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 8a5733df4ec..efbfb4c7dd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2017,7 +2017,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.09.28 +youtube_dl==2019.10.16 # homeassistant.components.zengge zengge==0.2 From 6ffc520b1ce3376bfa5001f4a2cb876ce4cab061 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 16 Oct 2019 20:45:03 +0200 Subject: [PATCH 332/639] Axis - Improve discovery title by adding placeholder support (#27663) * Improve discovery title by adding placeholder support --- homeassistant/components/axis/.translations/en.json | 1 + homeassistant/components/axis/config_flow.py | 7 +++++++ homeassistant/components/axis/strings.json | 1 + tests/components/axis/test_config_flow.py | 2 ++ 4 files changed, 11 insertions(+) diff --git a/homeassistant/components/axis/.translations/en.json b/homeassistant/components/axis/.translations/en.json index 5fd5d9be565..c7d84aa8cc3 100644 --- a/homeassistant/components/axis/.translations/en.json +++ b/homeassistant/components/axis/.translations/en.json @@ -12,6 +12,7 @@ "device_unavailable": "Device is not available", "faulty_credentials": "Bad user credentials" }, + "flow_title": "Axis device: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 3473eba3065..5eb4f9daddd 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -191,6 +191,12 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): load_json, self.hass.config.path(CONFIG_FILE) ) + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + "name": discovery_info["hostname"][:-7], + "host": discovery_info[CONF_HOST], + } + if serialnumber not in config_file: self.discovery_schema = { vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): str, @@ -198,6 +204,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_PASSWORD): str, vol.Required(CONF_PORT, default=discovery_info[CONF_PORT]): int, } + return await self.async_step_user() try: diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 29fe09b7e5b..2dc23f3e466 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -1,6 +1,7 @@ { "config": { "title": "Axis device", + "flow_title": "Axis device: {name} ({host})", "step": { "user": { "title": "Set up Axis device", diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index 5ec3f933e9e..5aec416961d 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -186,6 +186,7 @@ async def test_zeroconf_flow(hass): data={ config_flow.CONF_HOST: "1.2.3.4", config_flow.CONF_PORT: 80, + "hostname": "name", "properties": {"macaddress": "00408C12345"}, }, context={"source": "zeroconf"}, @@ -319,6 +320,7 @@ async def test_zeroconf_flow_bad_config_file(hass): config_flow.DOMAIN, data={ config_flow.CONF_HOST: "1.2.3.4", + "hostname": "name", "properties": {"macaddress": "00408C12345"}, }, context={"source": "zeroconf"}, From 43c85c0549df0e4beb4f2dcccce6ac1015fc1304 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 17 Oct 2019 06:34:56 +0200 Subject: [PATCH 333/639] Add device action support to the alarm_control_panel integration (#27616) * Add device action support to the alarm_control_panel integration * Improve tests --- .../alarm_control_panel/device_action.py | 126 ++++++++ .../alarm_control_panel/strings.json | 11 + .../components/device_automation/__init__.py | 19 ++ .../alarm_control_panel/test_device_action.py | 274 ++++++++++++++++++ .../components/device_automation/test_init.py | 102 +++++++ .../test/alarm_control_panel.py | 91 ++++++ 6 files changed, 623 insertions(+) create mode 100644 homeassistant/components/alarm_control_panel/device_action.py create mode 100644 homeassistant/components/alarm_control_panel/strings.json create mode 100644 tests/components/alarm_control_panel/test_device_action.py create mode 100644 tests/testing_config/custom_components/test/alarm_control_panel.py diff --git a/homeassistant/components/alarm_control_panel/device_action.py b/homeassistant/components/alarm_control_panel/device_action.py new file mode 100644 index 00000000000..a3c2b482261 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/device_action.py @@ -0,0 +1,126 @@ +"""Provides device automations for Alarm control panel.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_CODE, + ATTR_ENTITY_ID, + CONF_CODE, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import ATTR_CODE_ARM_REQUIRED, DOMAIN + +ACTION_TYPES = {"arm_away", "arm_home", "arm_night", "disarm", "trigger"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Optional(CONF_CODE): cv.string, + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Alarm control panel devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add actions for each entity that belongs to this integration + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_away", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_home", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "arm_night", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "disarm", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "trigger", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + if CONF_CODE in config: + service_data[ATTR_CODE] = config[CONF_CODE] + + if config[CONF_TYPE] == "arm_away": + service = SERVICE_ALARM_ARM_AWAY + elif config[CONF_TYPE] == "arm_home": + service = SERVICE_ALARM_ARM_HOME + elif config[CONF_TYPE] == "arm_night": + service = SERVICE_ALARM_ARM_NIGHT + elif config[CONF_TYPE] == "disarm": + service = SERVICE_ALARM_DISARM + elif config[CONF_TYPE] == "trigger": + service = SERVICE_ALARM_TRIGGER + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) + + +async def async_get_action_capabilities(hass, config): + """List action capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + code_required = state.attributes.get(ATTR_CODE_ARM_REQUIRED) if state else False + + if config[CONF_TYPE] == "trigger" or ( + config[CONF_TYPE] != "disarm" and not code_required + ): + return {} + + return {"extra_fields": vol.Schema({vol.Optional(CONF_CODE): str})} diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json new file mode 100644 index 00000000000..f67635776dd --- /dev/null +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Arm {entity_name} away", + "arm_home": "Arm {entity_name} home", + "arm_night": "Arm {entity_name} night", + "disarm": "Disarm {entity_name}", + "trigger": "Trigger {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 9d0a5a72a47..0be1c3eb1dd 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -59,6 +59,9 @@ async def async_setup(hass, config): hass.components.websocket_api.async_register_command( websocket_device_automation_list_triggers ) + hass.components.websocket_api.async_register_command( + websocket_device_automation_get_action_capabilities + ) hass.components.websocket_api.async_register_command( websocket_device_automation_get_condition_capabilities ) @@ -209,6 +212,22 @@ async def websocket_device_automation_list_triggers(hass, connection, msg): connection.send_result(msg["id"], triggers) +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "device_automation/action/capabilities", + vol.Required("action"): dict, + } +) +async def websocket_device_automation_get_action_capabilities(hass, connection, msg): + """Handle request for device action capabilities.""" + action = msg["action"] + capabilities = await _async_get_device_automation_capabilities( + hass, "action", action + ) + connection.send_result(msg["id"], capabilities) + + @websocket_api.async_response @websocket_api.websocket_command( { diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py new file mode 100644 index 00000000000..c2dfcbd78b9 --- /dev/null +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -0,0 +1,274 @@ +"""The tests for Alarm control panel device actions.""" +import pytest + +from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.const import ( + CONF_PLATFORM, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, + STATE_UNKNOWN, +) +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + mock_device_registry, + mock_registry, + async_get_device_automations, + async_get_device_automation_capabilities, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions from a alarm_control_panel.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "arm_away", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "arm_home", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "arm_night", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "disarm", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + { + "domain": DOMAIN, + "type": "trigger", + "device_id": device_entry.id, + "entity_id": "alarm_control_panel.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_get_action_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["no_arm_code"].unique_id, + device_id=device_entry.id, + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "arm_away": {"extra_fields": []}, + "arm_home": {"extra_fields": []}, + "arm_night": {"extra_fields": []}, + "disarm": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "trigger": {"extra_fields": []}, + } + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert len(actions) == 5 + for action in actions: + capabilities = await async_get_device_automation_capabilities( + hass, "action", action + ) + assert capabilities == expected_capabilities[action["type"]] + + +async def test_get_action_capabilities_arm_code(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["arm_code"].unique_id, + device_id=device_entry.id, + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "arm_away": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "arm_home": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "arm_night": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "disarm": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "trigger": {"extra_fields": []}, + } + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert len(actions) == 5 + for action in actions: + capabilities = await async_get_device_automation_capabilities( + hass, "action", action + ) + assert capabilities == expected_capabilities[action["type"]] + + +async def test_action(hass): + """Test for turn_on and turn_off actions.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_arm_away", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "arm_away", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_arm_home", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "arm_home", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_arm_night", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "arm_night", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event_disarm"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "disarm", + "code": "1234", + }, + }, + { + "trigger": { + "platform": "event", + "event_type": "test_event_trigger", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "alarm_control_panel.alarm_no_arm_code", + "type": "trigger", + }, + }, + ] + }, + ) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state == STATE_UNKNOWN + ) + + hass.bus.async_fire("test_event_arm_away") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_ARMED_AWAY + ) + + hass.bus.async_fire("test_event_arm_home") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_ARMED_HOME + ) + + hass.bus.async_fire("test_event_arm_night") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_ARMED_NIGHT + ) + + hass.bus.async_fire("test_event_disarm") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_DISARMED + ) + + hass.bus.async_fire("test_event_trigger") + await hass.async_block_till_done() + assert ( + hass.states.get("alarm_control_panel.alarm_no_arm_code").state + == STATE_ALARM_TRIGGERED + ) diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index 1af4b541a92..3c0e3b1eca7 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -170,6 +170,106 @@ async def test_websocket_get_triggers(hass, hass_ws_client, device_reg, entity_r assert _same_lists(triggers, expected_triggers) +async def test_websocket_get_action_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get the expected action capabilities for an alarm through websocket.""" + await async_setup_component(hass, "device_automation", {}) + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + "alarm_control_panel", "test", "5678", device_id=device_entry.id + ) + expected_capabilities = { + "arm_away": {"extra_fields": []}, + "arm_home": {"extra_fields": []}, + "arm_night": {"extra_fields": []}, + "disarm": { + "extra_fields": [{"name": "code", "optional": True, "type": "string"}] + }, + "trigger": {"extra_fields": []}, + } + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 1, "type": "device_automation/action/list", "device_id": device_entry.id} + ) + msg = await client.receive_json() + + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + actions = msg["result"] + + id = 2 + assert len(actions) == 5 + for action in actions: + await client.send_json( + { + "id": id, + "type": "device_automation/action/capabilities", + "action": action, + } + ) + msg = await client.receive_json() + assert msg["id"] == id + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities[action["type"]] + id = id + 1 + + +async def test_websocket_get_bad_action_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no action capabilities for a non existing domain.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/action/capabilities", + "action": {"domain": "beer"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + +async def test_websocket_get_no_action_capabilities( + hass, hass_ws_client, device_reg, entity_reg +): + """Test we get no action capabilities for a domain with no device action capabilities.""" + await async_setup_component(hass, "device_automation", {}) + expected_capabilities = {} + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 1, + "type": "device_automation/action/capabilities", + "action": {"domain": "deconz"}, + } + ) + msg = await client.receive_json() + assert msg["id"] == 1 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + capabilities = msg["result"] + assert capabilities == expected_capabilities + + async def test_websocket_get_condition_capabilities( hass, hass_ws_client, device_reg, entity_reg ): @@ -204,6 +304,7 @@ async def test_websocket_get_condition_capabilities( conditions = msg["result"] id = 2 + assert len(conditions) == 2 for condition in conditions: await client.send_json( { @@ -301,6 +402,7 @@ async def test_websocket_get_trigger_capabilities( triggers = msg["result"] id = 2 + assert len(triggers) == 2 for trigger in triggers: await client.send_json( { diff --git a/tests/testing_config/custom_components/test/alarm_control_panel.py b/tests/testing_config/custom_components/test/alarm_control_panel.py new file mode 100644 index 00000000000..0e2842f8695 --- /dev/null +++ b/tests/testing_config/custom_components/test/alarm_control_panel.py @@ -0,0 +1,91 @@ +""" +Provide a mock alarm_control_panel platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.alarm_control_panel import AlarmControlPanel +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from tests.common import MockEntity + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + "arm_code": MockAlarm( + name=f"Alarm arm code", + code_arm_required=True, + unique_id="unique_arm_code", + ), + "no_arm_code": MockAlarm( + name=f"Alarm no arm code", + code_arm_required=False, + unique_id="unique_no_arm_code", + ), + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockAlarm(MockEntity, AlarmControlPanel): + """Mock Alarm control panel class.""" + + def __init__(self, **values): + """Init the Mock Alarm Control Panel.""" + self._state = None + + MockEntity.__init__(self, **values) + + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return self._handle("code_arm_required") + + @property + def state(self): + """Return the state of the device.""" + return self._state + + def alarm_arm_away(self, code=None): + """Send arm away command.""" + self._state = STATE_ALARM_ARMED_AWAY + self.async_write_ha_state() + + def alarm_arm_home(self, code=None): + """Send arm home command.""" + self._state = STATE_ALARM_ARMED_HOME + self.async_write_ha_state() + + def alarm_arm_night(self, code=None): + """Send arm night command.""" + self._state = STATE_ALARM_ARMED_NIGHT + self.async_write_ha_state() + + def alarm_disarm(self, code=None): + """Send disarm command.""" + if code == "1234": + self._state = STATE_ALARM_DISARMED + self.async_write_ha_state() + + def alarm_trigger(self, code=None): + """Send alarm trigger command.""" + self._state = STATE_ALARM_TRIGGERED + self.async_write_ha_state() From e79a5baf9ec11bf211d8faf099e5cfc33db6d9e3 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Thu, 17 Oct 2019 06:36:19 +0200 Subject: [PATCH 334/639] Move imports in slack and socialblade (#27747) * Moved imports to top-level in samsungtv, slack and socialblade * Rewinded top-level imports in samsungtv component --- homeassistant/components/slack/notify.py | 10 ++++------ homeassistant/components/socialblade/sensor.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 1b9895aab76..b645a590c3c 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -3,11 +3,10 @@ import logging import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth +import slacker +from slacker import Slacker import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -15,6 +14,8 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,7 +46,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the Slack notification service.""" - import slacker channel = config.get(CONF_CHANNEL) api_key = config.get(CONF_API_KEY) @@ -67,7 +67,6 @@ class SlackNotificationService(BaseNotificationService): def __init__(self, default_channel, api_token, username, icon, is_allowed_path): """Initialize the service.""" - from slacker import Slacker self._default_channel = default_channel self._api_token = api_token @@ -84,7 +83,6 @@ class SlackNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - import slacker if kwargs.get(ATTR_TARGET) is None: targets = [self._default_channel] diff --git a/homeassistant/components/socialblade/sensor.py b/homeassistant/components/socialblade/sensor.py index 0acfb63a629..3d53e76a27a 100644 --- a/homeassistant/components/socialblade/sensor.py +++ b/homeassistant/components/socialblade/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import socialbladeclient import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -71,7 +72,6 @@ class SocialBladeSensor(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Social Blade.""" - import socialbladeclient try: data = socialbladeclient.get_data(self.channel_id) From 23db94c62764c17842db4c93d980aa2941c82ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 17 Oct 2019 07:36:43 +0300 Subject: [PATCH 335/639] Run mypy in pre-commit without args to match CI (#27741) --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55e00443ba1..48d77cfdc6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,4 +17,5 @@ repos: rev: v0.730 hooks: - id: mypy + args: [] exclude: ^script/scaffold/templates/ From 46f1166edd8847ce2d25104de0be7c500d590a98 Mon Sep 17 00:00:00 2001 From: kennedyshead Date: Thu, 17 Oct 2019 10:32:02 +0200 Subject: [PATCH 336/639] Fix On/Off for melissa (#27733) * Fixed On/Off for melissa fixes #27092 * reformatted --- homeassistant/components/melissa/climate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/melissa/climate.py b/homeassistant/components/melissa/climate.py index 10ea6200c6f..38f4977c96a 100644 --- a/homeassistant/components/melissa/climate.py +++ b/homeassistant/components/melissa/climate.py @@ -156,7 +156,9 @@ class MelissaClimate(ClimateDevice): return mode = self.hass_mode_to_melissa(hvac_mode) - await self.async_send({self._api.MODE: mode}) + await self.async_send( + {self._api.MODE: mode, self._api.STATE: self._api.STATE_ON} + ) async def async_send(self, value): """Send action to service.""" From 2d6d6ba90e5e2f179a07c4ecd7de1744e86a8025 Mon Sep 17 00:00:00 2001 From: Antonio Larrosa Date: Thu, 17 Oct 2019 11:29:08 +0200 Subject: [PATCH 337/639] Forget auth token when going offline so we can reconnect (#26630) When an amcrest camera was unplugged and then plugged again it was impossible to reconnect to it, since the old auth token was reused while we need to use a new one. In fact, the method that is called every minute to check the camera availability is going to fail always since we're reusing an old token. By forgetting the token (setting it to None) when going offline, we ensure that we'll regenerate it in the next commands thus allowing to reconnect to the camera when it comes back online. --- homeassistant/components/amcrest/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index f915872abf0..d49104a0b26 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -167,6 +167,8 @@ class AmcrestChecker(Http): offline = not self.available if offline and was_online: _LOGGER.error("%s camera offline: Too many errors", self._wrap_name) + with self._token_lock: + self._token = None dispatcher_send( self._hass, service_signal(SERVICE_UPDATE, self._wrap_name) ) From 7fd606a254ed902c0edb330849654ece07d49a1b Mon Sep 17 00:00:00 2001 From: Tomasz Jagusz Date: Thu, 17 Oct 2019 11:30:18 +0200 Subject: [PATCH 338/639] bump rpi.gpio to 0.7.0 (#27753) --- homeassistant/components/mcp23017/manifest.json | 2 +- homeassistant/components/rpi_gpio/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/mcp23017/manifest.json b/homeassistant/components/mcp23017/manifest.json index 2dbffd829f8..13c36424dd6 100644 --- a/homeassistant/components/mcp23017/manifest.json +++ b/homeassistant/components/mcp23017/manifest.json @@ -3,7 +3,7 @@ "name": "MCP23017 I/O Expander", "documentation": "https://www.home-assistant.io/integrations/mcp23017", "requirements": [ - "RPi.GPIO==0.6.5", + "RPi.GPIO==0.7.0", "adafruit-blinka==1.2.1", "adafruit-circuitpython-mcp230xx==1.1.2" ], diff --git a/homeassistant/components/rpi_gpio/manifest.json b/homeassistant/components/rpi_gpio/manifest.json index 0bee2baeddf..4d3ea4da010 100644 --- a/homeassistant/components/rpi_gpio/manifest.json +++ b/homeassistant/components/rpi_gpio/manifest.json @@ -3,7 +3,7 @@ "name": "Rpi gpio", "documentation": "https://www.home-assistant.io/integrations/rpi_gpio", "requirements": [ - "RPi.GPIO==0.6.5" + "RPi.GPIO==0.7.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index efbfb4c7dd1..c8d66f91468 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -82,7 +82,7 @@ PyXiaomiGateway==0.12.4 # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio -# RPi.GPIO==0.6.5 +# RPi.GPIO==0.7.0 # homeassistant.components.remember_the_milk RtmAPI==0.7.2 From b187ca93d02f3dea2e8f4f12fb8c29ad6b7a9b12 Mon Sep 17 00:00:00 2001 From: Tomasz Jagusz Date: Thu, 17 Oct 2019 12:24:53 +0200 Subject: [PATCH 339/639] Move imports in rpi_gpio (#27752) * move imports for rpi_gpio * fixed pylint error * fix pylint error * removed empty line * add missing blank line * sort with isort --- homeassistant/components/rpi_gpio/__init__.py | 13 ++----------- homeassistant/components/rpi_gpio/binary_sensor.py | 2 +- homeassistant/components/rpi_gpio/cover.py | 4 ++-- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/rpi_gpio/__init__.py b/homeassistant/components/rpi_gpio/__init__.py index 31509614df4..ed7eefbb1fe 100644 --- a/homeassistant/components/rpi_gpio/__init__.py +++ b/homeassistant/components/rpi_gpio/__init__.py @@ -1,6 +1,8 @@ """Support for controlling GPIO pins of a Raspberry Pi.""" import logging +from RPi import GPIO # pylint: disable=import-error + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -10,7 +12,6 @@ DOMAIN = "rpi_gpio" def setup(hass, config): """Set up the Raspberry PI GPIO component.""" - from RPi import GPIO # pylint: disable=import-error def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -27,34 +28,24 @@ def setup(hass, config): def setup_output(port): """Set up a GPIO as output.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.setup(port, GPIO.OUT) def setup_input(port, pull_mode): """Set up a GPIO as input.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.setup(port, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP) def write_output(port, value): """Write a value to a GPIO.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.output(port, value) def read_input(port): """Read a value from a GPIO.""" - from RPi import GPIO # pylint: disable=import-error - return GPIO.input(port) def edge_detect(port, event_callback, bounce): """Add detection for RISING and FALLING events.""" - from RPi import GPIO # pylint: disable=import-error - GPIO.add_event_detect(port, GPIO.BOTH, callback=event_callback, bouncetime=bounce) diff --git a/homeassistant/components/rpi_gpio/binary_sensor.py b/homeassistant/components/rpi_gpio/binary_sensor.py index 4acbed9a0fa..3e38da47eed 100644 --- a/homeassistant/components/rpi_gpio/binary_sensor.py +++ b/homeassistant/components/rpi_gpio/binary_sensor.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import rpi_gpio -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv diff --git a/homeassistant/components/rpi_gpio/cover.py b/homeassistant/components/rpi_gpio/cover.py index 83cc497324d..648171b9738 100644 --- a/homeassistant/components/rpi_gpio/cover.py +++ b/homeassistant/components/rpi_gpio/cover.py @@ -4,9 +4,9 @@ from time import sleep import voluptuous as vol -from homeassistant.components.cover import CoverDevice, PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME from homeassistant.components import rpi_gpio +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice +from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) From e54f5102aaab112d4b89fc12e34c8393cfd85d6d Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 14:58:23 +0200 Subject: [PATCH 340/639] Move imports in ifttt component (#27792) --- homeassistant/components/ifttt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index bed0cb45b1d..05d773e9fd6 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -2,6 +2,7 @@ import json import logging +import pyfttt import requests import voluptuous as vol @@ -69,7 +70,6 @@ async def async_setup(hass, config): target_keys[target] = api_keys[target] try: - import pyfttt for target, key in target_keys.items(): res = pyfttt.send_event(key, event, value1, value2, value3) From 35e0acf0a531e0a4abed047de190320c708a340f Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 14:58:56 +0200 Subject: [PATCH 341/639] Move imports in keyboard component (#27791) --- homeassistant/components/keyboard/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/keyboard/__init__.py b/homeassistant/components/keyboard/__init__.py index 39725eec86b..0c5acf5b593 100644 --- a/homeassistant/components/keyboard/__init__.py +++ b/homeassistant/components/keyboard/__init__.py @@ -1,4 +1,5 @@ """Support to emulate keyboard presses on host machine.""" +from pykeyboard import PyKeyboard # pylint: disable=import-error import voluptuous as vol from homeassistant.const import ( @@ -17,9 +18,8 @@ TAP_KEY_SCHEMA = vol.Schema({}) def setup(hass, config): """Listen for keyboard events.""" - import pykeyboard # pylint: disable=import-error - keyboard = pykeyboard.PyKeyboard() + keyboard = PyKeyboard() keyboard.special_key_assignment() hass.services.register( From 9f7138452470b50f2da801dbb85ad1b8757e4c6b Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 14:59:36 +0200 Subject: [PATCH 342/639] Move imports in linux_battery component (#27789) --- homeassistant/components/linux_battery/sensor.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/linux_battery/sensor.py b/homeassistant/components/linux_battery/sensor.py index 9256c3ad18d..bc02affdaed 100644 --- a/homeassistant/components/linux_battery/sensor.py +++ b/homeassistant/components/linux_battery/sensor.py @@ -2,6 +2,7 @@ import logging import os +from batinfo import Batteries import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -72,15 +73,12 @@ class LinuxBatterySensor(Entity): def __init__(self, name, battery_id, system): """Initialize the battery sensor.""" - import batinfo - - self._battery = batinfo.Batteries() + self._battery = Batteries() self._name = name self._battery_stat = None self._battery_id = battery_id - 1 self._system = system - self._unit_of_measurement = "%" @property def name(self): @@ -100,7 +98,7 @@ class LinuxBatterySensor(Entity): @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" - return self._unit_of_measurement + return "%" @property def device_state_attributes(self): From 4efa6689e493760e8b96b3fe8a5f650e50506f7f Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 15:00:00 +0200 Subject: [PATCH 343/639] Move imports in ampio component (#27788) --- homeassistant/components/ampio/air_quality.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ampio/air_quality.py b/homeassistant/components/ampio/air_quality.py index e63f59839a8..c925909a9a8 100644 --- a/homeassistant/components/ampio/air_quality.py +++ b/homeassistant/components/ampio/air_quality.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from asmog import AmpioSmog import voluptuous as vol from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity @@ -23,7 +24,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Ampio Smog air quality platform.""" - from asmog import AmpioSmog name = config.get(CONF_NAME) station_id = config[CONF_STATION_ID] From dc72aa48da3e8d64e7aa27e12c0d61802f81121d Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 15:00:32 +0200 Subject: [PATCH 344/639] Move imports in liveboxplaytv component (#27790) --- homeassistant/components/liveboxplaytv/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/liveboxplaytv/media_player.py b/homeassistant/components/liveboxplaytv/media_player.py index c466d71c4c5..996b4f33b50 100644 --- a/homeassistant/components/liveboxplaytv/media_player.py +++ b/homeassistant/components/liveboxplaytv/media_player.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from liveboxplaytv import LiveboxPlayTv +import pyteleloisirs import requests import voluptuous as vol @@ -85,7 +87,6 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): def __init__(self, host, port, name): """Initialize the Livebox Play TV device.""" - from liveboxplaytv import LiveboxPlayTv self._client = LiveboxPlayTv(host, port) # Assume that the appliance is not muted @@ -103,7 +104,6 @@ class LiveboxPlayTvDevice(MediaPlayerDevice): async def async_update(self): """Retrieve the latest data.""" - import pyteleloisirs try: self._state = self.refresh_state() From 88a78a4a18a8b592b63f5da39d9ae09f769cca28 Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 15:01:09 +0200 Subject: [PATCH 345/639] Move imports in amcrest component (#27787) --- homeassistant/components/amcrest/camera.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index f75a5adbe9c..e9e1e2b5f84 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -2,19 +2,20 @@ import asyncio from datetime import timedelta import logging -from urllib3.exceptions import HTTPError from amcrest import AmcrestError +from haffmpeg.camera import CameraMjpeg +from urllib3.exceptions import HTTPError import voluptuous as vol from homeassistant.components.camera import ( - Camera, CAMERA_SERVICE_SCHEMA, SUPPORT_ON_OFF, SUPPORT_STREAM, + Camera, ) from homeassistant.components.ffmpeg import DATA_FFMPEG -from homeassistant.const import CONF_NAME, STATE_ON, STATE_OFF +from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_stream, async_aiohttp_proxy_web, @@ -159,7 +160,6 @@ class AmcrestCam(Camera): return await async_aiohttp_proxy_web(self.hass, request, stream_coro) # streaming via ffmpeg - from haffmpeg.camera import CameraMjpeg streaming_url = self._rtsp_url stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) From 9dc0c05ee0fc8d87c0f3a72366dc08bd98171097 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 15:01:50 +0200 Subject: [PATCH 346/639] Move imports in imap + imap_email_content component (#27793) --- homeassistant/components/imap/sensor.py | 15 +++++---------- .../components/imap_email_content/sensor.py | 5 +---- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/imap/sensor.py b/homeassistant/components/imap/sensor.py index 0ae79d34cf0..a10fefa1b16 100644 --- a/homeassistant/components/imap/sensor.py +++ b/homeassistant/components/imap/sensor.py @@ -2,6 +2,7 @@ import asyncio import logging +from aioimaplib import IMAP4_SSL, AioImapException import async_timeout import voluptuous as vol @@ -107,24 +108,20 @@ class ImapSensor(Entity): async def connection(self): """Return a connection to the server, establishing it if necessary.""" - import aioimaplib - if self._connection is None: try: - self._connection = aioimaplib.IMAP4_SSL(self._server, self._port) + self._connection = IMAP4_SSL(self._server, self._port) await self._connection.wait_hello_from_server() await self._connection.login(self._user, self._password) await self._connection.select(self._folder) self._does_push = self._connection.has_capability("IDLE") - except (aioimaplib.AioImapException, asyncio.TimeoutError): + except (AioImapException, asyncio.TimeoutError): self._connection = None return self._connection async def idle_loop(self): """Wait for data pushed from server.""" - import aioimaplib - while True: try: if await self.connection(): @@ -138,17 +135,15 @@ class ImapSensor(Entity): await idle else: await self.async_update_ha_state() - except (aioimaplib.AioImapException, asyncio.TimeoutError): + except (AioImapException, asyncio.TimeoutError): self.disconnected() async def async_update(self): """Periodic polling of state.""" - import aioimaplib - try: if await self.connection(): await self.refresh_email_count() - except (aioimaplib.AioImapException, asyncio.TimeoutError): + except (AioImapException, asyncio.TimeoutError): self.disconnected() async def refresh_email_count(self): diff --git a/homeassistant/components/imap_email_content/sensor.py b/homeassistant/components/imap_email_content/sensor.py index c5171cde646..62dceae0dad 100644 --- a/homeassistant/components/imap_email_content/sensor.py +++ b/homeassistant/components/imap_email_content/sensor.py @@ -4,6 +4,7 @@ import datetime import email from collections import deque +import imaplib import voluptuous as vol from homeassistant.helpers.entity import Entity @@ -88,8 +89,6 @@ class EmailReader: def connect(self): """Login and setup the connection.""" - import imaplib - try: self.connection = imaplib.IMAP4_SSL(self._server, self._port) self.connection.login(self._user, self._password) @@ -110,8 +109,6 @@ class EmailReader: def read_next(self): """Read the next email from the email server.""" - import imaplib - try: self.connection.select(self._folder, readonly=True) From 8350e1246a48ed3b2297baacf8a3568cd34741c5 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 15:03:05 +0200 Subject: [PATCH 347/639] Move imports in netgear_lte component (#27777) --- homeassistant/components/netgear_lte/__init__.py | 4 +--- homeassistant/components/netgear_lte/notify.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/netgear_lte/__init__.py b/homeassistant/components/netgear_lte/__init__.py index 2514b37657f..4758a13c391 100644 --- a/homeassistant/components/netgear_lte/__init__.py +++ b/homeassistant/components/netgear_lte/__init__.py @@ -5,6 +5,7 @@ import logging import aiohttp import attr +import eternalegypt import voluptuous as vol from homeassistant.const import ( @@ -139,7 +140,6 @@ class ModemData: async def async_update(self): """Call the API to update the data.""" - import eternalegypt try: self.data = await self.modem.information() @@ -264,7 +264,6 @@ async def async_setup(hass, config): async def _setup_lte(hass, lte_config): """Set up a Netgear LTE modem.""" - import eternalegypt host = lte_config[CONF_HOST] password = lte_config[CONF_PASSWORD] @@ -322,7 +321,6 @@ async def _login(hass, modem_data, password): async def _retry_login(hass, modem_data, password): """Sleep and retry setup.""" - import eternalegypt _LOGGER.warning("Could not connect to %s. Will keep trying", modem_data.host) diff --git a/homeassistant/components/netgear_lte/notify.py b/homeassistant/components/netgear_lte/notify.py index 4f13662519d..9700ee3c715 100644 --- a/homeassistant/components/netgear_lte/notify.py +++ b/homeassistant/components/netgear_lte/notify.py @@ -2,6 +2,7 @@ import logging import attr +import eternalegypt from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService, DOMAIN @@ -27,7 +28,6 @@ class NetgearNotifyService(BaseNotificationService): async def async_send_message(self, message="", **kwargs): """Send a message to a user.""" - import eternalegypt modem_data = self.hass.data[DATA_KEY].get_modem_data(self.config) if not modem_data: From ab598da4bae5010618def19176fb14c6b80bc8c3 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 15:03:50 +0200 Subject: [PATCH 348/639] Move imports in nest component (#27778) --- homeassistant/components/nest/__init__.py | 9 ++------- homeassistant/components/nest/climate.py | 4 ++-- homeassistant/components/nest/local_auth.py | 5 ++--- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index cf1ba36aa89..32bbd009417 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -4,6 +4,8 @@ import socket from datetime import datetime, timedelta import threading +from nest import Nest +from nest.nest import AuthorizationError, APIError import voluptuous as vol from homeassistant import config_entries @@ -142,7 +144,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up Nest from a config entry.""" - from nest import Nest nest = Nest(access_token=entry.data["tokens"]["access_token"]) @@ -286,8 +287,6 @@ class NestDevice: def initialize(self): """Initialize Nest.""" - from nest.nest import AuthorizationError, APIError - try: # Do not optimize next statement, it is here for initialize # persistence Nest API connection. @@ -302,8 +301,6 @@ class NestDevice: def structures(self): """Generate a list of structures.""" - from nest.nest import AuthorizationError, APIError - try: for structure in self.nest.structures: if structure.name not in self.local_structure: @@ -332,8 +329,6 @@ class NestDevice: def _devices(self, device_type): """Generate a list of Nest devices.""" - from nest.nest import AuthorizationError, APIError - try: for structure in self.nest.structures: if structure.name not in self.local_structure: diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index eec7108cdea..795ce5c80e9 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -1,6 +1,7 @@ """Support for Nest thermostats.""" import logging +from nest.nest import APIError import voluptuous as vol from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice @@ -232,7 +233,6 @@ class NestThermostat(ClimateDevice): def set_temperature(self, **kwargs): """Set new target temperature.""" - import nest temp = None target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) @@ -247,7 +247,7 @@ class NestThermostat(ClimateDevice): try: if temp is not None: self.device.target = temp - except nest.nest.APIError as api_error: + except APIError as api_error: _LOGGER.error("An error occurred while setting temperature: %s", api_error) # restore target temperature self.schedule_update_ha_state(True) diff --git a/homeassistant/components/nest/local_auth.py b/homeassistant/components/nest/local_auth.py index 51d826c242f..38d1827326d 100644 --- a/homeassistant/components/nest/local_auth.py +++ b/homeassistant/components/nest/local_auth.py @@ -2,6 +2,8 @@ import asyncio from functools import partial +from nest.nest import NestAuth, AUTHORIZE_URL, AuthorizationError + from homeassistant.core import callback from . import config_flow from .const import DOMAIN @@ -21,14 +23,11 @@ def initialize(hass, client_id, client_secret): async def generate_auth_url(client_id, flow_id): """Generate an authorize url.""" - from nest.nest import AUTHORIZE_URL - return AUTHORIZE_URL.format(client_id, flow_id) async def resolve_auth_code(hass, client_id, client_secret, code): """Resolve an authorization code.""" - from nest.nest import NestAuth, AuthorizationError result = asyncio.Future() auth = NestAuth( From 62fcea2a8d72cb78c8a984d65d9e76f9e3c27443 Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 15:04:41 +0200 Subject: [PATCH 349/639] moved imports to top level (#27781) --- homeassistant/components/airvisual/sensor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 20e5196c0f1..888d6ae6ec9 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -1,7 +1,9 @@ """Support for AirVisual air quality sensors.""" -from logging import getLogger from datetime import timedelta +from logging import getLogger +from pyairvisual import Client +from pyairvisual.errors import AirVisualError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -14,8 +16,8 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL, - CONF_STATE, CONF_SHOW_ON_MAP, + CONF_STATE, ) from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.entity import Entity @@ -97,7 +99,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Configure the platform and add the sensors.""" - from pyairvisual import Client city = config.get(CONF_CITY) state = config.get(CONF_STATE) @@ -249,7 +250,6 @@ class AirVisualData: async def _async_update(self): """Update AirVisual data.""" - from pyairvisual.errors import AirVisualError try: if self.city and self.state and self.country: From 12a8e7520e9785d72d31e443efb3d432fae29ef6 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 15:05:14 +0200 Subject: [PATCH 350/639] Move imports in netgear component (#27776) --- homeassistant/components/netgear/device_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/netgear/device_tracker.py b/homeassistant/components/netgear/device_tracker.py index b52e446ba5d..2e20f6423a5 100644 --- a/homeassistant/components/netgear/device_tracker.py +++ b/homeassistant/components/netgear/device_tracker.py @@ -1,6 +1,7 @@ """Support for Netgear routers.""" import logging +from pynetgear import Netgear import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -71,14 +72,13 @@ class NetgearDeviceScanner(DeviceScanner): accesspoints, ): """Initialize the scanner.""" - import pynetgear self.tracked_devices = devices self.excluded_devices = excluded_devices self.tracked_accesspoints = accesspoints self.last_results = [] - self._api = pynetgear.Netgear(password, host, username, port, ssl) + self._api = Netgear(password, host, username, port, ssl) _LOGGER.info("Logging in") From 2c535c92bdbb76b138ec60336f280cb5f3b4f89a Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 15:05:45 +0200 Subject: [PATCH 351/639] moved imports to top level (#27784) --- homeassistant/components/alarmdotcom/alarm_control_panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alarmdotcom/alarm_control_panel.py b/homeassistant/components/alarmdotcom/alarm_control_panel.py index f80e8d6eb1e..07d69960e0b 100644 --- a/homeassistant/components/alarmdotcom/alarm_control_panel.py +++ b/homeassistant/components/alarmdotcom/alarm_control_panel.py @@ -2,6 +2,7 @@ import logging import re +from pyalarmdotcom import Alarmdotcom import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm @@ -49,7 +50,6 @@ class AlarmDotCom(alarm.AlarmControlPanel): def __init__(self, hass, name, code, username, password): """Initialize the Alarm.com status.""" - from pyalarmdotcom import Alarmdotcom _LOGGER.debug("Setting up Alarm.com...") self._hass = hass From 28cef89e038381570c75e5d5fd66821fc16b76af Mon Sep 17 00:00:00 2001 From: Jeff Irion Date: Thu, 17 Oct 2019 06:33:20 -0700 Subject: [PATCH 352/639] Generate ADB key for Android TV integration (#27344) * Generate ADB key for Android TV integration * Remove 'do_nothing' function * Remove 'return True' * Re-add 2 'return True' lines --- .../components/androidtv/manifest.json | 4 +- .../components/androidtv/media_player.py | 46 ++++++++----- requirements_all.txt | 4 +- requirements_test_all.txt | 4 +- tests/components/androidtv/patchers.py | 14 +++- .../components/androidtv/test_media_player.py | 69 +++++++++++++------ 6 files changed, 96 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index e84ed35c763..9ec993b9f91 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Androidtv", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.0.4", - "androidtv==0.0.30" + "adb-shell==0.0.7", + "androidtv==0.0.32" ], "dependencies": [], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index fcf4950f5e2..62ae93f96e4 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -1,8 +1,10 @@ """Support for functionality to interact with Android TV / Fire TV devices.""" import functools import logging +import os import voluptuous as vol +from adb_shell.auth.keygen import keygen from adb_shell.exceptions import ( InvalidChecksumError, InvalidCommandError, @@ -40,6 +42,7 @@ from homeassistant.const import ( ) from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.storage import STORAGE_DIR ANDROIDTV_DOMAIN = "androidtv" @@ -133,27 +136,39 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if CONF_ADB_SERVER_IP not in config: # Use "adb_shell" (Python ADB implementation) - adb_log = "using Python ADB implementation " + ( - f"with adbkey='{config[CONF_ADBKEY]}'" - if CONF_ADBKEY in config - else "without adbkey authentication" - ) - if CONF_ADBKEY in config: + if CONF_ADBKEY not in config: + # Generate ADB key files (if they don't exist) + adbkey = hass.config.path(STORAGE_DIR, "androidtv_adbkey") + if not os.path.isfile(adbkey): + keygen(adbkey) + + adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" + + aftv = setup( + host, + adbkey, + device_class=config[CONF_DEVICE_CLASS], + state_detection_rules=config[CONF_STATE_DETECTION_RULES], + auth_timeout_s=10.0, + ) + + else: + adb_log = ( + f"using Python ADB implementation with adbkey='{config[CONF_ADBKEY]}'" + ) + aftv = setup( host, config[CONF_ADBKEY], device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], + auth_timeout_s=10.0, ) - else: - aftv = setup( - host, - device_class=config[CONF_DEVICE_CLASS], - state_detection_rules=config[CONF_STATE_DETECTION_RULES], - ) else: # Use "pure-python-adb" (communicate with ADB server) + adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" + aftv = setup( host, adb_server_ip=config[CONF_ADB_SERVER_IP], @@ -161,7 +176,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device_class=config[CONF_DEVICE_CLASS], state_detection_rules=config[CONF_STATE_DETECTION_RULES], ) - adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" if not aftv.available: # Determine the name that will be used for the device in the log @@ -257,7 +271,7 @@ def adb_decorator(override_available=False): "establishing attempt in the next update. Error: %s", err, ) - self.aftv.adb.close() + self.aftv.adb_close() self._available = False # pylint: disable=protected-access return None @@ -429,7 +443,7 @@ class AndroidTVDevice(ADBDevice): # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.connect(always_log_errors=False) + self._available = self.aftv.adb_connect(always_log_errors=False) # To be safe, wait until the next update to run ADB commands if # using the Python ADB implementation. @@ -508,7 +522,7 @@ class FireTVDevice(ADBDevice): # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.connect(always_log_errors=False) + self._available = self.aftv.adb_connect(always_log_errors=False) # To be safe, wait until the next update to run ADB commands if # using the Python ADB implementation. diff --git a/requirements_all.txt b/requirements_all.txt index c8d66f91468..6a70451cc12 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -112,7 +112,7 @@ adafruit-blinka==1.2.1 adafruit-circuitpython-mcp230xx==1.1.2 # homeassistant.components.androidtv -adb-shell==0.0.4 +adb-shell==0.0.7 # homeassistant.components.adguard adguardhome==0.2.1 @@ -203,7 +203,7 @@ ambiclimate==0.2.1 amcrest==1.5.3 # homeassistant.components.androidtv -androidtv==0.0.30 +androidtv==0.0.32 # homeassistant.components.anel_pwrctrl anel_pwrctrl-homeassistant==0.0.1.dev2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 967943894fb..ece529ef6e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -49,7 +49,7 @@ YesssSMS==0.4.1 abodepy==0.16.5 # homeassistant.components.androidtv -adb-shell==0.0.4 +adb-shell==0.0.7 # homeassistant.components.adguard adguardhome==0.2.1 @@ -98,7 +98,7 @@ airly==0.0.2 ambiclimate==0.2.1 # homeassistant.components.androidtv -androidtv==0.0.30 +androidtv==0.0.32 # homeassistant.components.apns apns2==0.3.0 diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index 73aa5225989..5fc6bc754fa 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,7 +1,7 @@ """Define patches used for androidtv tests.""" from socket import error as socket_error -from unittest.mock import patch +from unittest.mock import mock_open, patch class AdbDeviceFake: @@ -128,3 +128,15 @@ def patch_shell(response=None, error=False): PATCH_ADB_DEVICE = patch("androidtv.adb_manager.AdbDevice", AdbDeviceFake) +PATCH_ANDROIDTV_OPEN = patch("androidtv.adb_manager.open", mock_open()) +PATCH_KEYGEN = patch("homeassistant.components.androidtv.media_player.keygen") +PATCH_SIGNER = patch("androidtv.adb_manager.PythonRSASigner") + + +def isfile(filepath): + """Mock `os.path.isfile`.""" + return filepath.endswith("adbkey") + + +PATCH_ISFILE = patch("os.path.isfile", isfile) +PATCH_ACCESS = patch("os.access", return_value=True) diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index feffc70d841..85f562a3500 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -5,6 +5,7 @@ from homeassistant.setup import async_setup_component from homeassistant.components.androidtv.media_player import ( ANDROIDTV_DOMAIN, CONF_ADB_SERVER_IP, + CONF_ADBKEY, ) from homeassistant.components.media_player.const import DOMAIN from homeassistant.const import ( @@ -61,14 +62,8 @@ CONFIG_FIRETV_ADB_SERVER = { } -async def _test_reconnect(hass, caplog, config): - """Test that the error and reconnection attempts are logged correctly. - - "Handles device/service unavailable. Log a warning once when - unavailable, log once when reconnected." - - https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html - """ +def _setup(hass, config): + """Perform common setup tasks for the tests.""" if CONF_ADB_SERVER_IP not in config[DOMAIN]: patch_key = "python" else: @@ -79,10 +74,26 @@ async def _test_reconnect(hass, caplog, config): else: entity_id = "media_player.fire_tv" + return patch_key, entity_id + + +async def _test_reconnect(hass, caplog, config): + """Test that the error and reconnection attempts are logged correctly. + + "Handles device/service unavailable. Log a warning once when + unavailable, log once when reconnected." + + https://developers.home-assistant.io/docs/en/integration_quality_scale_index.html + """ + patch_key, entity_id = _setup(hass, config) + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ patch_key - ], patchers.patch_shell("")[patch_key]: + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -93,7 +104,7 @@ async def _test_reconnect(hass, caplog, config): with patchers.patch_connect(False)[patch_key], patchers.patch_shell(error=True)[ patch_key - ]: + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: for _ in range(5): await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -105,7 +116,9 @@ async def _test_reconnect(hass, caplog, config): assert caplog.record_tuples[1][1] == logging.WARNING caplog.set_level(logging.DEBUG) - with patchers.patch_connect(True)[patch_key], patchers.patch_shell("1")[patch_key]: + with patchers.patch_connect(True)[patch_key], patchers.patch_shell("1")[ + patch_key + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: # Update 1 will reconnect await hass.helpers.entity_component.async_update_entity(entity_id) @@ -143,19 +156,13 @@ async def _test_adb_shell_returns_none(hass, config): The state should be `None` and the device should be unavailable. """ - if CONF_ADB_SERVER_IP not in config[DOMAIN]: - patch_key = "python" - else: - patch_key = "server" - - if config[DOMAIN].get(CONF_DEVICE_CLASS) != "firetv": - entity_id = "media_player.android_tv" - else: - entity_id = "media_player.fire_tv" + patch_key, entity_id = _setup(hass, config) with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ patch_key - ], patchers.patch_shell("")[patch_key]: + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_KEYGEN, patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: assert await async_setup_component(hass, DOMAIN, config) await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) @@ -164,7 +171,7 @@ async def _test_adb_shell_returns_none(hass, config): with patchers.patch_shell(None)[patch_key], patchers.patch_shell(error=True)[ patch_key - ]: + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER: await hass.helpers.entity_component.async_update_entity(entity_id) state = hass.states.get(entity_id) assert state is not None @@ -251,3 +258,21 @@ async def test_adb_shell_returns_none_firetv_adb_server(hass): """ assert await _test_adb_shell_returns_none(hass, CONFIG_FIRETV_ADB_SERVER) + + +async def test_setup_with_adbkey(hass): + """Test that setup succeeds when using an ADB key.""" + config = CONFIG_ANDROIDTV_PYTHON_ADB.copy() + config[DOMAIN][CONF_ADBKEY] = hass.config.path("user_provided_adbkey") + patch_key, entity_id = _setup(hass, config) + + with patchers.PATCH_ADB_DEVICE, patchers.patch_connect(True)[ + patch_key + ], patchers.patch_shell("")[ + patch_key + ], patchers.PATCH_ANDROIDTV_OPEN, patchers.PATCH_SIGNER, patchers.PATCH_ISFILE, patchers.PATCH_ACCESS: + assert await async_setup_component(hass, DOMAIN, config) + await hass.helpers.entity_component.async_update_entity(entity_id) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF From 136df743e3da6ed07fbce3af955d290b46ff780b Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 15:38:58 +0200 Subject: [PATCH 353/639] moved imports to top level (#27782) --- homeassistant/components/aladdin_connect/cover.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/aladdin_connect/cover.py b/homeassistant/components/aladdin_connect/cover.py index b3da4fb4cbc..4cfcd5403dd 100644 --- a/homeassistant/components/aladdin_connect/cover.py +++ b/homeassistant/components/aladdin_connect/cover.py @@ -1,21 +1,22 @@ """Platform for the Aladdin Connect cover component.""" import logging +from aladdin_connect import AladdinConnectClient import voluptuous as vol from homeassistant.components.cover import ( - CoverDevice, PLATFORM_SCHEMA, - SUPPORT_OPEN, SUPPORT_CLOSE, + SUPPORT_OPEN, + CoverDevice, ) from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, + CONF_USERNAME, STATE_CLOSED, - STATE_OPENING, STATE_CLOSING, STATE_OPEN, + STATE_OPENING, ) import homeassistant.helpers.config_validation as cv @@ -40,7 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Aladdin Connect platform.""" - from aladdin_connect import AladdinConnectClient username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) From d52476333e935a35825149fbdf353d9fc739a8d4 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 17 Oct 2019 17:06:33 +0200 Subject: [PATCH 354/639] Update devcontainer.json --- .devcontainer/devcontainer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index afb273331aa..5bfd37fab36 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "dockerFile": "../Dockerfile.dev", "postCreateCommand": "mkdir -p config && pip3 install -e .", "appPort": 8123, - "runArgs": ["-e", "GIT_EDITOR=\"code --wait\""], + "runArgs": ["-e", "GIT_EDITOR=code --wait"], "extensions": [ "ms-python.python", "visualstudioexptteam.vscodeintellicode", From ba0107f9127f4c13dbbcf5ff5d23d69717bf60ab Mon Sep 17 00:00:00 2001 From: bouni Date: Thu, 17 Oct 2019 17:07:36 +0200 Subject: [PATCH 355/639] Move imports in android_ip_webcam component (#27797) --- .../components/android_ip_webcam/__init__.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/android_ip_webcam/__init__.py b/homeassistant/components/android_ip_webcam/__init__.py index 33362bd37cc..1f9df527c28 100644 --- a/homeassistant/components/android_ip_webcam/__init__.py +++ b/homeassistant/components/android_ip_webcam/__init__.py @@ -1,34 +1,35 @@ """Support for Android IP Webcam.""" import asyncio -import logging from datetime import timedelta +import logging +from pydroid_ipcam import PyDroidIPCam import voluptuous as vol -from homeassistant.core import callback +from homeassistant.components.mjpeg.camera import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL from homeassistant.const import ( - CONF_NAME, CONF_HOST, - CONF_PORT, - CONF_USERNAME, + CONF_NAME, CONF_PASSWORD, + CONF_PLATFORM, + CONF_PORT, + CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SWITCHES, CONF_TIMEOUT, - CONF_SCAN_INTERVAL, - CONF_PLATFORM, + CONF_USERNAME, ) -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.core import callback from homeassistant.helpers import discovery +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow -from homeassistant.components.mjpeg.camera import CONF_MJPEG_URL, CONF_STILL_IMAGE_URL _LOGGER = logging.getLogger(__name__) @@ -187,7 +188,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the IP Webcam component.""" - from pydroid_ipcam import PyDroidIPCam webcams = hass.data[DATA_IP_WEBCAM] = {} websession = async_get_clientsession(hass) From e992cfb45c65dd0a8f5d6dcd2c1021604a0966f9 Mon Sep 17 00:00:00 2001 From: tombbo <53979375+tombbo@users.noreply.github.com> Date: Thu, 17 Oct 2019 21:07:09 +0200 Subject: [PATCH 356/639] Add on_off_inverted to KNX climate (#25900) * Added a new configuration boolean parameter on_off_inverted to KNX Climate component. * Remove unexpected spaces around equals. * Parameter name changed to on_off_invert and modified to new version of XKNX library. * Dict[key] for required config keys and keys with default config schema values. --- homeassistant/components/knx/climate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 07aac11b972..014cd8d9ba1 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -44,6 +44,7 @@ CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address" CONF_OPERATION_MODES = "operation_modes" CONF_ON_OFF_ADDRESS = "on_off_address" CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address" +CONF_ON_OFF_INVERT = "on_off_invert" CONF_MIN_TEMP = "min_temp" CONF_MAX_TEMP = "max_temp" @@ -51,6 +52,7 @@ DEFAULT_NAME = "KNX Climate" DEFAULT_SETPOINT_SHIFT_STEP = 0.5 DEFAULT_SETPOINT_SHIFT_MAX = 6 DEFAULT_SETPOINT_SHIFT_MIN = -6 +DEFAULT_ON_OFF_INVERT = False # Map KNX operation modes to HA modes. This list might not be full. OPERATION_MODES = { # Map DPT 201.105 HVAC control modes @@ -102,6 +104,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_OPERATION_MODE_COMFORT_ADDRESS): cv.string, vol.Optional(CONF_ON_OFF_ADDRESS): cv.string, vol.Optional(CONF_ON_OFF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT): cv.boolean, vol.Optional(CONF_OPERATION_MODES): vol.All( cv.ensure_list, [vol.In(OPERATION_MODES)] ), @@ -182,6 +185,7 @@ def async_add_entities_config(hass, config, async_add_entities): min_temp=config.get(CONF_MIN_TEMP), max_temp=config.get(CONF_MAX_TEMP), mode=climate_mode, + on_off_invert=config[CONF_ON_OFF_INVERT], ) hass.data[DATA_KNX].xknx.devices.add(climate) From 7637ceb880614558c3f19850fb7be2779c166688 Mon Sep 17 00:00:00 2001 From: Quentame Date: Thu, 17 Oct 2019 21:17:23 +0200 Subject: [PATCH 357/639] Move imports in html5 component (#27473) * Move imports in html5 component * Fix tests 1 * Fix tests 2 --- homeassistant/components/html5/notify.py | 8 +++----- tests/components/html5/test_notify.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/html5/notify.py b/homeassistant/components/html5/notify.py index a802609ac85..18b7ff27ab4 100644 --- a/homeassistant/components/html5/notify.py +++ b/homeassistant/components/html5/notify.py @@ -9,6 +9,9 @@ import time import uuid from aiohttp.hdrs import AUTHORIZATION +import jwt +from pywebpush import WebPusher +from py_vapid import Vapid import voluptuous as vol from voluptuous.humanize import humanize_error @@ -311,7 +314,6 @@ class HTML5PushCallbackView(HomeAssistantView): def decode_jwt(self, token): """Find the registration that signed this JWT and return it.""" - import jwt # 1. Check claims w/o verifying to see if a target is in there. # 2. If target in claims, attempt to verify against the given name. @@ -335,7 +337,6 @@ class HTML5PushCallbackView(HomeAssistantView): # https://auth0.com/docs/quickstart/backend/python def check_authorization_header(self, request): """Check the authorization header.""" - import jwt auth = request.headers.get(AUTHORIZATION, None) if not auth: @@ -491,7 +492,6 @@ class HTML5NotificationService(BaseNotificationService): def _push_message(self, payload, **kwargs): """Send the message.""" - from pywebpush import WebPusher timestamp = int(time.time()) ttl = int(kwargs.get(ATTR_TTL, DEFAULT_TTL)) @@ -550,7 +550,6 @@ class HTML5NotificationService(BaseNotificationService): def add_jwt(timestamp, target, tag, jwt_secret): """Create JWT json to put into payload.""" - import jwt jwt_exp = datetime.fromtimestamp(timestamp) + timedelta(days=JWT_VALID_DAYS) jwt_claims = { @@ -565,7 +564,6 @@ def add_jwt(timestamp, target, tag, jwt_secret): def create_vapid_headers(vapid_email, subscription_info, vapid_private_key): """Create encrypted headers to send to WebPusher.""" - from py_vapid import Vapid if vapid_email and vapid_private_key and ATTR_ENDPOINT in subscription_info: url = urlparse(subscription_info.get(ATTR_ENDPOINT)) diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index d9246e685dc..481d7a010c9 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -87,7 +87,7 @@ class TestHtml5Notify: assert service is not None - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_dismissing_message(self, mock_wp): """Test dismissing message.""" hass = MagicMock() @@ -115,7 +115,7 @@ class TestHtml5Notify: assert payload["dismiss"] is True assert payload["tag"] == "test" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_sending_message(self, mock_wp): """Test sending message.""" hass = MagicMock() @@ -145,7 +145,7 @@ class TestHtml5Notify: assert payload["body"] == "Hello" assert payload["icon"] == "beer.png" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_gcm_key_include(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -176,7 +176,7 @@ class TestHtml5Notify: assert mock_wp.mock_calls[1][2]["gcm_key"] is not None assert mock_wp.mock_calls[4][2]["gcm_key"] is None - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_key_include(self, mock_wp): """Test if the FCM header is included.""" hass = MagicMock() @@ -201,7 +201,7 @@ class TestHtml5Notify: # Get the keys passed to the WebPusher's send method assert mock_wp.mock_calls[1][2]["headers"]["Authorization"] is not None - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_send_with_unknown_priority(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -226,7 +226,7 @@ class TestHtml5Notify: # Get the keys passed to the WebPusher's send method assert mock_wp.mock_calls[1][2]["headers"]["priority"] == "normal" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_no_targets(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -251,7 +251,7 @@ class TestHtml5Notify: # Get the keys passed to the WebPusher's send method assert mock_wp.mock_calls[1][2]["headers"]["priority"] == "normal" - @patch("pywebpush.WebPusher") + @patch("homeassistant.components.html5.notify.WebPusher") def test_fcm_additional_data(self, mock_wp): """Test if the gcm_key is only included for GCM endpoints.""" hass = MagicMock() @@ -475,7 +475,7 @@ async def test_callback_view_with_jwt(hass, hass_client): registrations = {"device": SUBSCRIPTION_1} client = await mock_client(hass, hass_client, registrations) - with patch("pywebpush.WebPusher") as mock_wp: + with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp: await hass.services.async_call( "notify", "notify", @@ -511,7 +511,7 @@ async def test_send_fcm_without_targets(hass, hass_client): """Test that the notification is send with FCM without targets.""" registrations = {"device": SUBSCRIPTION_5} await mock_client(hass, hass_client, registrations) - with patch("pywebpush.WebPusher") as mock_wp: + with patch("homeassistant.components.html5.notify.WebPusher") as mock_wp: await hass.services.async_call( "notify", "notify", From 1a5b4c105ab2f18abecfffdc9ae4ecd41d4cabbb Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 18 Oct 2019 11:04:27 +1100 Subject: [PATCH 358/639] Move imports in mqtt component (#27835) * move imports to top-level in mqtt server * move imports to top-level in mqtt configflow * move imports to top-level in mqtt init * move imports to top-level in mqtt vacuum * move imports to top-level in mqtt light --- homeassistant/components/mqtt/__init__.py | 47 ++++-------------- homeassistant/components/mqtt/config_flow.py | 3 +- homeassistant/components/mqtt/const.py | 2 + .../components/mqtt/light/__init__.py | 34 ++++--------- homeassistant/components/mqtt/light/schema.py | 12 +++++ .../components/mqtt/light/schema_basic.py | 2 +- .../components/mqtt/light/schema_json.py | 2 +- .../components/mqtt/light/schema_template.py | 2 +- homeassistant/components/mqtt/models.py | 20 ++++++++ homeassistant/components/mqtt/server.py | 10 ++-- homeassistant/components/mqtt/subscription.py | 3 +- .../components/mqtt/vacuum/__init__.py | 48 ++----------------- .../components/mqtt/vacuum/schema.py | 31 ++++++++++++ .../components/mqtt/vacuum/schema_legacy.py | 2 +- .../components/mqtt/vacuum/schema_state.py | 2 +- tests/common.py | 3 +- tests/components/mqtt/test_legacy_vacuum.py | 26 +++++----- tests/components/mqtt/test_server.py | 16 +++++-- tests/components/mqtt/test_state_vacuum.py | 9 ++-- 19 files changed, 130 insertions(+), 144 deletions(-) create mode 100644 homeassistant/components/mqtt/light/schema.py create mode 100644 homeassistant/components/mqtt/models.py create mode 100644 homeassistant/components/mqtt/vacuum/schema.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index e3605cb8664..119c9b520d7 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1,5 +1,6 @@ """Support for MQTT message handling.""" import asyncio +import sys from functools import partial, wraps import inspect from itertools import groupby @@ -15,6 +16,8 @@ from typing import Any, Callable, List, Optional, Union import attr import requests.certs import voluptuous as vol +import paho.mqtt.client as mqtt +from paho.mqtt.matcher import MQTTMatcher from homeassistant import config_entries from homeassistant.components import websocket_api @@ -36,6 +39,7 @@ from homeassistant.exceptions import ( ConfigEntryNotReady, ) from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.loader import bind_hass @@ -50,7 +54,12 @@ from .const import ( DEFAULT_DISCOVERY, CONF_STATE_TOPIC, ATTR_DISCOVERY_HASH, + PROTOCOL_311, + DEFAULT_QOS, ) +from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .models import PublishPayloadType, Message, MessageCallbackType +from .subscription import async_subscribe_topics, async_unsubscribe_topics _LOGGER = logging.getLogger(__name__) @@ -95,11 +104,9 @@ CONF_VIA_DEVICE = "via_device" CONF_DEPRECATED_VIA_HUB = "via_hub" PROTOCOL_31 = "3.1" -PROTOCOL_311 = "3.1.1" DEFAULT_PORT = 1883 DEFAULT_KEEPALIVE = 60 -DEFAULT_QOS = 0 DEFAULT_RETAIN = False DEFAULT_PROTOCOL = PROTOCOL_311 DEFAULT_DISCOVERY_PREFIX = "homeassistant" @@ -329,23 +336,9 @@ MQTT_PUBLISH_SCHEMA = vol.Schema( # pylint: disable=invalid-name -PublishPayloadType = Union[str, bytes, int, float, None] SubscribePayloadType = Union[str, bytes] # Only bytes if encoding is None -@attr.s(slots=True, frozen=True) -class Message: - """MQTT Message.""" - - topic = attr.ib(type=str) - payload = attr.ib(type=PublishPayloadType) - qos = attr.ib(type=int) - retain = attr.ib(type=bool) - - -MessageCallbackType = Callable[[Message], None] - - def _build_publish_data(topic: Any, qos: int, retain: bool) -> ServiceDataType: """Build the arguments for the publish service without the payload.""" data = {ATTR_TOPIC: topic} @@ -629,8 +622,6 @@ async def async_setup_entry(hass, entry): elif conf_tls_version == "1.0": tls_version = ssl.PROTOCOL_TLSv1 else: - import sys - # Python3.6 supports automatic negotiation of highest TLS version if sys.hexversion >= 0x03060000: tls_version = ssl.PROTOCOL_TLS # pylint: disable=no-member @@ -735,8 +726,6 @@ class MQTT: tls_version: Optional[int], ) -> None: """Initialize Home Assistant MQTT client.""" - import paho.mqtt.client as mqtt - self.hass = hass self.broker = broker self.port = port @@ -808,8 +797,6 @@ class MQTT: return CONNECTION_FAILED_RECOVERABLE if result != 0: - import paho.mqtt.client as mqtt - _LOGGER.error("Failed to connect: %s", mqtt.error_string(result)) return CONNECTION_FAILED @@ -891,8 +878,6 @@ class MQTT: Resubscribe to all topics we were subscribed to and publish birth message. """ - import paho.mqtt.client as mqtt - if result_code != mqtt.CONNACK_ACCEPTED: _LOGGER.error( "Unable to connect to the MQTT broker: %s", @@ -984,8 +969,6 @@ class MQTT: def _raise_on_error(result_code: int) -> None: """Raise error if error result.""" if result_code != 0: - import paho.mqtt.client as mqtt - raise HomeAssistantError( "Error talking to MQTT: {}".format(mqtt.error_string(result_code)) ) @@ -993,8 +976,6 @@ def _raise_on_error(result_code: int) -> None: def _match_topic(subscription: str, topic: str) -> bool: """Test if topic matches subscription.""" - from paho.mqtt.matcher import MQTTMatcher - matcher = MQTTMatcher() matcher[subscription] = True try: @@ -1028,8 +1009,6 @@ class MqttAttributes(Entity): async def _attributes_subscribe_topics(self): """(Re)Subscribe to topics.""" - from .subscription import async_subscribe_topics - attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE) if attr_tpl is not None: attr_tpl.hass = self.hass @@ -1065,8 +1044,6 @@ class MqttAttributes(Entity): async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" - from .subscription import async_unsubscribe_topics - self._attributes_sub_state = await async_unsubscribe_topics( self.hass, self._attributes_sub_state ) @@ -1102,7 +1079,6 @@ class MqttAvailability(Entity): async def _availability_subscribe_topics(self): """(Re)Subscribe to topics.""" - from .subscription import async_subscribe_topics @callback def availability_message_received(msg: Message) -> None: @@ -1128,8 +1104,6 @@ class MqttAvailability(Entity): async def async_will_remove_from_hass(self): """Unsubscribe when removed.""" - from .subscription import async_unsubscribe_topics - self._availability_sub_state = await async_unsubscribe_topics( self.hass, self._availability_sub_state ) @@ -1154,9 +1128,6 @@ class MqttDiscoveryUpdate(Entity): """Subscribe to discovery updates.""" await super().async_added_to_hass() - from homeassistant.helpers.dispatcher import async_dispatcher_connect - from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash - @callback def discovery_callback(payload): """Handle discovery update.""" diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index d3c6ee819b5..a8a378e723c 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -3,6 +3,7 @@ from collections import OrderedDict import queue import voluptuous as vol +import paho.mqtt.client as mqtt from homeassistant import config_entries from homeassistant.const import ( @@ -125,8 +126,6 @@ class FlowHandler(config_entries.ConfigFlow): def try_connection(broker, port, username, password, protocol="3.1"): """Test if we can connect to an MQTT broker.""" - import paho.mqtt.client as mqtt - if protocol == "3.1": proto = mqtt.MQTTv31 else: diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index b365ee9d33e..3234bebbfc1 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -5,3 +5,5 @@ DEFAULT_DISCOVERY = False ATTR_DISCOVERY_HASH = "discovery_hash" CONF_STATE_TOPIC = "state_topic" +PROTOCOL_311 = "3.1.1" +DEFAULT_QOS = 0 diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 688cef03467..95a850fb9e8 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -16,34 +16,24 @@ from homeassistant.components.mqtt.discovery import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA +from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic +from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json +from .schema_template import PLATFORM_SCHEMA_TEMPLATE, async_setup_entity_template _LOGGER = logging.getLogger(__name__) -CONF_SCHEMA = "schema" - def validate_mqtt_light(value): """Validate MQTT light schema.""" - from . import schema_basic - from . import schema_json - from . import schema_template - schemas = { - "basic": schema_basic.PLATFORM_SCHEMA_BASIC, - "json": schema_json.PLATFORM_SCHEMA_JSON, - "template": schema_template.PLATFORM_SCHEMA_TEMPLATE, + "basic": PLATFORM_SCHEMA_BASIC, + "json": PLATFORM_SCHEMA_JSON, + "template": PLATFORM_SCHEMA_TEMPLATE, } return schemas[value[CONF_SCHEMA]](value) -MQTT_LIGHT_SCHEMA_SCHEMA = vol.Schema( - { - vol.Optional(CONF_SCHEMA, default="basic"): vol.All( - vol.Lower, vol.Any("basic", "json", "template") - ) - } -) - PLATFORM_SCHEMA = vol.All( MQTT_LIGHT_SCHEMA_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_light ) @@ -81,14 +71,10 @@ async def _async_setup_entity( config, async_add_entities, config_entry=None, discovery_hash=None ): """Set up a MQTT Light.""" - from . import schema_basic - from . import schema_json - from . import schema_template - setup_entity = { - "basic": schema_basic.async_setup_entity_basic, - "json": schema_json.async_setup_entity_json, - "template": schema_template.async_setup_entity_template, + "basic": async_setup_entity_basic, + "json": async_setup_entity_json, + "template": async_setup_entity_template, } await setup_entity[config[CONF_SCHEMA]]( config, async_add_entities, config_entry, discovery_hash diff --git a/homeassistant/components/mqtt/light/schema.py b/homeassistant/components/mqtt/light/schema.py new file mode 100644 index 00000000000..a7ab5e986a7 --- /dev/null +++ b/homeassistant/components/mqtt/light/schema.py @@ -0,0 +1,12 @@ +"""Shared schema code.""" +import voluptuous as vol + +CONF_SCHEMA = "schema" + +MQTT_LIGHT_SCHEMA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SCHEMA, default="basic"): vol.All( + vol.Lower, vol.Any("basic", "json", "template") + ) + } +) diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 216762f9b2b..829809dd9c3 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -56,7 +56,7 @@ from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -from . import MQTT_LIGHT_SCHEMA_SCHEMA +from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 1e8114a48e6..c4de1edbc3c 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -59,7 +59,7 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util -from . import MQTT_LIGHT_SCHEMA_SCHEMA +from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 410eff6143f..c80ab2f95a7 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -49,7 +49,7 @@ import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util from homeassistant.helpers.restore_state import RestoreEntity -from . import MQTT_LIGHT_SCHEMA_SCHEMA +from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py new file mode 100644 index 00000000000..5f014aadd08 --- /dev/null +++ b/homeassistant/components/mqtt/models.py @@ -0,0 +1,20 @@ +"""Modesl used by multiple MQTT modules.""" +from typing import Union, Callable + +import attr + +# pylint: disable=invalid-name +PublishPayloadType = Union[str, bytes, int, float, None] + + +@attr.s(slots=True, frozen=True) +class Message: + """MQTT Message.""" + + topic = attr.ib(type=str) + payload = attr.ib(type=PublishPayloadType) + qos = attr.ib(type=int) + retain = attr.ib(type=bool) + + +MessageCallbackType = Callable[[Message], None] diff --git a/homeassistant/components/mqtt/server.py b/homeassistant/components/mqtt/server.py index 2c70d18d772..f5d369a75c7 100644 --- a/homeassistant/components/mqtt/server.py +++ b/homeassistant/components/mqtt/server.py @@ -4,10 +4,14 @@ import logging import tempfile import voluptuous as vol +from hbmqtt.broker import Broker, BrokerException +from passlib.apps import custom_app_context from homeassistant.const import EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv +from .const import PROTOCOL_311 + _LOGGER = logging.getLogger(__name__) # None allows custom config to be created through generate_config @@ -33,8 +37,6 @@ def async_start(hass, password, server_config): This method is a coroutine. """ - from hbmqtt.broker import Broker, BrokerException - passwd = tempfile.NamedTemporaryFile() gen_server_config, client_config = generate_config(hass, passwd, password) @@ -63,8 +65,6 @@ def async_start(hass, password, server_config): def generate_config(hass, passwd, password): """Generate a configuration based on current Home Assistant instance.""" - from . import PROTOCOL_311 - config = { "listeners": { "default": { @@ -83,8 +83,6 @@ def generate_config(hass, passwd, password): username = "homeassistant" # Encrypt with what hbmqtt uses to verify - from passlib.apps import custom_app_context - passwd.write( "homeassistant:{}\n".format(custom_app_context.encrypt(password)).encode( "utf-8" diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index d85399b5dcb..be48a769a23 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -8,7 +8,8 @@ from homeassistant.components import mqtt from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass -from . import DEFAULT_QOS, MessageCallbackType +from .const import DEFAULT_QOS +from .models import MessageCallbackType _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index 5fdaa744ca9..12fd4c51693 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -15,51 +15,19 @@ from homeassistant.components.mqtt.discovery import ( clear_discovery_hash, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect +from .schema import CONF_SCHEMA, LEGACY, STATE, MQTT_VACUUM_SCHEMA +from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy +from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state _LOGGER = logging.getLogger(__name__) -CONF_SCHEMA = "schema" -LEGACY = "legacy" -STATE = "state" - def validate_mqtt_vacuum(value): """Validate MQTT vacuum schema.""" - from . import schema_legacy - from . import schema_state - - schemas = { - LEGACY: schema_legacy.PLATFORM_SCHEMA_LEGACY, - STATE: schema_state.PLATFORM_SCHEMA_STATE, - } + schemas = {LEGACY: PLATFORM_SCHEMA_LEGACY, STATE: PLATFORM_SCHEMA_STATE} return schemas[value[CONF_SCHEMA]](value) -def services_to_strings(services, service_to_string): - """Convert SUPPORT_* service bitmask to list of service strings.""" - strings = [] - for service in service_to_string: - if service & services: - strings.append(service_to_string[service]) - return strings - - -def strings_to_services(strings, string_to_service): - """Convert service strings to SUPPORT_* service bitmask.""" - services = 0 - for string in strings: - services |= string_to_service[string] - return services - - -MQTT_VACUUM_SCHEMA = vol.Schema( - { - vol.Optional(CONF_SCHEMA, default=LEGACY): vol.All( - vol.Lower, vol.Any(LEGACY, STATE) - ) - } -) - PLATFORM_SCHEMA = vol.All( MQTT_VACUUM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_mqtt_vacuum ) @@ -95,13 +63,7 @@ async def _async_setup_entity( config, async_add_entities, config_entry, discovery_hash=None ): """Set up the MQTT vacuum.""" - from . import schema_legacy - from . import schema_state - - setup_entity = { - LEGACY: schema_legacy.async_setup_entity_legacy, - STATE: schema_state.async_setup_entity_state, - } + setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state} await setup_entity[config[CONF_SCHEMA]]( config, async_add_entities, config_entry, discovery_hash ) diff --git a/homeassistant/components/mqtt/vacuum/schema.py b/homeassistant/components/mqtt/vacuum/schema.py new file mode 100644 index 00000000000..949b5cede9c --- /dev/null +++ b/homeassistant/components/mqtt/vacuum/schema.py @@ -0,0 +1,31 @@ +"""Shared schema code.""" +import voluptuous as vol + +CONF_SCHEMA = "schema" +LEGACY = "legacy" +STATE = "state" + +MQTT_VACUUM_SCHEMA = vol.Schema( + { + vol.Optional(CONF_SCHEMA, default=LEGACY): vol.All( + vol.Lower, vol.Any(LEGACY, STATE) + ) + } +) + + +def services_to_strings(services, service_to_string): + """Convert SUPPORT_* service bitmask to list of service strings.""" + strings = [] + for service in service_to_string: + if service & services: + strings.append(service_to_string[service]) + return strings + + +def strings_to_services(strings, string_to_service): + """Convert service strings to SUPPORT_* service bitmask.""" + services = 0 + for string in strings: + services |= string_to_service[string] + return services diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index f2fa8f8da66..d770cfbb7f8 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -33,7 +33,7 @@ from homeassistant.components.mqtt import ( subscription, ) -from . import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services +from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 1ab415aef7b..40b3eeb752c 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -46,7 +46,7 @@ from homeassistant.components.mqtt import ( CONF_QOS, ) -from . import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services +from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) diff --git a/tests/common.py b/tests/common.py index 40e02842146..5532e6ccb5c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -27,6 +27,7 @@ from homeassistant.auth import ( ) from homeassistant.auth.permissions import system_policies from homeassistant.components import mqtt, recorder +from homeassistant.components.mqtt.models import Message from homeassistant.config import async_process_component_config from homeassistant.const import ( ATTR_DISCOVERED, @@ -271,7 +272,7 @@ def async_fire_mqtt_message(hass, topic, payload, qos=0, retain=False): """Fire the MQTT message.""" if isinstance(payload, str): payload = payload.encode("utf-8") - msg = mqtt.Message(topic, payload, qos, retain) + msg = Message(topic, payload, qos, retain) hass.data["mqtt"]._mqtt_handle_message(msg) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index add27bebdeb..c35740407c7 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -5,10 +5,8 @@ import json from homeassistant.components import mqtt, vacuum from homeassistant.components.mqtt import CONF_COMMAND_TOPIC from homeassistant.components.mqtt.discovery import async_start -from homeassistant.components.mqtt.vacuum import ( - schema_legacy as mqttvacuum, - services_to_strings, -) +from homeassistant.components.mqtt.vacuum import schema_legacy as mqttvacuum +from homeassistant.components.mqtt.vacuum.schema import services_to_strings from homeassistant.components.mqtt.vacuum.schema_legacy import ( ALL_SERVICES, SERVICE_TO_STRING, @@ -80,7 +78,7 @@ async def test_default_supported_features(hass, mqtt_mock): async def test_all_commands(hass, mqtt_mock): """Test simple commands to the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -221,7 +219,7 @@ async def test_attributes_without_supported_features(hass, mqtt_mock): async def test_status(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -260,7 +258,7 @@ async def test_status(hass, mqtt_mock): async def test_status_battery(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -277,7 +275,7 @@ async def test_status_battery(hass, mqtt_mock): async def test_status_cleaning(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -294,7 +292,7 @@ async def test_status_cleaning(hass, mqtt_mock): async def test_status_docked(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -311,7 +309,7 @@ async def test_status_docked(hass, mqtt_mock): async def test_status_charging(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -328,7 +326,7 @@ async def test_status_charging(hass, mqtt_mock): async def test_status_fan_speed(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -345,7 +343,7 @@ async def test_status_fan_speed(hass, mqtt_mock): async def test_status_error(hass, mqtt_mock): """Test status updates from the vacuum.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) @@ -371,7 +369,7 @@ async def test_battery_template(hass, mqtt_mock): config = deepcopy(DEFAULT_CONFIG) config.update( { - mqttvacuum.CONF_SUPPORTED_FEATURES: mqttvacuum.services_to_strings( + mqttvacuum.CONF_SUPPORTED_FEATURES: services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ), mqttvacuum.CONF_BATTERY_LEVEL_TOPIC: "retroroomba/battery_level", @@ -390,7 +388,7 @@ async def test_battery_template(hass, mqtt_mock): async def test_status_invalid_json(hass, mqtt_mock): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( ALL_SERVICES, SERVICE_TO_STRING ) diff --git a/tests/components/mqtt/test_server.py b/tests/components/mqtt/test_server.py index 3627c95040e..71dff7ef3ac 100644 --- a/tests/components/mqtt/test_server.py +++ b/tests/components/mqtt/test_server.py @@ -19,9 +19,13 @@ class TestMQTT: """Stop everything that was started.""" self.hass.stop() - @patch("passlib.apps.custom_app_context", Mock(return_value="")) + @patch( + "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") + ) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) + @patch( + "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock()) + ) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_no_http_pass(self, mock_mqtt): @@ -41,9 +45,13 @@ class TestMQTT: assert mock_mqtt.mock_calls[1][2]["username"] == "homeassistant" assert mock_mqtt.mock_calls[1][2]["password"] == password - @patch("passlib.apps.custom_app_context", Mock(return_value="")) + @patch( + "homeassistant.components.mqtt.server.custom_app_context", Mock(return_value="") + ) @patch("tempfile.NamedTemporaryFile", Mock(return_value=MagicMock())) - @patch("hbmqtt.broker.Broker", Mock(return_value=MagicMock())) + @patch( + "homeassistant.components.mqtt.server.Broker", Mock(return_value=MagicMock()) + ) @patch("hbmqtt.broker.Broker.start", Mock(return_value=mock_coro())) @patch("homeassistant.components.mqtt.MQTT") def test_creating_config_with_pass_and_http_pass(self, mock_mqtt): diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index fe100bdcb6e..572c3b05752 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -5,11 +5,8 @@ import json from homeassistant.components import mqtt, vacuum from homeassistant.components.mqtt import CONF_COMMAND_TOPIC, CONF_STATE_TOPIC from homeassistant.components.mqtt.discovery import async_start -from homeassistant.components.mqtt.vacuum import ( - CONF_SCHEMA, - schema_state as mqttvacuum, - services_to_strings, -) +from homeassistant.components.mqtt.vacuum import CONF_SCHEMA, schema_state as mqttvacuum +from homeassistant.components.mqtt.vacuum.schema import services_to_strings from homeassistant.components.mqtt.vacuum.schema_state import SERVICE_TO_STRING from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, @@ -259,7 +256,7 @@ async def test_no_fan_vacuum(hass, mqtt_mock): async def test_status_invalid_json(hass, mqtt_mock): """Test to make sure nothing breaks if the vacuum sends bad JSON.""" config = deepcopy(DEFAULT_CONFIG) - config[mqttvacuum.CONF_SUPPORTED_FEATURES] = mqttvacuum.services_to_strings( + config[mqttvacuum.CONF_SUPPORTED_FEATURES] = services_to_strings( mqttvacuum.ALL_SERVICES, SERVICE_TO_STRING ) From bb80d9bd169f69b0b73e3bf9829532759c503834 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:06:41 +0200 Subject: [PATCH 359/639] Move imports in august component (#27810) --- homeassistant/components/august/__init__.py | 15 ++++++--------- homeassistant/components/august/binary_sensor.py | 9 +++------ homeassistant/components/august/lock.py | 6 +++--- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 93b5ec6ec78..468e6e429a7 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -1,18 +1,20 @@ """Support for August devices.""" -import logging from datetime import timedelta +import logging +from august.api import Api +from august.authenticator import AuthenticationState, Authenticator, ValidationResult +from requests import RequestException, Session import voluptuous as vol -from requests import RequestException -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_PASSWORD, - CONF_USERNAME, CONF_TIMEOUT, + CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -62,7 +64,6 @@ def request_configuration(hass, config, api, authenticator): def august_configuration_callback(data): """Run when the configuration callback is called.""" - from august.authenticator import ValidationResult result = authenticator.validate_verification_code(data.get("verification_code")) @@ -94,7 +95,6 @@ def request_configuration(hass, config, api, authenticator): def setup_august(hass, config, api, authenticator): """Set up the August component.""" - from august.authenticator import AuthenticationState authentication = None try: @@ -134,9 +134,6 @@ def setup_august(hass, config, api, authenticator): def setup(hass, config): """Set up the August component.""" - from august.api import Api - from august.authenticator import Authenticator - from requests import Session conf = config[DOMAIN] api_http_session = None diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index d68582d30c5..14d03189c92 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -2,6 +2,9 @@ from datetime import datetime, timedelta import logging +from august.activity import ActivityType +from august.lock import LockDoorStatus + from homeassistant.components.binary_sensor import BinarySensorDevice from . import DATA_AUGUST @@ -26,7 +29,6 @@ def _retrieve_online_state(data, doorbell): def _retrieve_motion_state(data, doorbell): - from august.activity import ActivityType return _activity_time_based_state( data, doorbell, [ActivityType.DOORBELL_MOTION, ActivityType.DOORBELL_DING] @@ -34,7 +36,6 @@ def _retrieve_motion_state(data, doorbell): def _retrieve_ding_state(data, doorbell): - from august.activity import ActivityType return _activity_time_based_state(data, doorbell, [ActivityType.DOORBELL_DING]) @@ -65,8 +66,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): data = hass.data[DATA_AUGUST] devices = [] - from august.lock import LockDoorStatus - for door in data.locks: for sensor_type in SENSOR_TYPES_DOOR: state_provider = SENSOR_TYPES_DOOR[sensor_type][2] @@ -136,8 +135,6 @@ class AugustDoorBinarySensor(BinarySensorDevice): self._state = state_provider(self._data, self._door) self._available = self._state is not None - from august.lock import LockDoorStatus - self._state = self._state == LockDoorStatus.OPEN @property diff --git a/homeassistant/components/august/lock.py b/homeassistant/components/august/lock.py index 8b8c019eb2d..a541be67097 100644 --- a/homeassistant/components/august/lock.py +++ b/homeassistant/components/august/lock.py @@ -2,6 +2,9 @@ from datetime import timedelta import logging +from august.activity import ActivityType +from august.lock import LockStatus + from homeassistant.components.lock import LockDevice from homeassistant.const import ATTR_BATTERY_LEVEL @@ -51,8 +54,6 @@ class AugustLock(LockDevice): self._lock_detail = self._data.get_lock_detail(self._lock.device_id) - from august.activity import ActivityType - activity = self._data.get_latest_device_activity( self._lock.device_id, ActivityType.LOCK_OPERATION ) @@ -73,7 +74,6 @@ class AugustLock(LockDevice): @property def is_locked(self): """Return true if device is on.""" - from august.lock import LockStatus return self._lock_status is LockStatus.LOCKED From 3cf7983e00231f74fb5c030e0f4d9e28c7f6e0d1 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:08:58 +0200 Subject: [PATCH 360/639] Move imports in asterisk_mbox component (#27807) --- homeassistant/components/asterisk_mbox/__init__.py | 12 ++++++------ homeassistant/components/asterisk_mbox/mailbox.py | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/asterisk_mbox/__init__.py b/homeassistant/components/asterisk_mbox/__init__.py index 6c9412d07d8..1ecba9f4c8f 100644 --- a/homeassistant/components/asterisk_mbox/__init__.py +++ b/homeassistant/components/asterisk_mbox/__init__.py @@ -1,6 +1,12 @@ """Support for Asterisk Voicemail interface.""" import logging +from asterisk_mbox import Client as asteriskClient +from asterisk_mbox.commands import ( + CMD_MESSAGE_CDR, + CMD_MESSAGE_CDR_AVAILABLE, + CMD_MESSAGE_LIST, +) import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT @@ -51,7 +57,6 @@ class AsteriskData: def __init__(self, hass, host, port, password, config): """Init the Asterisk data object.""" - from asterisk_mbox import Client as asteriskClient self.hass = hass self.config = config @@ -76,11 +81,6 @@ class AsteriskData: @callback def handle_data(self, command, msg): """Handle changes to the mailbox.""" - from asterisk_mbox.commands import ( - CMD_MESSAGE_LIST, - CMD_MESSAGE_CDR_AVAILABLE, - CMD_MESSAGE_CDR, - ) if command == CMD_MESSAGE_LIST: _LOGGER.debug("AsteriskVM sent updated message list: Len %d", len(msg)) diff --git a/homeassistant/components/asterisk_mbox/mailbox.py b/homeassistant/components/asterisk_mbox/mailbox.py index 4d3c255fd5b..3cd6fe059b6 100644 --- a/homeassistant/components/asterisk_mbox/mailbox.py +++ b/homeassistant/components/asterisk_mbox/mailbox.py @@ -1,6 +1,8 @@ """Support for the Asterisk Voicemail interface.""" import logging +from asterisk_mbox import ServerError + from homeassistant.components.mailbox import CONTENT_TYPE_MPEG, Mailbox, StreamError from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -50,7 +52,6 @@ class AsteriskMailbox(Mailbox): async def async_get_media(self, msgid): """Return the media blob for the msgid.""" - from asterisk_mbox import ServerError client = self.hass.data[ASTERISK_DOMAIN].client try: From 5eb781d3785b140cfbee0c0eaa93a72536876001 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:09:47 +0200 Subject: [PATCH 361/639] Move imports in arlo component (#27806) --- homeassistant/components/arlo/__init__.py | 10 +++++----- homeassistant/components/arlo/camera.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/arlo/__init__.py b/homeassistant/components/arlo/__init__.py index 80fa37b6787..df24bdd1a92 100644 --- a/homeassistant/components/arlo/__init__.py +++ b/homeassistant/components/arlo/__init__.py @@ -1,14 +1,15 @@ """Support for Netgear Arlo IP cameras.""" -import logging from datetime import timedelta +import logging +from pyarlo import PyArlo +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from requests.exceptions import HTTPError, ConnectTimeout +from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.helpers import config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL -from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import track_time_interval _LOGGER = logging.getLogger(__name__) @@ -47,7 +48,6 @@ def setup(hass, config): scan_interval = conf.get(CONF_SCAN_INTERVAL) try: - from pyarlo import PyArlo arlo = PyArlo(username, password, preload=False) if not arlo.is_connected: diff --git a/homeassistant/components/arlo/camera.py b/homeassistant/components/arlo/camera.py index a05dc40a9ef..958c383765a 100644 --- a/homeassistant/components/arlo/camera.py +++ b/homeassistant/components/arlo/camera.py @@ -1,6 +1,7 @@ """Support for Netgear Arlo IP cameras.""" import logging +from haffmpeg.camera import CameraMjpeg import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -77,7 +78,6 @@ class ArloCam(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg video = self._camera.last_video if not video: From 56c13503c35e1e6eed46ab6671e60a4520bb58c8 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:10:16 +0200 Subject: [PATCH 362/639] Move imports in aqualogic component (#27805) --- homeassistant/components/aqualogic/__init__.py | 4 ++-- homeassistant/components/aqualogic/switch.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aqualogic/__init__.py b/homeassistant/components/aqualogic/__init__.py index cabe00b6c6d..9f693966382 100644 --- a/homeassistant/components/aqualogic/__init__.py +++ b/homeassistant/components/aqualogic/__init__.py @@ -1,9 +1,10 @@ """Support for AquaLogic devices.""" from datetime import timedelta import logging -import time import threading +import time +from aqualogic.core import AquaLogic import voluptuous as vol from homeassistant.const import ( @@ -71,7 +72,6 @@ class AquaLogicProcessor(threading.Thread): def run(self): """Event thread.""" - from aqualogic.core import AquaLogic while True: self._panel = AquaLogic() diff --git a/homeassistant/components/aqualogic/switch.py b/homeassistant/components/aqualogic/switch.py index b5a7a409647..74f1a9d9f9a 100644 --- a/homeassistant/components/aqualogic/switch.py +++ b/homeassistant/components/aqualogic/switch.py @@ -1,6 +1,7 @@ """Support for AquaLogic switches.""" import logging +from aqualogic.core import States import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -50,7 +51,6 @@ class AquaLogicSwitch(SwitchDevice): def __init__(self, processor, switch_type): """Initialize switch.""" - from aqualogic.core import States self._processor = processor self._type = switch_type From 447d99a1ae99b13fb993bd438d7669516e0c85ae Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:10:28 +0200 Subject: [PATCH 363/639] Move imports in apcupsd component (#27803) --- homeassistant/components/apcupsd/__init__.py | 4 ++-- homeassistant/components/apcupsd/sensor.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 512bd01b72a..71f25f04387 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -1,7 +1,8 @@ """Support for APCUPSd via its Network Information Server (NIS).""" -import logging from datetime import timedelta +import logging +from apcaccess import status import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT @@ -64,7 +65,6 @@ class APCUPSdData: def __init__(self, host, port): """Initialize the data object.""" - from apcaccess import status self._host = host self._port = port diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 837e6e45c6c..255eb1624ff 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -1,12 +1,13 @@ """Support for APCUPSd sensors.""" import logging +from apcaccess.status import ALL_UNITS import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.components import apcupsd -from homeassistant.const import TEMP_CELSIUS, CONF_RESOURCES, POWER_WATT +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_RESOURCES, POWER_WATT, TEMP_CELSIUS +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -135,7 +136,6 @@ def infer_unit(value): Split the unit off the end of the value and return the value, unit tuple pair. Else return the original value and None as the unit. """ - from apcaccess.status import ALL_UNITS for unit in ALL_UNITS: if value.endswith(unit): From 54ef96e79aaeeaacf60a6fffa1c9f11a526770bf Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:11:01 +0200 Subject: [PATCH 364/639] Move imports in awair component (#27811) --- homeassistant/components/awair/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/awair/sensor.py b/homeassistant/components/awair/sensor.py index c899e009796..f15e4a80e36 100644 --- a/homeassistant/components/awair/sensor.py +++ b/homeassistant/components/awair/sensor.py @@ -4,6 +4,7 @@ from datetime import timedelta import logging import math +from python_awair import AwairClient import voluptuous as vol from homeassistant.const import ( @@ -105,7 +106,6 @@ PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( # used at this time is the `uuid` value. async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Connect to the Awair API and find devices.""" - from python_awair import AwairClient token = config[CONF_ACCESS_TOKEN] client = AwairClient(token, session=async_get_clientsession(hass)) From 9d583ad9f98cd3f1c2c6832206c039712c0fe4db Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:11:11 +0200 Subject: [PATCH 365/639] Move imports in baidu component (#27812) --- homeassistant/components/baidu/tts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/baidu/tts.py b/homeassistant/components/baidu/tts.py index 85737d1affd..8d753753e5a 100644 --- a/homeassistant/components/baidu/tts.py +++ b/homeassistant/components/baidu/tts.py @@ -1,6 +1,7 @@ """Support for Baidu speech service.""" import logging +from aip import AipSpeech import voluptuous as vol from homeassistant.components.tts import CONF_LANG, PLATFORM_SCHEMA, Provider @@ -106,7 +107,6 @@ class BaiduTTSProvider(Provider): def get_tts_audio(self, message, language, options=None): """Load TTS from BaiduTTS.""" - from aip import AipSpeech aip_speech = AipSpeech( self._app_data["appid"], From 6998687742943c5e6e2d0f068fb855d657e4cbb8 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:11:20 +0200 Subject: [PATCH 366/639] Move imports in gitlab_ci component (#27827) --- homeassistant/components/gitlab_ci/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gitlab_ci/sensor.py b/homeassistant/components/gitlab_ci/sensor.py index d8055c88f30..9edbe9733a8 100644 --- a/homeassistant/components/gitlab_ci/sensor.py +++ b/homeassistant/components/gitlab_ci/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from gitlab import Gitlab, GitlabAuthenticationError, GitlabGetError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -141,12 +142,10 @@ class GitLabData: def __init__(self, gitlab_id, priv_token, interval, url): """Fetch data from GitLab API for most recent CI job.""" - import gitlab self._gitlab_id = gitlab_id - self._gitlab = gitlab.Gitlab(url, private_token=priv_token, per_page=1) + self._gitlab = Gitlab(url, private_token=priv_token, per_page=1) self._gitlab.auth() - self._gitlab_exceptions = gitlab.exceptions self.update = Throttle(interval)(self._update) self.available = False @@ -174,9 +173,9 @@ class GitLabData: self.build_id = _last_job.attributes.get("id") self.branch = _last_job.attributes.get("ref") self.available = True - except self._gitlab_exceptions.GitlabAuthenticationError as erra: + except GitlabAuthenticationError as erra: _LOGGER.error("Authentication Error: %s", erra) self.available = False - except self._gitlab_exceptions.GitlabGetError as errg: + except GitlabGetError as errg: _LOGGER.error("Project Not Found: %s", errg) self.available = False From 61edd33da74d75df3e7cd8c26c685be7db8d06c8 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:11:51 +0200 Subject: [PATCH 367/639] Move imports in google component (#27826) --- homeassistant/components/google/__init__.py | 20 +++++++++----------- homeassistant/components/google/calendar.py | 5 ++--- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 62aa2212bb1..9cb9be0fa4f 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -4,6 +4,15 @@ import logging import os import yaml +import httplib2 +from oauth2client.client import ( + OAuth2WebServerFlow, + OAuth2DeviceCodeError, + FlowExchangeError, +) +from oauth2client.file import Storage +from googleapiclient import discovery as google_discovery + import voluptuous as vol from voluptuous.error import Error as VoluptuousError @@ -126,13 +135,6 @@ def do_authentication(hass, hass_config, config): Notify user of user_code and verification_url then poll until we have an access token. """ - from oauth2client.client import ( - OAuth2WebServerFlow, - OAuth2DeviceCodeError, - FlowExchangeError, - ) - from oauth2client.file import Storage - oauth = OAuth2WebServerFlow( client_id=config[CONF_CLIENT_ID], client_secret=config[CONF_CLIENT_SECRET], @@ -341,10 +343,6 @@ class GoogleCalendarService: def get(self): """Get the calendar service from the storage file token.""" - import httplib2 - from oauth2client.file import Storage - from googleapiclient import discovery as google_discovery - credentials = Storage(self.token_file).get() http = credentials.authorize(httplib2.Http()) service = google_discovery.build( diff --git a/homeassistant/components/google/calendar.py b/homeassistant/components/google/calendar.py index 31e9f186a4e..8a6eb644621 100644 --- a/homeassistant/components/google/calendar.py +++ b/homeassistant/components/google/calendar.py @@ -3,6 +3,8 @@ import copy from datetime import timedelta import logging +from httplib2 import ServerNotFoundError # pylint: disable=import-error + from homeassistant.components.calendar import ( ENTITY_ID_FORMAT, CalendarEventDevice, @@ -126,9 +128,6 @@ class GoogleCalendarData: self.event = None def _prepare_query(self): - # pylint: disable=import-error - from httplib2 import ServerNotFoundError - try: service = self.calendar_service.get() except ServerNotFoundError: From 3a608314c491ef43d1e0a15ddcb0d4fd9556df41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 18 Oct 2019 03:12:58 +0300 Subject: [PATCH 368/639] Mypy setup fixes (#27825) * Install our core dependencies for mypy in azure To match local setups and tox. * Use "system" mypy in pre-commit instead of the "real" mypy hook The results of mypy depend on what is installed. And the mypy hook runs in a virtualenv of its own, meaning we'd need to install and maintain another set of our dependencies there... no. Use the "system" one and reuse the environment that is set up anyway already instead. * Reintroduce needed ruamel.yaml type ignore This ignore is required when ruamel.yaml is installed, and we want it to be as it's part of the core dependency set. --- .pre-commit-config.yaml | 14 +++++++++++--- azure-pipelines-ci.yml | 4 ++-- homeassistant/util/ruamel_yaml.py | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 48d77cfdc6f..3773a3213aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,9 +13,17 @@ repos: additional_dependencies: - flake8-docstrings==1.3.1 - pydocstyle==4.0.0 -- repo: https://github.com/pre-commit/mirrors-mypy.git - rev: v0.730 +# Using a local "system" mypy instead of the mypy hook, because its +# results depend on what is installed. And the mypy hook runs in a +# virtualenv of its own, meaning we'd need to install and maintain +# another set of our dependencies there... no. Use the "system" one +# and reuse the environment that is set up anyway already instead. +- repo: local hooks: - id: mypy - args: [] + name: mypy + entry: mypy + language: system + types: [python] + require_serial: true exclude: ^script/scaffold/templates/ diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index a566baf6561..257eac57c2d 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -174,12 +174,12 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_test.txt | homeassistant/package_constraints.txt' + keyfile: 'requirements_test.txt | setup.py | homeassistant/package_constraints.txt' build: | python -m venv venv . venv/bin/activate - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate mypy homeassistant diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index 6793784abae..b7e8927888c 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -90,7 +90,7 @@ def load_yaml(fname: str, round_trip: bool = False) -> JSON_TYPE: if round_trip: yaml = YAML(typ="rt") # type ignore: https://bitbucket.org/ruamel/yaml/pull-requests/42 - yaml.preserve_quotes = True + yaml.preserve_quotes = True # type: ignore else: if ExtSafeConstructor.name is None: ExtSafeConstructor.name = fname From fe036ed0940c5be3ee901ba060ca8b40a01fd324 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:13:20 +0200 Subject: [PATCH 369/639] Move imports in flic component (#27821) --- .../components/flic/binary_sensor.py | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/flic/binary_sensor.py b/homeassistant/components/flic/binary_sensor.py index 4fa97334889..416d39e5332 100644 --- a/homeassistant/components/flic/binary_sensor.py +++ b/homeassistant/components/flic/binary_sensor.py @@ -2,6 +2,14 @@ import logging import threading +from pyflic import ( + FlicClient, + ButtonConnectionChannel, + ClickType, + ConnectionStatus, + ScanWizard, + ScanWizardResult, +) import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -49,7 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the flic platform.""" - import pyflic # Initialize flic client responsible for # connecting to buttons and retrieving events @@ -58,7 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): discovery = config.get(CONF_DISCOVERY) try: - client = pyflic.FlicClient(host, port) + client = FlicClient(host, port) except ConnectionRefusedError: _LOGGER.error("Failed to connect to flic server") return @@ -88,15 +95,13 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def start_scanning(config, add_entities, client): """Start a new flic client for scanning and connecting to new buttons.""" - import pyflic - - scan_wizard = pyflic.ScanWizard() + scan_wizard = ScanWizard() def scan_completed_callback(scan_wizard, result, address, name): """Restart scan wizard to constantly check for new buttons.""" - if result == pyflic.ScanWizardResult.WizardSuccess: + if result == ScanWizardResult.WizardSuccess: _LOGGER.info("Found new button %s", address) - elif result != pyflic.ScanWizardResult.WizardFailedTimeout: + elif result != ScanWizardResult.WizardFailedTimeout: _LOGGER.warning( "Failed to connect to button %s. Reason: %s", address, result ) @@ -123,7 +128,6 @@ class FlicButton(BinarySensorDevice): def __init__(self, hass, client, address, timeout, ignored_click_types): """Initialize the flic button.""" - import pyflic self._hass = hass self._address = address @@ -131,10 +135,10 @@ class FlicButton(BinarySensorDevice): self._is_down = False self._ignored_click_types = ignored_click_types or [] self._hass_click_types = { - pyflic.ClickType.ButtonClick: CLICK_TYPE_SINGLE, - pyflic.ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE, - pyflic.ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE, - pyflic.ClickType.ButtonHold: CLICK_TYPE_HOLD, + ClickType.ButtonClick: CLICK_TYPE_SINGLE, + ClickType.ButtonSingleClick: CLICK_TYPE_SINGLE, + ClickType.ButtonDoubleClick: CLICK_TYPE_DOUBLE, + ClickType.ButtonHold: CLICK_TYPE_HOLD, } self._channel = self._create_channel() @@ -142,9 +146,7 @@ class FlicButton(BinarySensorDevice): def _create_channel(self): """Create a new connection channel to the button.""" - import pyflic - - channel = pyflic.ButtonConnectionChannel(self._address) + channel = ButtonConnectionChannel(self._address) channel.on_button_up_or_down = self._on_up_down # If all types of clicks should be ignored, skip registering callbacks @@ -212,12 +214,10 @@ class FlicButton(BinarySensorDevice): def _on_up_down(self, channel, click_type, was_queued, time_diff): """Update device state, if event was not queued.""" - import pyflic - if was_queued and self._queued_event_check(click_type, time_diff): return - self._is_down = click_type == pyflic.ClickType.ButtonDown + self._is_down = click_type == ClickType.ButtonDown self.schedule_update_ha_state() def _on_click(self, channel, click_type, was_queued, time_diff): @@ -243,9 +243,7 @@ class FlicButton(BinarySensorDevice): def _connection_status_changed(self, channel, connection_status, disconnect_reason): """Remove device, if button disconnects.""" - import pyflic - - if connection_status == pyflic.ConnectionStatus.Disconnected: + if connection_status == ConnectionStatus.Disconnected: _LOGGER.warning( "Button (%s) disconnected. Reason: %s", self.address, disconnect_reason ) From 0965e358ea88aa465db93beb5d2d018cd0de61d5 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:14:53 +0200 Subject: [PATCH 370/639] Move imports in fitbit component (#27820) --- homeassistant/components/fitbit/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fitbit/sensor.py b/homeassistant/components/fitbit/sensor.py index 534477d88cf..0d4b8d61e7a 100644 --- a/homeassistant/components/fitbit/sensor.py +++ b/homeassistant/components/fitbit/sensor.py @@ -4,6 +4,9 @@ import logging import datetime import time +from fitbit import Fitbit +from fitbit.api import FitbitOauth2Client +from oauthlib.oauth2.rfc6749.errors import MismatchingStateError, MissingTokenError import voluptuous as vol from homeassistant.core import callback @@ -234,13 +237,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if "fitbit" in _CONFIGURING: hass.components.configurator.request_done(_CONFIGURING.pop("fitbit")) - import fitbit - access_token = config_file.get(ATTR_ACCESS_TOKEN) refresh_token = config_file.get(ATTR_REFRESH_TOKEN) expires_at = config_file.get(ATTR_LAST_SAVED_AT) if None not in (access_token, refresh_token): - authd_client = fitbit.Fitbit( + authd_client = Fitbit( config_file.get(ATTR_CLIENT_ID), config_file.get(ATTR_CLIENT_SECRET), access_token=access_token, @@ -294,7 +295,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(dev, True) else: - oauth = fitbit.api.FitbitOauth2Client( + oauth = FitbitOauth2Client( config_file.get(ATTR_CLIENT_ID), config_file.get(ATTR_CLIENT_SECRET) ) @@ -337,9 +338,6 @@ class FitbitAuthCallbackView(HomeAssistantView): @callback def get(self, request): """Finish OAuth callback request.""" - from oauthlib.oauth2.rfc6749.errors import MismatchingStateError - from oauthlib.oauth2.rfc6749.errors import MissingTokenError - hass = request.app["hass"] data = request.query From 22b904f5e05f3b0ac388f6971955b78a12d03045 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:15:18 +0200 Subject: [PATCH 371/639] Move imports in flux_led component (#27822) --- homeassistant/components/flux_led/light.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/flux_led/light.py b/homeassistant/components/flux_led/light.py index 0a95de783fa..5bd84cd157f 100644 --- a/homeassistant/components/flux_led/light.py +++ b/homeassistant/components/flux_led/light.py @@ -3,6 +3,7 @@ import logging import socket import random +from flux_led import BulbScanner, WifiLedBulb import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_NAME, CONF_PROTOCOL, ATTR_MODE @@ -135,8 +136,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Flux lights.""" - import flux_led - lights = [] light_ips = [] @@ -156,7 +155,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return # Find the bulbs on the LAN - scanner = flux_led.BulbScanner() + scanner = BulbScanner() scanner.scan(timeout=10) for device in scanner.getBulbInfo(): ipaddr = device["ipaddr"] @@ -187,9 +186,8 @@ class FluxLight(Light): def _connect(self): """Connect to Flux light.""" - import flux_led - self._bulb = flux_led.WifiLedBulb(self._ipaddr, timeout=5) + self._bulb = WifiLedBulb(self._ipaddr, timeout=5) if self._protocol: self._bulb.setProtocol(self._protocol) From fdf839774e936f6c052f522315a0fe7d1026f8e4 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:17:24 +0200 Subject: [PATCH 372/639] Move imports in fritz + fritzbox_netmonitor component (#27823) * Move imports in fritz + fritzbox_netmonitor component * Fix PyLint 1 --- homeassistant/components/fritz/device_tracker.py | 5 ++--- homeassistant/components/fritzbox_netmonitor/sensor.py | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index afe0aa3ed02..c2b1e3ab54e 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -1,6 +1,7 @@ """Support for FRITZ!Box routers.""" import logging +from fritzconnection import FritzHosts # pylint: disable=import-error import voluptuous as vol import homeassistant.helpers.config_validation as cv @@ -41,11 +42,9 @@ class FritzBoxScanner(DeviceScanner): self.password = config[CONF_PASSWORD] self.success_init = True - import fritzconnection as fc # pylint: disable=import-error - # Establish a connection to the FRITZ!Box. try: - self.fritz_box = fc.FritzHosts( + self.fritz_box = FritzHosts( address=self.host, user=self.username, password=self.password ) except (ValueError, TypeError): diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py index 9d07e7a8055..92a29e37c51 100644 --- a/homeassistant/components/fritzbox_netmonitor/sensor.py +++ b/homeassistant/components/fritzbox_netmonitor/sensor.py @@ -3,6 +3,10 @@ import logging from datetime import timedelta from requests.exceptions import RequestException +from fritzconnection import FritzStatus # pylint: disable=import-error +from fritzconnection.fritzconnection import ( # pylint: disable=import-error + FritzConnectionException, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -45,15 +49,11 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the FRITZ!Box monitor sensors.""" - # pylint: disable=import-error - import fritzconnection as fc - from fritzconnection.fritzconnection import FritzConnectionException - name = config.get(CONF_NAME) host = config.get(CONF_HOST) try: - fstatus = fc.FritzStatus(address=host) + fstatus = FritzStatus(address=host) except (ValueError, TypeError, FritzConnectionException): fstatus = None From bc58649c2becda4dbc46095c912c57201719c1a5 Mon Sep 17 00:00:00 2001 From: Tomasz Jagusz Date: Fri, 18 Oct 2019 02:17:56 +0200 Subject: [PATCH 373/639] Move imports in MCP23017 component (#27769) * mcp23017 move imports * fix pylint errors --- homeassistant/components/mcp23017/binary_sensor.py | 10 ++++------ homeassistant/components/mcp23017/switch.py | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/mcp23017/binary_sensor.py b/homeassistant/components/mcp23017/binary_sensor.py index c059ad6a1b6..088052c469e 100644 --- a/homeassistant/components/mcp23017/binary_sensor.py +++ b/homeassistant/components/mcp23017/binary_sensor.py @@ -2,6 +2,10 @@ import logging import voluptuous as vol +import board # pylint: disable=import-error +import busio # pylint: disable=import-error +import adafruit_mcp230xx # pylint: disable=import-error +import digitalio # pylint: disable=import-error from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME @@ -37,10 +41,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_devices, discovery_info=None): """Set up the MCP23017 binary sensors.""" - import board - import busio - import adafruit_mcp230xx - pull_mode = config[CONF_PULL_MODE] invert_logic = config[CONF_INVERT_LOGIC] i2c_address = config[CONF_I2C_ADDRESS] @@ -65,8 +65,6 @@ class MCP23017BinarySensor(BinarySensorDevice): def __init__(self, name, pin, pull_mode, invert_logic): """Initialize the MCP23017 binary sensor.""" - import digitalio - self._name = name or DEVICE_DEFAULT_NAME self._pin = pin self._pull_mode = pull_mode diff --git a/homeassistant/components/mcp23017/switch.py b/homeassistant/components/mcp23017/switch.py index 46978319198..399ed17c44b 100644 --- a/homeassistant/components/mcp23017/switch.py +++ b/homeassistant/components/mcp23017/switch.py @@ -2,6 +2,10 @@ import logging import voluptuous as vol +import board # pylint: disable=import-error +import busio # pylint: disable=import-error +import adafruit_mcp230xx # pylint: disable=import-error +import digitalio # pylint: disable=import-error from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import DEVICE_DEFAULT_NAME @@ -31,10 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MCP23017 devices.""" - import board - import busio - import adafruit_mcp230xx - invert_logic = config.get(CONF_INVERT_LOGIC) i2c_address = config.get(CONF_I2C_ADDRESS) @@ -54,8 +54,6 @@ class MCP23017Switch(ToggleEntity): def __init__(self, name, pin, invert_logic): """Initialize the pin.""" - import digitalio - self._name = name or DEVICE_DEFAULT_NAME self._pin = pin self._invert_logic = invert_logic From d95b4a6a0bdd5e6c57c34bc74fda5f07c201d556 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:18:11 +0200 Subject: [PATCH 374/639] Move imports in anel_pwrctrl component (#27798) --- homeassistant/components/anel_pwrctrl/switch.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/anel_pwrctrl/switch.py b/homeassistant/components/anel_pwrctrl/switch.py index 6184465ef16..3c181d7d04b 100644 --- a/homeassistant/components/anel_pwrctrl/switch.py +++ b/homeassistant/components/anel_pwrctrl/switch.py @@ -1,13 +1,14 @@ """Support for ANEL PwrCtrl switches.""" +from datetime import timedelta import logging import socket -from datetime import timedelta +from anel_pwrctrl import DeviceMaster import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -36,8 +37,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port_recv = config.get(CONF_PORT_RECV) port_send = config.get(CONF_PORT_SEND) - from anel_pwrctrl import DeviceMaster - try: master = DeviceMaster( username=username, From 21754fd7ccefa0bc85ea26e0dd40b3ab6ccaadd1 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:18:22 +0200 Subject: [PATCH 375/639] Move imports in bbb_gpio component (#27813) --- homeassistant/components/bbb_gpio/__init__.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/bbb_gpio/__init__.py b/homeassistant/components/bbb_gpio/__init__.py index bfaa2a7c50d..e68633c0688 100644 --- a/homeassistant/components/bbb_gpio/__init__.py +++ b/homeassistant/components/bbb_gpio/__init__.py @@ -1,6 +1,8 @@ """Support for controlling GPIO pins of a Beaglebone Black.""" import logging +from Adafruit_BBIO import GPIO # pylint: disable=import-error + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -11,7 +13,6 @@ DOMAIN = "bbb_gpio" def setup(hass, config): """Set up the BeagleBone Black GPIO component.""" # pylint: disable=import-error - from Adafruit_BBIO import GPIO def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -27,39 +28,29 @@ def setup(hass, config): def setup_output(pin): """Set up a GPIO as output.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.setup(pin, GPIO.OUT) def setup_input(pin, pull_mode): """Set up a GPIO as input.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.setup(pin, GPIO.IN, GPIO.PUD_DOWN if pull_mode == "DOWN" else GPIO.PUD_UP) def write_output(pin, value): """Write a value to a GPIO.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.output(pin, value) def read_input(pin): """Read a value from a GPIO.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO return GPIO.input(pin) is GPIO.HIGH def edge_detect(pin, event_callback, bounce): """Add detection for RISING and FALLING events.""" - # pylint: disable=import-error - from Adafruit_BBIO import GPIO GPIO.add_event_detect(pin, GPIO.BOTH, callback=event_callback, bouncetime=bounce) From e17b8b011a7ef6764453b4992d39b7a341918c18 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 02:18:47 +0200 Subject: [PATCH 376/639] Move imports in bitcoin component (#27814) --- homeassistant/components/bitcoin/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bitcoin/sensor.py b/homeassistant/components/bitcoin/sensor.py index 4d8d5643826..b62bb434e85 100644 --- a/homeassistant/components/bitcoin/sensor.py +++ b/homeassistant/components/bitcoin/sensor.py @@ -1,11 +1,12 @@ """Bitcoin information service that uses blockchain.info.""" -import logging from datetime import timedelta +import logging +from blockchain import exchangerates, statistics import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_DISPLAY_OPTIONS, ATTR_ATTRIBUTION, CONF_CURRENCY +from homeassistant.const import ATTR_ATTRIBUTION, CONF_CURRENCY, CONF_DISPLAY_OPTIONS import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -55,7 +56,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Bitcoin sensors.""" - from blockchain import exchangerates currency = config.get(CONF_CURRENCY) @@ -169,7 +169,6 @@ class BitcoinData: def update(self): """Get the latest data from blockchain.info.""" - from blockchain import statistics, exchangerates self.stats = statistics.get() self.ticker = exchangerates.get_ticker() From 2d1f7932ba4478daf28e837b51c6e21f23ec7511 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 18 Oct 2019 01:19:07 +0100 Subject: [PATCH 377/639] bump client (#27799) --- homeassistant/components/geniushub/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/geniushub/manifest.json b/homeassistant/components/geniushub/manifest.json index 96497388a48..f9e8e6eb4f0 100644 --- a/homeassistant/components/geniushub/manifest.json +++ b/homeassistant/components/geniushub/manifest.json @@ -3,7 +3,7 @@ "name": "Genius Hub", "documentation": "https://www.home-assistant.io/integrations/geniushub", "requirements": [ - "geniushub-client==0.6.26" + "geniushub-client==0.6.28" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/requirements_all.txt b/requirements_all.txt index 6a70451cc12..6b8b1931a4a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -531,7 +531,7 @@ gearbest_parser==1.0.7 geizhals==0.0.9 # homeassistant.components.geniushub -geniushub-client==0.6.26 +geniushub-client==0.6.28 # homeassistant.components.geo_json_events # homeassistant.components.nsw_rural_fire_service_feed From bd0403c65ebd32d26063fce399609073fca3a3bd Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:19:34 +0200 Subject: [PATCH 378/639] Move imports in telegram_bot component (#27785) --- .../components/telegram_bot/__init__.py | 20 ++++++++++--------- .../components/telegram_bot/polling.py | 9 ++++----- .../components/telegram_bot/webhooks.py | 5 +++-- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/telegram_bot/__init__.py b/homeassistant/components/telegram_bot/__init__.py index a36f41edf3b..7acf4985def 100644 --- a/homeassistant/components/telegram_bot/__init__.py +++ b/homeassistant/components/telegram_bot/__init__.py @@ -7,6 +7,16 @@ import logging import requests from requests.auth import HTTPBasicAuth, HTTPDigestAuth +from telegram import ( + Bot, + InlineKeyboardButton, + InlineKeyboardMarkup, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, +) +from telegram.error import TelegramError +from telegram.parsemode import ParseMode +from telegram.utils.request import Request import voluptuous as vol from homeassistant.components.notify import ATTR_DATA, ATTR_MESSAGE, ATTR_TITLE @@ -375,8 +385,6 @@ async def async_setup(hass, config): def initialize_bot(p_config): """Initialize telegram bot with proxy support.""" - from telegram import Bot - from telegram.utils.request import Request api_key = p_config.get(CONF_API_KEY) proxy_url = p_config.get(CONF_PROXY_URL) @@ -396,7 +404,6 @@ class TelegramNotificationService: def __init__(self, hass, bot, allowed_chat_ids, parser): """Initialize the service.""" - from telegram.parsemode import ParseMode self.allowed_chat_ids = allowed_chat_ids self._default_user = self.allowed_chat_ids[0] @@ -457,7 +464,6 @@ class TelegramNotificationService: - a string like: `/cmd1, /cmd2, /cmd3` - or a string like: `text_b1:/cmd1, text_b2:/cmd2` """ - from telegram import InlineKeyboardButton buttons = [] if isinstance(row_keyboard, str): @@ -507,8 +513,6 @@ class TelegramNotificationService: params[ATTR_REPLY_TO_MSGID] = data[ATTR_REPLY_TO_MSGID] # Keyboards: if ATTR_KEYBOARD in data: - from telegram import ReplyKeyboardMarkup, ReplyKeyboardRemove - keys = data.get(ATTR_KEYBOARD) keys = keys if isinstance(keys, list) else [keys] if keys: @@ -517,9 +521,8 @@ class TelegramNotificationService: ) else: params[ATTR_REPLYMARKUP] = ReplyKeyboardRemove(True) - elif ATTR_KEYBOARD_INLINE in data: - from telegram import InlineKeyboardMarkup + elif ATTR_KEYBOARD_INLINE in data: keys = data.get(ATTR_KEYBOARD_INLINE) keys = keys if isinstance(keys, list) else [keys] params[ATTR_REPLYMARKUP] = InlineKeyboardMarkup( @@ -529,7 +532,6 @@ class TelegramNotificationService: def _send_msg(self, func_send, msg_error, *args_msg, **kwargs_msg): """Send one message.""" - from telegram.error import TelegramError try: out = func_send(*args_msg, **kwargs_msg) diff --git a/homeassistant/components/telegram_bot/polling.py b/homeassistant/components/telegram_bot/polling.py index 7ca486e33b2..314cb31a373 100644 --- a/homeassistant/components/telegram_bot/polling.py +++ b/homeassistant/components/telegram_bot/polling.py @@ -1,6 +1,10 @@ """Support for Telegram bot using polling.""" import logging +from telegram import Update +from telegram.error import TelegramError, TimedOut, NetworkError, RetryAfter +from telegram.ext import Updater, Handler + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback @@ -32,8 +36,6 @@ async def async_setup_platform(hass, config): def process_error(bot, update, error): """Telegram bot error handler.""" - from telegram.error import TelegramError, TimedOut, NetworkError, RetryAfter - try: raise error except (TimedOut, NetworkError, RetryAfter): @@ -45,8 +47,6 @@ def process_error(bot, update, error): def message_handler(handler): """Create messages handler.""" - from telegram import Update - from telegram.ext import Handler class MessageHandler(Handler): """Telegram bot message handler.""" @@ -72,7 +72,6 @@ class TelegramPoll(BaseTelegramBotEntity): def __init__(self, bot, hass, allowed_chat_ids): """Initialize the polling instance.""" - from telegram.ext import Updater BaseTelegramBotEntity.__init__(self, hass, allowed_chat_ids) diff --git a/homeassistant/components/telegram_bot/webhooks.py b/homeassistant/components/telegram_bot/webhooks.py index c71510eddd9..16da2e741e4 100644 --- a/homeassistant/components/telegram_bot/webhooks.py +++ b/homeassistant/components/telegram_bot/webhooks.py @@ -2,6 +2,8 @@ import datetime as dt import logging +from telegram.error import TimedOut + from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP from homeassistant.const import ( @@ -26,7 +28,6 @@ REMOVE_HANDLER_URL = "" async def async_setup_platform(hass, config): """Set up the Telegram webhooks platform.""" - import telegram bot = initialize_bot(config) @@ -55,7 +56,7 @@ async def async_setup_platform(hass, config): while retry_num < 3: try: return bot.setWebhook(handler_url, timeout=5) - except telegram.error.TimedOut: + except TimedOut: retry_num += 1 _LOGGER.warning("Timeout trying to set webhook (retry #%d)", retry_num) From 6d083969c2bb22307c5c75693d5470cfab731642 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 18 Oct 2019 02:20:10 +0200 Subject: [PATCH 379/639] Add device action support to the lock integration (#27499) * Add device action support to the lock integration * Check that the enitity supports open service --- .../components/lock/device_action.py | 92 ++++++++++ homeassistant/components/lock/strings.json | 5 + tests/components/lock/test_device_action.py | 170 ++++++++++++++++++ .../custom_components/test/lock.py | 54 ++++++ 4 files changed, 321 insertions(+) create mode 100644 homeassistant/components/lock/device_action.py create mode 100644 tests/components/lock/test_device_action.py create mode 100644 tests/testing_config/custom_components/test/lock.py diff --git a/homeassistant/components/lock/device_action.py b/homeassistant/components/lock/device_action.py new file mode 100644 index 00000000000..c678bfc17cf --- /dev/null +++ b/homeassistant/components/lock/device_action.py @@ -0,0 +1,92 @@ +"""Provides device automations for Lock.""" +from typing import Optional, List +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_DOMAIN, + CONF_TYPE, + CONF_DEVICE_ID, + CONF_ENTITY_ID, + SERVICE_LOCK, + SERVICE_OPEN, + SERVICE_UNLOCK, +) +from homeassistant.core import HomeAssistant, Context +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv +from . import DOMAIN, SUPPORT_OPEN + +ACTION_TYPES = {"lock", "unlock", "open"} + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(ACTION_TYPES), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Lock devices.""" + registry = await entity_registry.async_get_registry(hass) + actions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + # Add actions for each entity that belongs to this integration + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "lock", + } + ) + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "unlock", + } + ) + + state = hass.states.get(entry.entity_id) + if state: + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if features & (SUPPORT_OPEN): + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: "open", + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + service_data = {ATTR_ENTITY_ID: config[CONF_ENTITY_ID]} + + if config[CONF_TYPE] == "lock": + service = SERVICE_LOCK + elif config[CONF_TYPE] == "unlock": + service = SERVICE_UNLOCK + elif config[CONF_TYPE] == "open": + service = SERVICE_OPEN + + await hass.services.async_call( + DOMAIN, service, service_data, blocking=True, context=context + ) diff --git a/homeassistant/components/lock/strings.json b/homeassistant/components/lock/strings.json index baa9bc1604f..9c858916476 100644 --- a/homeassistant/components/lock/strings.json +++ b/homeassistant/components/lock/strings.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Lock {entity_name}", + "open": "Open {entity_name}", + "unlock": "Unlock {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} is locked", "is_unlocked": "{entity_name} is unlocked" diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py new file mode 100644 index 00000000000..2006f9b3ff1 --- /dev/null +++ b/tests/components/lock/test_device_action.py @@ -0,0 +1,170 @@ +"""The tests for Lock device actions.""" +import pytest + +from homeassistant.components.lock import DOMAIN +from homeassistant.const import CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions_support_open(hass, device_reg, entity_reg): + """Test we get the expected actions from a lock which supports open.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["support_open"].unique_id, + device_id=device_entry.id, + ) + + expected_actions = [ + { + "domain": DOMAIN, + "type": "lock", + "device_id": device_entry.id, + "entity_id": "lock.support_open_lock", + }, + { + "domain": DOMAIN, + "type": "unlock", + "device_id": device_entry.id, + "entity_id": "lock.support_open_lock", + }, + { + "domain": DOMAIN, + "type": "open", + "device_id": device_entry.id, + "entity_id": "lock.support_open_lock", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_get_actions_not_support_open(hass, device_reg, entity_reg): + """Test we get the expected actions from a lock which doesn't support open.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["no_support_open"].unique_id, + device_id=device_entry.id, + ) + + expected_actions = [ + { + "domain": DOMAIN, + "type": "lock", + "device_id": device_entry.id, + "entity_id": "lock.no_support_open_lock", + }, + { + "domain": DOMAIN, + "type": "unlock", + "device_id": device_entry.id, + "entity_id": "lock.no_support_open_lock", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass): + """Test for lock actions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event_lock"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "lock.entity", + "type": "lock", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event_unlock"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "lock.entity", + "type": "unlock", + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event_open"}, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "lock.entity", + "type": "open", + }, + }, + ] + }, + ) + + lock_calls = async_mock_service(hass, "lock", "lock") + unlock_calls = async_mock_service(hass, "lock", "unlock") + open_calls = async_mock_service(hass, "lock", "open") + + hass.bus.async_fire("test_event_lock") + await hass.async_block_till_done() + assert len(lock_calls) == 1 + assert len(unlock_calls) == 0 + assert len(open_calls) == 0 + + hass.bus.async_fire("test_event_unlock") + await hass.async_block_till_done() + assert len(lock_calls) == 1 + assert len(unlock_calls) == 1 + assert len(open_calls) == 0 + + hass.bus.async_fire("test_event_open") + await hass.async_block_till_done() + assert len(lock_calls) == 1 + assert len(unlock_calls) == 1 + assert len(open_calls) == 1 diff --git a/tests/testing_config/custom_components/test/lock.py b/tests/testing_config/custom_components/test/lock.py new file mode 100644 index 00000000000..db6ce38b097 --- /dev/null +++ b/tests/testing_config/custom_components/test/lock.py @@ -0,0 +1,54 @@ +""" +Provide a mock lock platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.lock import LockDevice, SUPPORT_OPEN +from tests.common import MockEntity + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + "support_open": MockLock( + name=f"Support open Lock", + is_locked=True, + supported_features=SUPPORT_OPEN, + unique_id="unique_support_open", + ), + "no_support_open": MockLock( + name=f"No support open Lock", + is_locked=True, + supported_features=0, + unique_id="unique_no_support_open", + ), + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockLock(MockEntity, LockDevice): + """Mock Lock class.""" + + @property + def is_locked(self): + """Return true if the lock is locked.""" + return self._handle("is_locked") + + @property + def supported_features(self): + """Return the class of this sensor.""" + return self._handle("supported_features") From 3e7fcc757598b35f7d0c9b3a7728936fd1934d75 Mon Sep 17 00:00:00 2001 From: scheric <38077357+scheric@users.noreply.github.com> Date: Fri, 18 Oct 2019 02:21:00 +0200 Subject: [PATCH 380/639] Add grid sensors to SolarEdge_local (#27247) * Add grid sensors * Formatting * Add possibility to add attributes * Add optimizer attribute * Remove bare 'except' * add proper exception * Remove return attribution 0 * Ad inverter attribution * Style change * Add attribute name to sensors constants * SENSOR_TYPES alphabetical and snake_case lower * Formatting * forgot snake_case lower * Add extra meter sensors * add critical error for debugging * Update sensor.py * swam meter sensors * Add suitable icons to meter reading * Fix for pointless-statement homeassistant/components/solaredge_local/sensor.py:173:8: W0104: Statement seems to have no effect (pointless-statement) homeassistant/components/solaredge_local/sensor.py:192:8: W0104: Statement seems to have no effect (pointless-statement) homeassistant/components/solaredge_local/sensor.py:349:16: W0104: Statement seems to have no effect (pointless-statement) homeassistant/components/solaredge_local/sensor.py:356:16: W0104: Statement seems to have no effect (pointless-statement) * Rename import energy sensor * Insert feadback * Change to debug info * Add check if attribute name exist * Remove unnecessary else * Add return None if no attributes * flake --- .../components/solaredge_local/sensor.py | 133 +++++++++++++++++- 1 file changed, 126 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/solaredge_local/sensor.py b/homeassistant/components/solaredge_local/sensor.py index ce51efa07ca..917fb86ddcb 100644 --- a/homeassistant/components/solaredge_local/sensor.py +++ b/homeassistant/components/solaredge_local/sensor.py @@ -24,63 +24,107 @@ from homeassistant.util import Throttle DOMAIN = "solaredge_local" UPDATE_DELAY = timedelta(seconds=10) +INVERTER_MODES = ( + "SHUTTING_DOWN", + "ERROR", + "STANDBY", + "PAIRING", + "POWER_PRODUCTION", + "AC_CHARGING", + "NOT_PAIRED", + "NIGHT_MODE", + "GRID_MONITORING", + "IDLE", +) + # Supported sensor types: -# Key: ['json_key', 'name', unit, icon] +# Key: ['json_key', 'name', unit, icon, attribute name] SENSOR_TYPES = { - "current_power": ["currentPower", "Current Power", POWER_WATT, "mdi:solar-power"], + "current_AC_voltage": ["gridvoltage", "Grid Voltage", "V", "mdi:current-ac", None], + "current_DC_voltage": ["dcvoltage", "DC Voltage", "V", "mdi:current-dc", None], + "current_frequency": [ + "gridfrequency", + "Grid Frequency", + "Hz", + "mdi:current-ac", + None, + ], + "current_power": [ + "currentPower", + "Current Power", + POWER_WATT, + "mdi:solar-power", + None, + ], "energy_this_month": [ "energyThisMonth", - "Energy this month", + "Energy This Month", ENERGY_WATT_HOUR, "mdi:solar-power", + None, ], "energy_this_year": [ "energyThisYear", - "Energy this year", + "Energy This Year", ENERGY_WATT_HOUR, "mdi:solar-power", + None, ], "energy_today": [ "energyToday", - "Energy today", + "Energy Today", ENERGY_WATT_HOUR, "mdi:solar-power", + None, ], "inverter_temperature": [ "invertertemperature", "Inverter Temperature", TEMP_CELSIUS, "mdi:thermometer", + "operating_mode", ], "lifetime_energy": [ "energyTotal", - "Lifetime energy", + "Lifetime Energy", ENERGY_WATT_HOUR, "mdi:solar-power", + None, + ], + "optimizer_connected": [ + "optimizers", + "Optimizers Online", + "optimizers", + "mdi:solar-panel", + "optimizers_connected", ], "optimizer_current": [ "optimizercurrent", "Average Optimizer Current", "A", "mdi:solar-panel", + None, ], "optimizer_power": [ "optimizerpower", "Average Optimizer Power", POWER_WATT, "mdi:solar-panel", + None, ], "optimizer_temperature": [ "optimizertemperature", "Average Optimizer Temperature", TEMP_CELSIUS, "mdi:solar-panel", + None, ], "optimizer_voltage": [ "optimizervoltage", "Average Optimizer Voltage", "V", "mdi:solar-panel", + None, ], } @@ -122,8 +166,48 @@ def setup_platform(hass, config, add_entities, discovery_info=None): "Inverter Temperature", TEMP_FAHRENHEIT, "mdi:thermometer", + "operating_mode", + None, ] + try: + if status.metersList[0]: + sensors["import_current_power"] = [ + "currentPowerimport", + "current import Power", + POWER_WATT, + "mdi:arrow-collapse-down", + None, + ] + sensors["import_meter_reading"] = [ + "totalEnergyimport", + "total import Energy", + ENERGY_WATT_HOUR, + "mdi:counter", + None, + ] + except IndexError: + _LOGGER.debug("Import meter sensors are not created") + + try: + if status.metersList[1]: + sensors["export_current_power"] = [ + "currentPowerexport", + "current export Power", + POWER_WATT, + "mdi:arrow-expand-up", + None, + ] + sensors["export_meter_reading"] = [ + "totalEnergyexport", + "total export Energy", + ENERGY_WATT_HOUR, + "mdi:counter", + None, + ] + except IndexError: + _LOGGER.debug("Export meter sensors are not created") + # Create solaredge data service which will retrieve and update the data. data = SolarEdgeData(hass, api) @@ -137,6 +221,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_info[1], sensor_info[2], sensor_info[3], + sensor_info[4], ) entities.append(sensor) @@ -146,7 +231,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class SolarEdgeSensor(Entity): """Representation of an SolarEdge Monitoring API sensor.""" - def __init__(self, platform_name, data, json_key, name, unit, icon): + def __init__(self, platform_name, data, json_key, name, unit, icon, attr): """Initialize the sensor.""" self._platform_name = platform_name self._data = data @@ -156,6 +241,7 @@ class SolarEdgeSensor(Entity): self._name = name self._unit_of_measurement = unit self._icon = icon + self._attr = attr @property def name(self): @@ -167,6 +253,16 @@ class SolarEdgeSensor(Entity): """Return the unit of measurement.""" return self._unit_of_measurement + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._attr: + try: + return {self._attr: self._data.info[self._json_key]} + except KeyError: + return None + return None + @property def icon(self): """Return the sensor icon.""" @@ -191,6 +287,7 @@ class SolarEdgeData: self.hass = hass self.api = api self.data = {} + self.info = {} @Throttle(UPDATE_DELAY) def update(self): @@ -243,6 +340,28 @@ class SolarEdgeData: self.data["invertertemperature"] = round( status.inverters.primary.temperature.value, 2 ) + self.data["dcvoltage"] = round(status.inverters.primary.voltage, 2) + self.data["gridfrequency"] = round(status.frequencyHz, 2) + self.data["gridvoltage"] = round(status.voltage, 2) + self.data["optimizers"] = status.optimizersStatus.online + + self.info["optimizers"] = status.optimizersStatus.total + self.info["invertertemperature"] = INVERTER_MODES[status.status] + + try: + if status.metersList[1]: + self.data["currentPowerimport"] = status.metersList[1].currentPower + self.data["totalEnergyimport"] = status.metersList[1].totalEnergy + except IndexError: + pass + + try: + if status.metersList[0]: + self.data["currentPowerexport"] = status.metersList[0].currentPower + self.data["totalEnergyexport"] = status.metersList[0].totalEnergy + except IndexError: + pass + if maintenance.system.name: self.data["optimizertemperature"] = round(statistics.mean(temperature), 2) self.data["optimizervoltage"] = round(statistics.mean(voltage), 2) From dcdcfdd376f0657949592da57cb04e7710be02cf Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 02:22:16 +0200 Subject: [PATCH 381/639] Unload linky config entry (#27831) --- homeassistant/components/linky/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/linky/__init__.py b/homeassistant/components/linky/__init__.py index a7f3d7bb03e..ad5b6743d37 100644 --- a/homeassistant/components/linky/__init__.py +++ b/homeassistant/components/linky/__init__.py @@ -47,9 +47,15 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Linky sensors.""" - hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "sensor") ) - + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload Linky sensors.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_unload(entry, "sensor") + ) return True From 86a4be16367b6944c6ef0736d4461de1c5cc05fd Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 18 Oct 2019 02:22:40 +0200 Subject: [PATCH 382/639] Fix attribution (#27815) --- homeassistant/components/airly/sensor.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index 03439d7d206..bce32d64041 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -1,5 +1,6 @@ """Support for the Airly sensor service.""" from homeassistant.const import ( + ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_NAME, DEVICE_CLASS_HUMIDITY, @@ -93,7 +94,7 @@ class AirlySensor(Entity): self._state = None self._icon = None self._unit_of_measurement = None - self._attrs = {} + self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} @property def name(self): @@ -136,11 +137,6 @@ class AirlySensor(Entity): """Return the unit the value is expressed in.""" return SENSOR_TYPES[self.kind][ATTR_UNIT] - @property - def attribution(self): - """Return the attribution.""" - return ATTRIBUTION - @property def available(self): """Return True if entity is available.""" From 81178661aef2636cfab18a8f1709c907861cd34b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tiit=20R=C3=A4tsep?= Date: Fri, 18 Oct 2019 03:23:11 +0300 Subject: [PATCH 383/639] Added handling for connection errors in state update, added available property (#27794) --- homeassistant/components/soma/__init__.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 5bf51e743e9..b4daa28b5b2 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -3,6 +3,7 @@ import logging import voluptuous as vol from api.soma_api import SomaApi +from requests import RequestException import homeassistant.helpers.config_validation as cv from homeassistant import config_entries @@ -75,6 +76,12 @@ class SomaEntity(Entity): self.device = device self.api = api self.current_position = 50 + self.is_available = True + + @property + def available(self): + """Return true if the last API commands returned successfully.""" + return self.is_available @property def unique_id(self): @@ -100,12 +107,19 @@ class SomaEntity(Entity): async def async_update(self): """Update the device with the latest data.""" - response = await self.hass.async_add_executor_job( - self.api.get_shade_state, self.device["mac"] - ) + try: + response = await self.hass.async_add_executor_job( + self.api.get_shade_state, self.device["mac"] + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + self.is_available = False + return if response["result"] != "success": _LOGGER.error( "Unable to reach device %s (%s)", self.device["name"], response["msg"] ) + self.is_available = False return self.current_position = 100 - response["position"] + self.is_available = True From 564789470e9bf7533c488d6f1fd3a61608fe0f96 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 18 Oct 2019 02:25:16 +0200 Subject: [PATCH 384/639] Add device_info to HomematicIP climate and acp (#27771) --- .../homematicip_cloud/alarm_control_panel.py | 11 +++++++++++ .../components/homematicip_cloud/climate.py | 14 ++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index bb5999108ce..653c1ae147b 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -59,6 +59,17 @@ class HomematicipAlarmControlPanel(AlarmControlPanel): else: self._external_alarm_zone = security_zone + @property + def device_info(self): + """Return device specific attributes.""" + return { + "identifiers": {(HMIPC_DOMAIN, f"ACP {self._home.id}")}, + "name": self.name, + "manufacturer": "eQ-3", + "model": CONST_ALARM_CONTROL_PANEL_NAME, + "via_device": (HMIPC_DOMAIN, self._home.id), + } + @property def state(self) -> str: """Return the state of the device.""" diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index cf1c1baabe0..b8c055dda1f 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -56,12 +56,23 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): def __init__(self, hap: HomematicipHAP, device) -> None: """Initialize heating group.""" - device.modelType = "Group-Heating" + device.modelType = "HmIP-Heating-Group" self._simple_heating = None if device.actualTemperature is None: self._simple_heating = _get_first_heating_thermostat(device) super().__init__(hap, device) + @property + def device_info(self): + """Return device specific attributes.""" + return { + "identifiers": {(HMIPC_DOMAIN, self._device.id)}, + "name": self._device.label, + "manufacturer": "eQ-3", + "model": self._device.modelType, + "via_device": (HMIPC_DOMAIN, self._device.homeId), + } + @property def temperature_unit(self) -> str: """Return the unit of measurement.""" @@ -176,4 +187,3 @@ def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): for device in heating_group.devices: if isinstance(device, (AsyncHeatingThermostat, AsyncHeatingThermostatCompact)): return device - return None From 0888098718d5afa0ddcd2a7dcb81b1573109fb55 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 17 Oct 2019 19:31:53 -0500 Subject: [PATCH 385/639] Use URI provided by Plex for local connections (#27515) * Use provided URI for local connections * Use provided plexapi connection method * Remove unused mock from tests * Handle potential edge case(s) --- homeassistant/components/plex/config_flow.py | 5 +++-- homeassistant/components/plex/server.py | 18 +++++++--------- tests/components/plex/mock_classes.py | 22 -------------------- 3 files changed, 11 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 38727ccff06..9e74756977d 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -89,9 +89,10 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error("Invalid credentials provided, config not created") errors["base"] = "faulty_credentials" except (plexapi.exceptions.NotFound, requests.exceptions.ConnectionError): - _LOGGER.error( - "Plex server could not be reached: %s", server_config[CONF_URL] + server_identifier = ( + server_config.get(CONF_URL) or plex_server.server_choice or "Unknown" ) + _LOGGER.error("Plex server could not be reached: %s", server_identifier) errors["base"] = "not_found" except ServerNotSpecified as available_servers: diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 6ab11430766..d9ddc28c89a 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -39,11 +39,12 @@ class PlexServer: self._server_name = server_config.get(CONF_SERVER) self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) self.options = options + self.server_choice = None def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" - def _set_missing_url(): + def _connect_with_token(): account = plexapi.myplex.MyPlexAccount(token=self._token) available_servers = [ (x.name, x.clientIdentifier) @@ -56,13 +57,10 @@ class PlexServer: if not self._server_name and len(available_servers) > 1: raise ServerNotSpecified(available_servers) - server_choice = ( + self.server_choice = ( self._server_name if self._server_name else available_servers[0][0] ) - connections = account.resource(server_choice).connections - local_url = [x.httpuri for x in connections if x.local] - remote_url = [x.uri for x in connections if not x.local] - self._url = local_url[0] if local_url else remote_url[0] + self._plex_server = account.resource(self.server_choice).connect() def _connect_with_url(): session = None @@ -73,10 +71,10 @@ class PlexServer: self._url, self._token, session ) - if self._token and not self._url: - _set_missing_url() - - _connect_with_url() + if self._url: + _connect_with_url() + else: + _connect_with_token() def clients(self): """Pass through clients call to plexapi.""" diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 756249110ed..69e6a84df63 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -29,34 +29,12 @@ class MockResource: ] self.provides = ["server"] self._mock_plex_server = MockPlexServer(index) - self._connections = [] - for connection in range(2): - self._connections.append(MockConnection(connection)) - - @property - def connections(self): - """Mock the resource connection listing method.""" - return self._connections def connect(self): """Mock the resource connect method.""" return self._mock_plex_server -class MockConnection: # pylint: disable=too-few-public-methods - """Mock a single account resource connection object.""" - - def __init__(self, index, ssl=True): - """Initialize the object.""" - prefix = "https" if ssl else "http" - self.httpuri = ( - f"http://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" - ) - self.uri = f"{prefix}://{MOCK_SERVERS[index][CONF_HOST]}:{MOCK_SERVERS[index][CONF_PORT]}" - # Only first server is local - self.local = not bool(index) - - class MockPlexAccount: """Mock a PlexAccount instance.""" From 489340160b68923767fc4e60cd21a76310f1d8ee Mon Sep 17 00:00:00 2001 From: mvn23 Date: Fri, 18 Oct 2019 02:36:34 +0200 Subject: [PATCH 386/639] Add opentherm_gw options flow. (#27316) --- .../opentherm_gw/.translations/en.json | 11 ++++ .../components/opentherm_gw/__init__.py | 9 ++++ .../components/opentherm_gw/climate.py | 19 +++++-- .../components/opentherm_gw/config_flow.py | 53 +++++++++++++++++- .../components/opentherm_gw/strings.json | 15 ++++-- .../opentherm_gw/test_config_flow.py | 54 +++++++++++++++++-- 6 files changed, 149 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json index 65d7d9e92bb..4aba4ed047a 100644 --- a/homeassistant/components/opentherm_gw/.translations/en.json +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "description": "Options for the OpenTherm Gateway", + "data": { + "floor_temperature": "Floor Temperature", + "precision": "Precision" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index ba6de4c0bea..643f80ae8f9 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -75,6 +75,12 @@ CONFIG_SCHEMA = vol.Schema( ) +async def options_updated(hass, entry): + """Handle options update.""" + gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]] + async_dispatcher_send(hass, gateway.options_update_signal, entry) + + async def async_setup_entry(hass, config_entry): """Set up the OpenTherm Gateway component.""" if DATA_OPENTHERM_GW not in hass.data: @@ -83,6 +89,8 @@ async def async_setup_entry(hass, config_entry): gateway = OpenThermGatewayDevice(hass, config_entry) hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] = gateway + config_entry.add_update_listener(options_updated) + # Schedule directly on the loop to avoid blocking HA startup. hass.loop.create_task(gateway.connect_and_subscribe()) @@ -348,6 +356,7 @@ class OpenThermGatewayDevice: self.climate_config = config_entry.options self.status = {} self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" + self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update" self.gateway = pyotgw.pyotgw() async def connect_and_subscribe(self): diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 19763121e89..44f143d64da 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -39,7 +39,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ents = [] ents.append( OpenThermClimate( - hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] + hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]], + config_entry.options, ) ) @@ -49,12 +50,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class OpenThermClimate(ClimateDevice): """Representation of a climate device.""" - def __init__(self, gw_dev): + def __init__(self, gw_dev, options): """Initialize the device.""" self._gateway = gw_dev self.friendly_name = gw_dev.name - self.floor_temp = gw_dev.climate_config.get(CONF_FLOOR_TEMP) - self.temp_precision = gw_dev.climate_config.get(CONF_PRECISION) + self.floor_temp = options[CONF_FLOOR_TEMP] + self.temp_precision = options.get(CONF_PRECISION) self._current_operation = None self._current_temperature = None self._hvac_mode = HVAC_MODE_HEAT @@ -65,12 +66,22 @@ class OpenThermClimate(ClimateDevice): self._away_state_a = False self._away_state_b = False + @callback + def update_options(self, entry): + """Update climate entity options.""" + self.floor_temp = entry.options[CONF_FLOOR_TEMP] + self.temp_precision = entry.options.get(CONF_PRECISION) + self.async_schedule_update_ha_state() + async def async_added_to_hass(self): """Connect to the OpenTherm Gateway device.""" _LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name) async_dispatcher_connect( self.hass, self._gateway.update_signal, self.receive_report ) + async_dispatcher_connect( + self.hass, self._gateway.options_update_signal, self.update_options + ) @callback def receive_report(self, status): diff --git a/homeassistant/components/opentherm_gw/config_flow.py b/homeassistant/components/opentherm_gw/config_flow.py index e1b68f1ae49..2d7a65bbd84 100644 --- a/homeassistant/components/opentherm_gw/config_flow.py +++ b/homeassistant/components/opentherm_gw/config_flow.py @@ -6,11 +6,20 @@ import pyotgw import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME +from homeassistant.const import ( + CONF_DEVICE, + CONF_ID, + CONF_NAME, + PRECISION_HALVES, + PRECISION_TENTHS, + PRECISION_WHOLE, +) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from . import DOMAIN +from .const import CONF_FLOOR_TEMP, CONF_PRECISION class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -19,6 +28,12 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OpenThermGwOptionsFlow(config_entry) + async def async_step_init(self, info=None): """Handle config flow initiation.""" if info: @@ -89,3 +104,39 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry( title=name, data={CONF_ID: gw_id, CONF_DEVICE: device, CONF_NAME: name} ) + + +class OpenThermGwOptionsFlow(config_entries.OptionsFlow): + """Handle opentherm_gw options.""" + + def __init__(self, config_entry): + """Initialize the options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the opentherm_gw options.""" + if user_input is not None: + if user_input.get(CONF_PRECISION) == 0: + user_input[CONF_PRECISION] = None + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_PRECISION, + default=self.config_entry.options.get(CONF_PRECISION, 0), + ): vol.All( + vol.Coerce(float), + vol.In( + [0, PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE] + ), + ), + vol.Optional( + CONF_FLOOR_TEMP, + default=self.config_entry.options.get(CONF_FLOOR_TEMP, False), + ): bool, + } + ), + ) diff --git a/homeassistant/components/opentherm_gw/strings.json b/homeassistant/components/opentherm_gw/strings.json index a62a4625049..1c246432fb1 100644 --- a/homeassistant/components/opentherm_gw/strings.json +++ b/homeassistant/components/opentherm_gw/strings.json @@ -7,9 +7,7 @@ "data": { "name": "Name", "device": "Path or URL", - "id": "ID", - "precision": "Climate temperature precision", - "floor_temperature": "Floor climate temperature" + "id": "ID" } } }, @@ -19,5 +17,16 @@ "serial_error": "Error connecting to device", "timeout": "Connection attempt timed out" } + }, + "options": { + "step": { + "init": { + "description": "Options for the OpenTherm Gateway", + "data": { + "floor_temperature": "Floor Temperature", + "precision": "Precision" + } + } + } } } \ No newline at end of file diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index da80e2f9fbb..89f2783cf71 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -3,12 +3,16 @@ import asyncio from serial import SerialException from unittest.mock import patch -from homeassistant import config_entries, setup -from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME -from homeassistant.components.opentherm_gw.const import DOMAIN +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES +from homeassistant.components.opentherm_gw.const import ( + DOMAIN, + CONF_FLOOR_TEMP, + CONF_PRECISION, +) from pyotgw import OTGW_ABOUT -from tests.common import mock_coro +from tests.common import mock_coro, MockConfigEntry async def test_form_user(hass): @@ -161,3 +165,45 @@ async def test_form_connection_error(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "serial_error"} assert len(mock_connect.mock_calls) == 1 + + +async def test_options_form(hass): + """Test the options form.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Mock Gateway", + data={ + CONF_NAME: "Mock Gateway", + CONF_DEVICE: "/dev/null", + CONF_ID: "mock_gateway", + }, + options={}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.flow.async_init( + entry.entry_id, context={"source": "test"}, data=None + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], + user_input={CONF_FLOOR_TEMP: True, CONF_PRECISION: PRECISION_HALVES}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PRECISION] == PRECISION_HALVES + assert result["data"][CONF_FLOOR_TEMP] is True + + result = await hass.config_entries.options.flow.async_init( + entry.entry_id, context={"source": "test"}, data=None + ) + + result = await hass.config_entries.options.flow.async_configure( + result["flow_id"], user_input={CONF_PRECISION: 0} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_PRECISION] is None + assert result["data"][CONF_FLOOR_TEMP] is True From 4e25807b7d3c330778b08527c8cdaee059c85498 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 17 Oct 2019 20:51:27 -0400 Subject: [PATCH 387/639] Add ability for MQTT device tracker to map non-default topic payloads to zones/states (#27143) * add ability for MQTT device tracker to map nondefault topic payloads to zones * update new parameter name and add abbreviation * support for payload_home, payload_not_home, and payload_custom * use constants STATE_NOT_HOME and STATE_HOME as defaults * reference state constants directly * add empty dict as default for payload_custom * change parameter name for custom mapping of payloads to non-home zones to be more descriptive * removed 'payload_other_zones' per ballobs review * remove abbreviation for 'payload_other_zones' * add tests for feature --- .../components/mqtt/abbreviations.py | 2 + .../components/mqtt/device_tracker.py | 24 ++++++- tests/components/mqtt/test_device_tracker.py | 64 ++++++++++++++++++- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 2350dfc6634..5a5ed4555db 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -88,11 +88,13 @@ ABBREVIATIONS = { "pl_cls": "payload_close", "pl_disarm": "payload_disarm", "pl_hi_spd": "payload_high_speed", + "pl_home": "payload_home", "pl_lock": "payload_lock", "pl_loc": "payload_locate", "pl_lo_spd": "payload_low_speed", "pl_med_spd": "payload_medium_speed", "pl_not_avail": "payload_not_available", + "pl_not_home": "payload_not_home", "pl_off": "payload_off", "pl_off_spd": "payload_off_speed", "pl_on": "payload_on", diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index e9613e09a95..c9cce3ebeda 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -5,16 +5,23 @@ import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.device_tracker import PLATFORM_SCHEMA -from homeassistant.const import CONF_DEVICES from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_DEVICES, STATE_NOT_HOME, STATE_HOME from . import CONF_QOS _LOGGER = logging.getLogger(__name__) +CONF_PAYLOAD_HOME = "payload_home" +CONF_PAYLOAD_NOT_HOME = "payload_not_home" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( - {vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}} + { + vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, + vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, + vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, + } ) @@ -22,13 +29,24 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Set up the MQTT tracker.""" devices = config[CONF_DEVICES] qos = config[CONF_QOS] + payload_home = config[CONF_PAYLOAD_HOME] + payload_not_home = config[CONF_PAYLOAD_NOT_HOME] for dev_id, topic in devices.items(): @callback def async_message_received(msg, dev_id=dev_id): """Handle received MQTT message.""" - hass.async_create_task(async_see(dev_id=dev_id, location_name=msg.payload)) + if msg.payload == payload_home: + location_name = STATE_HOME + elif msg.payload == payload_not_home: + location_name = STATE_NOT_HOME + else: + location_name = msg.payload + + hass.async_create_task( + async_see(dev_id=dev_id, location_name=location_name) + ) await mqtt.async_subscribe(hass, topic, async_message_received, qos) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index caad12b3e39..14180d2dcf9 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -4,7 +4,7 @@ import pytest from homeassistant.components import device_tracker from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT -from homeassistant.const import CONF_PLATFORM +from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component from tests.common import async_fire_mqtt_message @@ -156,3 +156,65 @@ async def test_multi_level_wildcard_topic_not_matching(hass, mock_device_tracker async_fire_mqtt_message(hass, topic, location) await hass.async_block_till_done() assert hass.states.get(entity_id) is None + + +async def test_matching_custom_payload_for_home_and_not_home( + hass, mock_device_tracker_conf +): + """Test custom payload_home sets state to home and custom payload_not_home sets state to not_home.""" + dev_id = "paulus" + entity_id = ENTITY_ID_FORMAT.format(dev_id) + topic = "/location/paulus" + payload_home = "present" + payload_not_home = "not present" + + hass.config.components = set(["mqtt", "zone"]) + assert await async_setup_component( + hass, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: topic}, + "payload_home": payload_home, + "payload_not_home": payload_not_home, + } + }, + ) + async_fire_mqtt_message(hass, topic, payload_home) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_HOME + + async_fire_mqtt_message(hass, topic, payload_not_home) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_NOT_HOME + + +async def test_not_matching_custom_payload_for_home_and_not_home( + hass, mock_device_tracker_conf +): + """Test not matching payload does not set state to home or not_home.""" + dev_id = "paulus" + entity_id = ENTITY_ID_FORMAT.format(dev_id) + topic = "/location/paulus" + payload_home = "present" + payload_not_home = "not present" + payload_not_matching = "test" + + hass.config.components = set(["mqtt", "zone"]) + assert await async_setup_component( + hass, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: topic}, + "payload_home": payload_home, + "payload_not_home": payload_not_home, + } + }, + ) + async_fire_mqtt_message(hass, topic, payload_not_matching) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state != STATE_HOME + assert hass.states.get(entity_id).state != STATE_NOT_HOME From 2bc6b59e79ba773999334595cd5f117959a94860 Mon Sep 17 00:00:00 2001 From: Tsvi Mostovicz Date: Fri, 18 Oct 2019 06:32:24 +0300 Subject: [PATCH 388/639] Move holiday info into a single sensor with multiple attributess (#27654) * Move holiday onfo into a single sensor with multiple attributess * Add tests for holiday attributes --- .../components/jewish_calendar/__init__.py | 3 +- .../components/jewish_calendar/manifest.json | 2 +- .../components/jewish_calendar/sensor.py | 15 +++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/jewish_calendar/test_sensor.py | 78 +++++++++---------- 6 files changed, 52 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/jewish_calendar/__init__.py b/homeassistant/components/jewish_calendar/__init__.py index c7bbbdb2d90..bbe0c1d24fd 100644 --- a/homeassistant/components/jewish_calendar/__init__.py +++ b/homeassistant/components/jewish_calendar/__init__.py @@ -20,8 +20,7 @@ SENSOR_TYPES = { "data": { "date": ["Date", "mdi:judaism"], "weekly_portion": ["Parshat Hashavua", "mdi:book-open-variant"], - "holiday_name": ["Holiday name", "mdi:calendar-star"], - "holiday_type": ["Holiday type", "mdi:counter"], + "holiday": ["Holiday", "mdi:calendar-star"], "omer_count": ["Day of the Omer", "mdi:counter"], }, "time": { diff --git a/homeassistant/components/jewish_calendar/manifest.json b/homeassistant/components/jewish_calendar/manifest.json index 7b6653ba832..08182daedd0 100644 --- a/homeassistant/components/jewish_calendar/manifest.json +++ b/homeassistant/components/jewish_calendar/manifest.json @@ -3,7 +3,7 @@ "name": "Jewish calendar", "documentation": "https://www.home-assistant.io/integrations/jewish_calendar", "requirements": [ - "hdate==0.9.0" + "hdate==0.9.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/jewish_calendar/sensor.py b/homeassistant/components/jewish_calendar/sensor.py index 453b3de4bae..54a3d1497aa 100644 --- a/homeassistant/components/jewish_calendar/sensor.py +++ b/homeassistant/components/jewish_calendar/sensor.py @@ -44,6 +44,7 @@ class JewishCalendarSensor(Entity): self._havdalah_offset = data["havdalah_offset"] self._diaspora = data["diaspora"] self._state = None + self._holiday_attrs = {} @property def name(self): @@ -103,6 +104,14 @@ class JewishCalendarSensor(Entity): hebrew=self._hebrew, ) + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self._type == "holiday": + return self._holiday_attrs + + return {} + def get_state(self, after_shkia_date, after_tzais_date): """For a given type of sensor, return the state.""" # Terminology note: by convention in py-libhdate library, "upcoming" @@ -112,10 +121,10 @@ class JewishCalendarSensor(Entity): if self._type == "weekly_portion": # Compute the weekly portion based on the upcoming shabbat. return after_tzais_date.upcoming_shabbat.parasha - if self._type == "holiday_name": + if self._type == "holiday": + self._holiday_attrs["type"] = after_shkia_date.holiday_type.name + self._holiday_attrs["id"] = after_shkia_date.holiday_name return after_shkia_date.holiday_description - if self._type == "holiday_type": - return after_shkia_date.holiday_type if self._type == "omer_count": return after_shkia_date.omer_day diff --git a/requirements_all.txt b/requirements_all.txt index 6b8b1931a4a..df24fde50cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -619,7 +619,7 @@ hass-nabucasa==0.22 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.0 +hdate==0.9.1 # homeassistant.components.heatmiser heatmiserV3==0.9.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ece529ef6e8..5b224126d67 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -227,7 +227,7 @@ hass-nabucasa==0.22 hbmqtt==0.9.5 # homeassistant.components.jewish_calendar -hdate==0.9.0 +hdate==0.9.1 # homeassistant.components.here_travel_time herepy==0.6.3.1 diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 94b26f80d2d..07e0b7cb192 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -42,27 +42,17 @@ TEST_PARAMS = [ False, 'כ"ג אלול ה\' תשע"ח', ), - ( - dt(2018, 9, 10), - "UTC", - 31.778, - 35.235, - "hebrew", - "holiday_name", - False, - "א' ראש השנה", - ), + (dt(2018, 9, 10), "UTC", 31.778, 35.235, "hebrew", "holiday", False, "א' ראש השנה"), ( dt(2018, 9, 10), "UTC", 31.778, 35.235, "english", - "holiday_name", + "holiday", False, "Rosh Hashana I", ), - (dt(2018, 9, 10), "UTC", 31.778, 35.235, "english", "holiday_type", False, 1), ( dt(2018, 9, 8), "UTC", @@ -128,9 +118,8 @@ TEST_PARAMS = [ TEST_IDS = [ "date_output", "date_output_hebrew", - "holiday_name", - "holiday_name_english", - "holiday_type", + "holiday", + "holiday_english", "torah_reading", "first_stars_ny", "first_stars_jerusalem", @@ -187,7 +176,12 @@ async def test_jewish_calendar_sensor( dt_util.as_utc(time_zone.localize(result)) if isinstance(result, dt) else result ) - assert hass.states.get(f"sensor.test_{sensor}").state == str(result) + sensor_object = hass.states.get(f"sensor.test_{sensor}") + assert sensor_object.state == str(result) + + if sensor == "holiday": + assert sensor_object.attributes.get("type") == "YOM_TOV" + assert sensor_object.attributes.get("id") == "rosh_hashana_i" SHABBAT_PARAMS = [ @@ -256,8 +250,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", - "english_holiday_name": "Erev Rosh Hashana", - "hebrew_holiday_name": "ערב ראש השנה", + "english_holiday": "Erev Rosh Hashana", + "hebrew_holiday": "ערב ראש השנה", }, ), make_nyc_test_params( @@ -269,8 +263,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", - "english_holiday_name": "Rosh Hashana I", - "hebrew_holiday_name": "א' ראש השנה", + "english_holiday": "Rosh Hashana I", + "hebrew_holiday": "א' ראש השנה", }, ), make_nyc_test_params( @@ -282,8 +276,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 9, 15, 19, 50), "english_parshat_hashavua": "Vayeilech", "hebrew_parshat_hashavua": "וילך", - "english_holiday_name": "Rosh Hashana II", - "hebrew_holiday_name": "ב' ראש השנה", + "english_holiday": "Rosh Hashana II", + "hebrew_holiday": "ב' ראש השנה", }, ), make_nyc_test_params( @@ -306,8 +300,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Hoshana Raba", - "hebrew_holiday_name": "הושענא רבה", + "english_holiday": "Hoshana Raba", + "hebrew_holiday": "הושענא רבה", }, ), make_nyc_test_params( @@ -319,8 +313,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Shmini Atzeret", - "hebrew_holiday_name": "שמיני עצרת", + "english_holiday": "Shmini Atzeret", + "hebrew_holiday": "שמיני עצרת", }, ), make_nyc_test_params( @@ -332,8 +326,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 19, 13), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Simchat Torah", - "hebrew_holiday_name": "שמחת תורה", + "english_holiday": "Simchat Torah", + "hebrew_holiday": "שמחת תורה", }, ), make_jerusalem_test_params( @@ -345,8 +339,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Hoshana Raba", - "hebrew_holiday_name": "הושענא רבה", + "english_holiday": "Hoshana Raba", + "hebrew_holiday": "הושענא רבה", }, ), make_jerusalem_test_params( @@ -358,8 +352,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2018, 10, 6, 18, 56), "english_parshat_hashavua": "Bereshit", "hebrew_parshat_hashavua": "בראשית", - "english_holiday_name": "Shmini Atzeret", - "hebrew_holiday_name": "שמיני עצרת", + "english_holiday": "Shmini Atzeret", + "hebrew_holiday": "שמיני עצרת", }, ), make_jerusalem_test_params( @@ -382,8 +376,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": "unknown", "english_parshat_hashavua": "Bamidbar", "hebrew_parshat_hashavua": "במדבר", - "english_holiday_name": "Erev Shavuot", - "hebrew_holiday_name": "ערב שבועות", + "english_holiday": "Erev Shavuot", + "hebrew_holiday": "ערב שבועות", }, ), make_nyc_test_params( @@ -395,8 +389,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2016, 6, 18, 21, 19), "english_parshat_hashavua": "Nasso", "hebrew_parshat_hashavua": "נשא", - "english_holiday_name": "Shavuot", - "hebrew_holiday_name": "שבועות", + "english_holiday": "Shavuot", + "hebrew_holiday": "שבועות", }, ), make_jerusalem_test_params( @@ -408,8 +402,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", - "english_holiday_name": "Rosh Hashana I", - "hebrew_holiday_name": "א' ראש השנה", + "english_holiday": "Rosh Hashana I", + "hebrew_holiday": "א' ראש השנה", }, ), make_jerusalem_test_params( @@ -421,8 +415,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", - "english_holiday_name": "Rosh Hashana II", - "hebrew_holiday_name": "ב' ראש השנה", + "english_holiday": "Rosh Hashana II", + "hebrew_holiday": "ב' ראש השנה", }, ), make_jerusalem_test_params( @@ -434,8 +428,8 @@ SHABBAT_PARAMS = [ "english_upcoming_shabbat_havdalah": dt(2017, 9, 23, 19, 13), "english_parshat_hashavua": "Ha'Azinu", "hebrew_parshat_hashavua": "האזינו", - "english_holiday_name": "", - "hebrew_holiday_name": "", + "english_holiday": "", + "hebrew_holiday": "", }, ), ] From 9625e0463b24667a06481649a65750ac4d3c1df9 Mon Sep 17 00:00:00 2001 From: Bendik Brenne Date: Fri, 18 Oct 2019 06:44:09 +0200 Subject: [PATCH 389/639] Add sinch integration (notify component) (#26502) * Added sinch integration (notify component) * Updated requirements * Fixes according to lint * Update homeassistant/components/sinch/notify.py Co-Authored-By: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/sinch/notify.py Co-Authored-By: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> * Update homeassistant/components/sinch/notify.py Co-Authored-By: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> * Adds @bendikrb as codeowner * Imports to the top. Catching specific exceptions. Logic fixes * Updated CODEOWNERS * Reformatting (black) * Added sinch component to .coveragerc * Conform to pylintrc * Okay, Mr. Black * Fixed: Catching too general exception Exception --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/sinch/__init__.py | 1 + homeassistant/components/sinch/manifest.json | 12 +++ homeassistant/components/sinch/notify.py | 97 ++++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 115 insertions(+) create mode 100644 homeassistant/components/sinch/__init__.py create mode 100644 homeassistant/components/sinch/manifest.json create mode 100644 homeassistant/components/sinch/notify.py diff --git a/.coveragerc b/.coveragerc index 859e1c0f92c..389d289ea20 100644 --- a/.coveragerc +++ b/.coveragerc @@ -604,6 +604,7 @@ omit = homeassistant/components/skybeacon/sensor.py homeassistant/components/skybell/* homeassistant/components/slack/notify.py + homeassistant/components/sinch/* homeassistant/components/slide/* homeassistant/components/sma/sensor.py homeassistant/components/smappee/* diff --git a/CODEOWNERS b/CODEOWNERS index 40f1e93cfb9..2f228105cbb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -257,6 +257,7 @@ homeassistant/components/shell_command/* @home-assistant/core homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/simplisafe/* @bachya +homeassistant/components/sinch/* @bendikrb homeassistant/components/slide/* @ualex73 homeassistant/components/sma/* @kellerza homeassistant/components/smarthab/* @outadoc diff --git a/homeassistant/components/sinch/__init__.py b/homeassistant/components/sinch/__init__.py new file mode 100644 index 00000000000..43a5f2b2a5c --- /dev/null +++ b/homeassistant/components/sinch/__init__.py @@ -0,0 +1 @@ +"""Component to integrate with Sinch SMS API.""" diff --git a/homeassistant/components/sinch/manifest.json b/homeassistant/components/sinch/manifest.json new file mode 100644 index 00000000000..a1864428fee --- /dev/null +++ b/homeassistant/components/sinch/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "sinch", + "name": "Sinch", + "documentation": "https://www.home-assistant.io/components/sinch", + "dependencies": [], + "codeowners": [ + "@bendikrb" + ], + "requirements": [ + "clx-sdk-xms==1.0.0" + ] +} \ No newline at end of file diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py new file mode 100644 index 00000000000..173873c0a6c --- /dev/null +++ b/homeassistant/components/sinch/notify.py @@ -0,0 +1,97 @@ +"""Support for Sinch notifications.""" +import logging + +import voluptuous as vol +from clx.xms.api import MtBatchTextSmsResult +from clx.xms.client import Client +from clx.xms.exceptions import ( + ErrorResponseException, + UnexpectedResponseException, + UnauthorizedException, + NotFoundException, +) + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.notify import ( + ATTR_MESSAGE, + ATTR_DATA, + ATTR_TARGET, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.const import CONF_API_KEY, CONF_SENDER + +DOMAIN = "sinch" + +CONF_SERVICE_PLAN_ID = "service_plan_id" +CONF_DEFAULT_RECIPIENTS = "default_recipients" + +ATTR_SENDER = CONF_SENDER + +DEFAULT_SENDER = "Home Assistant" + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_SERVICE_PLAN_ID): cv.string, + vol.Optional(CONF_SENDER, default=DEFAULT_SENDER): cv.string, + vol.Optional(CONF_DEFAULT_RECIPIENTS, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + } +) + + +def get_service(hass, config, discovery_info=None): + """Get the Sinch notification service.""" + return SinchNotificationService(config) + + +class SinchNotificationService(BaseNotificationService): + """Send Notifications to Sinch SMS recipients.""" + + def __init__(self, config): + """Initialize the service.""" + self.default_recipients = config[CONF_DEFAULT_RECIPIENTS] + self.sender = config[CONF_SENDER] + self.client = Client(config[CONF_SERVICE_PLAN_ID], config[CONF_API_KEY]) + + def send_message(self, message="", **kwargs): + """Send a message to a user.""" + targets = kwargs.get(ATTR_TARGET, self.default_recipients) + data = kwargs.get(ATTR_DATA, {}) + + clx_args = {ATTR_MESSAGE: message, ATTR_SENDER: self.sender} + + if ATTR_SENDER in data: + clx_args[ATTR_SENDER] = data[ATTR_SENDER] + + if not targets: + _LOGGER.error("At least 1 target is required") + return + + try: + for target in targets: + result: MtBatchTextSmsResult = self.client.create_text_message( + clx_args[ATTR_SENDER], target, clx_args[ATTR_MESSAGE] + ) + batch_id = result.batch_id + _LOGGER.debug( + 'Successfully sent SMS to "%s" (batch_id: %s)', target, batch_id + ) + except ErrorResponseException as ex: + _LOGGER.error( + "Caught ErrorResponseException. Response code: %d (%s)", + ex.error_code, + ex, + ) + except NotFoundException as ex: + _LOGGER.error("Caught NotFoundException (request URL: %s)", ex.url) + except UnauthorizedException as ex: + _LOGGER.error( + "Caught UnauthorizedException (service plan: %s)", ex.service_plan_id + ) + except UnexpectedResponseException as ex: + _LOGGER.error("Caught UnexpectedResponseException: %s", ex) diff --git a/requirements_all.txt b/requirements_all.txt index df24fde50cf..9fe8b6622de 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -346,6 +346,9 @@ ciscosparkapi==0.4.2 # homeassistant.components.cppm_tracker clearpasspy==1.0.2 +# homeassistant.components.sinch +clx-sdk-xms==1.0.0 + # homeassistant.components.co2signal co2signal==0.4.2 From 7ec8384fa697dec9b152ada6662edb5d7f822b3e Mon Sep 17 00:00:00 2001 From: Tobias Efinger Date: Fri, 18 Oct 2019 07:11:51 +0200 Subject: [PATCH 390/639] Add service description for route53 integration (#27774) --- homeassistant/components/route53/services.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/route53/services.yaml b/homeassistant/components/route53/services.yaml index e69de29bb2d..20dbfa77f7a 100644 --- a/homeassistant/components/route53/services.yaml +++ b/homeassistant/components/route53/services.yaml @@ -0,0 +1,2 @@ +update_records: + description: Trigger update of records. \ No newline at end of file From 5cb145f5881afa4d8232240f40df36c2c132b7f9 Mon Sep 17 00:00:00 2001 From: Quentame Date: Fri, 18 Oct 2019 07:12:32 +0200 Subject: [PATCH 391/639] Move imports in openweathermap component (#27779) --- homeassistant/components/openweathermap/sensor.py | 7 ++----- homeassistant/components/openweathermap/weather.py | 11 ++++------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index 51dc92623f3..23f88f59aad 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pyowm import OWM +from pyowm.exceptions.api_call_error import APICallError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -56,7 +58,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenWeatherMap sensor.""" - from pyowm import OWM if None in (hass.config.latitude, hass.config.longitude): _LOGGER.error("Latitude or longitude not set in Home Assistant config") @@ -127,8 +128,6 @@ class OpenWeatherMapSensor(Entity): def update(self): """Get the latest data from OWM and updates the states.""" - from pyowm.exceptions.api_call_error import APICallError - try: self.owa_client.update() except APICallError: @@ -201,8 +200,6 @@ class WeatherData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from OpenWeatherMap.""" - from pyowm.exceptions.api_call_error import APICallError - try: obs = self.owm.weather_at_coords(self.latitude, self.longitude) except (APICallError, TypeError): diff --git a/homeassistant/components/openweathermap/weather.py b/homeassistant/components/openweathermap/weather.py index a51ea26607d..69ca965d660 100644 --- a/homeassistant/components/openweathermap/weather.py +++ b/homeassistant/components/openweathermap/weather.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from pyowm import OWM +from pyowm.exceptions.api_call_error import APICallError import voluptuous as vol from homeassistant.components.weather import ( @@ -71,7 +73,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenWeatherMap weather platform.""" - import pyowm longitude = config.get(CONF_LONGITUDE, round(hass.config.longitude, 5)) latitude = config.get(CONF_LATITUDE, round(hass.config.latitude, 5)) @@ -79,8 +80,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): mode = config.get(CONF_MODE) try: - owm = pyowm.OWM(config.get(CONF_API_KEY)) - except pyowm.exceptions.api_call_error.APICallError: + owm = OWM(config.get(CONF_API_KEY)) + except APICallError: _LOGGER.error("Error while connecting to OpenWeatherMap") return False @@ -225,8 +226,6 @@ class OpenWeatherMapWeather(WeatherEntity): def update(self): """Get the latest data from OWM and updates the states.""" - from pyowm.exceptions.api_call_error import APICallError - try: self._owm.update() self._owm.update_forecast() @@ -263,8 +262,6 @@ class WeatherData: @Throttle(MIN_TIME_BETWEEN_FORECAST_UPDATES) def update_forecast(self): """Get the latest forecast from OpenWeatherMap.""" - from pyowm.exceptions.api_call_error import APICallError - try: if self._mode == "daily": fcd = self.owm.daily_forecast_at_coords( From 511766cb065dcf3ae5e487a3e0f3b7e1a1d52148 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 07:13:29 +0200 Subject: [PATCH 392/639] Move imports in apns component (#27804) * Move imports in apns component * fixed apns tests --- homeassistant/components/apns/notify.py | 17 ++++++++--------- tests/components/apns/test_notify.py | 8 ++++---- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/apns/notify.py b/homeassistant/components/apns/notify.py index dbd45013a3c..c24c9cc1605 100644 --- a/homeassistant/components/apns/notify.py +++ b/homeassistant/components/apns/notify.py @@ -1,14 +1,11 @@ """APNS Notification platform.""" import logging +from apns2.client import APNsClient +from apns2.errors import Unregistered +from apns2.payload import Payload import voluptuous as vol -from homeassistant.config import load_yaml_config_file -from homeassistant.const import ATTR_NAME, CONF_NAME, CONF_PLATFORM -from homeassistant.helpers import template as template_helper -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import track_state_change - from homeassistant.components.notify import ( ATTR_DATA, ATTR_TARGET, @@ -16,6 +13,11 @@ from homeassistant.components.notify import ( PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.config import load_yaml_config_file +from homeassistant.const import ATTR_NAME, CONF_NAME, CONF_PLATFORM +from homeassistant.helpers import template as template_helper +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_state_change APNS_DEVICES = "apns.yaml" CONF_CERTFILE = "cert_file" @@ -213,9 +215,6 @@ class ApnsNotificationService(BaseNotificationService): def send_message(self, message=None, **kwargs): """Send push message to registered devices.""" - from apns2.client import APNsClient - from apns2.payload import Payload - from apns2.errors import Unregistered apns = APNsClient( self.certificate, use_sandbox=self.sandbox, use_alternative_port=False diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py index a4202f74d39..78f597c58ad 100644 --- a/tests/components/apns/test_notify.py +++ b/tests/components/apns/test_notify.py @@ -239,7 +239,7 @@ class TestApns(unittest.TestCase): assert "tracking123" == test_device_1.tracking_device_id assert "tracking456" == test_device_2.tracking_device_id - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") def test_send(self, mock_client): """Test updating an existing device.""" send = mock_client.return_value.send_notification @@ -274,7 +274,7 @@ class TestApns(unittest.TestCase): assert "test.mp3" == payload.sound assert "testing" == payload.category - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") def test_send_when_disabled(self, mock_client): """Test updating an existing device.""" send = mock_client.return_value.send_notification @@ -299,7 +299,7 @@ class TestApns(unittest.TestCase): assert not send.called - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") def test_send_with_state(self, mock_client): """Test updating an existing device.""" send = mock_client.return_value.send_notification @@ -334,7 +334,7 @@ class TestApns(unittest.TestCase): assert "5678" == target assert "Hello" == payload.alert - @patch("apns2.client.APNsClient") + @patch("homeassistant.components.apns.notify.APNsClient") @patch("homeassistant.components.apns.notify._write_device") def test_disable_when_unregistered(self, mock_write, mock_client): """Test disabling a device when it is unregistered.""" From 675c91b436341d4ac97b1382e4649adce5ae2066 Mon Sep 17 00:00:00 2001 From: Tomasz Jagusz Date: Fri, 18 Oct 2019 10:11:53 +0200 Subject: [PATCH 393/639] Move imports in yweather (#27842) Changes checked with pylint. Formatted with black Imports sorted with isort --- homeassistant/components/yweather/sensor.py | 19 +++++++++++-------- homeassistant/components/yweather/weather.py | 12 ++++++------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/yweather/sensor.py b/homeassistant/components/yweather/sensor.py index 4dc23699872..c7f752a8836 100644 --- a/homeassistant/components/yweather/sensor.py +++ b/homeassistant/components/yweather/sensor.py @@ -1,17 +1,23 @@ """Support for the Yahoo! Weather service.""" -import logging from datetime import timedelta +import logging import voluptuous as vol +from yahooweather import ( # pylint: disable=import-error + UNIT_C, + UNIT_F, + YahooWeather, + get_woeid, +) -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_CELSIUS, + ATTR_ATTRIBUTION, CONF_MONITORED_CONDITIONS, CONF_NAME, - ATTR_ATTRIBUTION, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -20,6 +26,7 @@ _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Weather details provided by Yahoo! Inc." CONF_FORECAST = "forecast" + CONF_WOEID = "woeid" DEFAULT_NAME = "Yweather" @@ -52,8 +59,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yahoo! weather sensor.""" - from yahooweather import get_woeid, UNIT_C, UNIT_F - unit = hass.config.units.temperature_unit woeid = config.get(CONF_WOEID) forecast = config.get(CONF_FORECAST) @@ -181,8 +186,6 @@ class YahooWeatherData: def __init__(self, woeid, temp_unit): """Initialize the data object.""" - from yahooweather import YahooWeather - self._yahoo = YahooWeather(woeid, temp_unit) @property diff --git a/homeassistant/components/yweather/weather.py b/homeassistant/components/yweather/weather.py index 6779fd1896d..202124aa340 100644 --- a/homeassistant/components/yweather/weather.py +++ b/homeassistant/components/yweather/weather.py @@ -3,6 +3,12 @@ from datetime import timedelta import logging import voluptuous as vol +from yahooweather import ( # pylint: disable=import-error + UNIT_C, + UNIT_F, + YahooWeather, + get_woeid, +) from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, @@ -21,7 +27,6 @@ DATA_CONDITION = "yahoo_condition" ATTRIBUTION = "Weather details provided by Yahoo! Inc." - CONF_WOEID = "woeid" DEFAULT_NAME = "Yweather" @@ -46,7 +51,6 @@ CONDITION_CLASSES = { "exceptional": [0, 1, 2, 19, 22], } - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_WOEID): cv.string, @@ -57,8 +61,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Yahoo! weather platform.""" - from yahooweather import get_woeid, UNIT_C, UNIT_F - unit = hass.config.units.temperature_unit woeid = config.get(CONF_WOEID) name = config.get(CONF_NAME) @@ -181,8 +183,6 @@ class YahooWeatherData: def __init__(self, woeid, temp_unit): """Initialize the data object.""" - from yahooweather import YahooWeather - self._yahoo = YahooWeather(woeid, temp_unit) @property From 58d2d858cd43758b554e4f081420dcdae577e4e6 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 15:08:40 +0200 Subject: [PATCH 394/639] Move imports in brunt component (#27856) --- homeassistant/components/brunt/cover.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index af809cc7878..7d4279cf5b2 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -2,17 +2,18 @@ import logging +from brunt import BruntAPI import voluptuous as vol -from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME from homeassistant.components.cover import ( ATTR_POSITION, - CoverDevice, PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, + CoverDevice, ) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -36,7 +37,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the brunt platform.""" # pylint: disable=no-name-in-module - from brunt import BruntAPI username = config[CONF_USERNAME] password = config[CONF_PASSWORD] From c0d084fb04c1a5dcced64e018add3c619a05485a Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 15:12:00 +0200 Subject: [PATCH 395/639] Move imports in blockchain component (#27852) --- homeassistant/components/blockchain/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blockchain/sensor.py b/homeassistant/components/blockchain/sensor.py index c95ccb3fed3..6d17484bdd7 100644 --- a/homeassistant/components/blockchain/sensor.py +++ b/homeassistant/components/blockchain/sensor.py @@ -1,12 +1,13 @@ """Support for Blockchain.info sensors.""" -import logging from datetime import timedelta +import logging +from pyblockchain import get_balance, validate_address import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -31,7 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Blockchain.info sensors.""" - from pyblockchain import validate_address addresses = config.get(CONF_ADDRESSES) name = config.get(CONF_NAME) @@ -81,6 +81,5 @@ class BlockchainSensor(Entity): def update(self): """Get the latest state of the sensor.""" - from pyblockchain import get_balance self._state = get_balance(self.addresses) From 3bb46d5080479980361ee6837b188b3ce9ed4a0e Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 15:12:36 +0200 Subject: [PATCH 396/639] Move blackbird imports (#27849) * Move imports in blackbird component * fixed tests after import move to top level --- homeassistant/components/blackbird/media_player.py | 7 +++---- tests/components/blackbird/test_media_player.py | 5 ++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/blackbird/media_player.py b/homeassistant/components/blackbird/media_player.py index eca7fa84f50..e1aa7200c07 100644 --- a/homeassistant/components/blackbird/media_player.py +++ b/homeassistant/components/blackbird/media_player.py @@ -2,9 +2,11 @@ import logging import socket +from pyblackbird import get_blackbird +from serial import SerialException import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( DOMAIN, SUPPORT_SELECT_SOURCE, @@ -72,9 +74,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): port = config.get(CONF_PORT) host = config.get(CONF_HOST) - from pyblackbird import get_blackbird - from serial import SerialException - connection = None if port is not None: try: diff --git a/tests/components/blackbird/test_media_player.py b/tests/components/blackbird/test_media_player.py index c41d5dcef41..34309fdbcf3 100644 --- a/tests/components/blackbird/test_media_player.py +++ b/tests/components/blackbird/test_media_player.py @@ -180,7 +180,10 @@ class TestBlackbirdMediaPlayer(unittest.TestCase): self.hass = tests.common.get_test_home_assistant() self.hass.start() # Note, source dictionary is unsorted! - with mock.patch("pyblackbird.get_blackbird", new=lambda *a: self.blackbird): + with mock.patch( + "homeassistant.components.blackbird.media_player.get_blackbird", + new=lambda *a: self.blackbird, + ): setup_platform( self.hass, { From 9392cbff03b65539b9eb1dd5a0912b636acf08b3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 18 Oct 2019 16:11:40 +0200 Subject: [PATCH 397/639] cryptography + numpy for python 3.8 (#27861) --- homeassistant/components/iqvia/manifest.json | 2 +- homeassistant/components/opencv/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/components/trend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 4 ++-- requirements_test_all.txt | 2 +- setup.py | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index caf422938b2..04723a6a1f6 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", "requirements": [ - "numpy==1.17.1", + "numpy==1.17.3", "pyiqvia==0.2.1" ], "dependencies": [], diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 40421674a4b..bd82da000cf 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -3,7 +3,7 @@ "name": "Opencv", "documentation": "https://www.home-assistant.io/integrations/opencv", "requirements": [ - "numpy==1.17.1", + "numpy==1.17.3", "opencv-python-headless==4.1.1.26" ], "dependencies": [], diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index e7d35829ffb..e0a8728b295 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", - "numpy==1.17.1", + "numpy==1.17.3", "protobuf==3.6.1" ], "dependencies": [], diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index 1432d2d21a0..cf9333be7c3 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -3,7 +3,7 @@ "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", "requirements": [ - "numpy==1.17.1" + "numpy==1.17.3" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1648bdc3db5..80357eccf71 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ attrs==19.2.0 bcrypt==3.1.7 certifi>=2019.9.11 contextvars==2.4;python_version<"3.7" -cryptography==2.7 +cryptography==2.8 distro==1.4.0 hass-nabucasa==0.22 home-assistant-frontend==20191014.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9fe8b6622de..951c9800943 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -9,7 +9,7 @@ contextvars==2.4;python_version<"3.7" importlib-metadata==0.23 jinja2>=2.10.1 PyJWT==1.7.1 -cryptography==2.7 +cryptography==2.8 pip>=8.0.3 python-slugify==3.0.6 pytz>=2019.03 @@ -884,7 +884,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.1 +numpy==1.17.3 # homeassistant.components.oasa_telematics oasatelematics==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b224126d67..c9a0013212c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -313,7 +313,7 @@ nuheat==0.3.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.17.1 +numpy==1.17.3 # homeassistant.components.google oauth2client==4.0.0 diff --git a/setup.py b/setup.py index 5b9988cff27..0e8f8313a98 100755 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ REQUIRES = [ "jinja2>=2.10.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. - "cryptography==2.7", + "cryptography==2.8", "pip>=8.0.3", "python-slugify==3.0.6", "pytz>=2019.03", From 03cc7f377bb6deb51e4cc431e0498bd70395ef03 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 17:12:42 +0200 Subject: [PATCH 398/639] Move imports in bom component (#27854) --- homeassistant/components/bom/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/bom/camera.py b/homeassistant/components/bom/camera.py index f417cf769a4..7460b84f734 100644 --- a/homeassistant/components/bom/camera.py +++ b/homeassistant/components/bom/camera.py @@ -1,4 +1,5 @@ """Provide animated GIF loops of BOM radar imagery.""" +from bomradarloop import BOMRadarLoop import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -119,7 +120,6 @@ class BOMRadarCam(Camera): def __init__(self, name, location, radar_id, delta, frames, outfile): """Initialize the component.""" - from bomradarloop import BOMRadarLoop super().__init__() self._name = name From 56dde68c5baab86bf084eedd6d3d23c7e1997a36 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 17:14:01 +0200 Subject: [PATCH 399/639] Move imports in bmw_connected_drive component (#27853) --- homeassistant/components/bmw_connected_drive/__init__.py | 8 ++++---- .../components/bmw_connected_drive/binary_sensor.py | 4 ++-- homeassistant/components/bmw_connected_drive/lock.py | 3 ++- homeassistant/components/bmw_connected_drive/sensor.py | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index 8e67da86dc3..455d821e669 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -1,12 +1,14 @@ """Reads vehicle status from BMW connected drive portal.""" import logging +from bimmer_connected.account import ConnectedDriveAccount +from bimmer_connected.country_selector import get_region_from_name import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import discovery -from homeassistant.helpers.event import track_utc_time_change import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_utc_time_change import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -118,8 +120,6 @@ class BMWConnectedDriveAccount: self, username: str, password: str, region_str: str, name: str, read_only ) -> None: """Constructor.""" - from bimmer_connected.account import ConnectedDriveAccount - from bimmer_connected.country_selector import get_region_from_name region = get_region_from_name(region_str) diff --git a/homeassistant/components/bmw_connected_drive/binary_sensor.py b/homeassistant/components/bmw_connected_drive/binary_sensor.py index c13de455984..8163ae4eae3 100644 --- a/homeassistant/components/bmw_connected_drive/binary_sensor.py +++ b/homeassistant/components/bmw_connected_drive/binary_sensor.py @@ -1,6 +1,8 @@ """Reads vehicle status from BMW connected drive portal.""" import logging +from bimmer_connected.state import ChargingState, LockState + from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import LENGTH_KILOMETERS @@ -141,8 +143,6 @@ class BMWConnectedDriveSensor(BinarySensorDevice): def update(self): """Read new state data from the library.""" - from bimmer_connected.state import LockState - from bimmer_connected.state import ChargingState vehicle_state = self._vehicle.state diff --git a/homeassistant/components/bmw_connected_drive/lock.py b/homeassistant/components/bmw_connected_drive/lock.py index 2055b442dcd..5323e94c1c3 100644 --- a/homeassistant/components/bmw_connected_drive/lock.py +++ b/homeassistant/components/bmw_connected_drive/lock.py @@ -1,6 +1,8 @@ """Support for BMW car locks with BMW ConnectedDrive.""" import logging +from bimmer_connected.state import LockState + from homeassistant.components.lock import LockDevice from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED @@ -87,7 +89,6 @@ class BMWLock(LockDevice): def update(self): """Update state of the lock.""" - from bimmer_connected.state import LockState _LOGGER.debug("%s: updating data for %s", self._vehicle.name, self._attribute) vehicle_state = self._vehicle.state diff --git a/homeassistant/components/bmw_connected_drive/sensor.py b/homeassistant/components/bmw_connected_drive/sensor.py index 28a4e853f2c..f919bba6b95 100644 --- a/homeassistant/components/bmw_connected_drive/sensor.py +++ b/homeassistant/components/bmw_connected_drive/sensor.py @@ -1,6 +1,8 @@ """Support for reading vehicle status from BMW connected drive portal.""" import logging +from bimmer_connected.state import ChargingState + from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, LENGTH_KILOMETERS, @@ -97,7 +99,6 @@ class BMWConnectedDriveSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - from bimmer_connected.state import ChargingState vehicle_state = self._vehicle.state charging_state = vehicle_state.charging_status in [ChargingState.CHARGING] From bb76678b36b8a0830581eebeddc0916331b277c5 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 17:14:50 +0200 Subject: [PATCH 400/639] Move imports in blink component (#27850) --- homeassistant/components/blink/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index bd11572ba1c..e233a8b21d8 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -1,22 +1,24 @@ """Support for Blink Home Camera System.""" -import logging from datetime import timedelta +import logging + +from blinkpy import blinkpy import voluptuous as vol -from homeassistant.helpers import config_validation as cv, discovery from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_NAME, - CONF_SCAN_INTERVAL, CONF_BINARY_SENSORS, - CONF_SENSORS, CONF_FILENAME, - CONF_MONITORED_CONDITIONS, CONF_MODE, + CONF_MONITORED_CONDITIONS, + CONF_NAME, CONF_OFFSET, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_SENSORS, + CONF_USERNAME, TEMP_FAHRENHEIT, ) +from homeassistant.helpers import config_validation as cv, discovery _LOGGER = logging.getLogger(__name__) @@ -97,7 +99,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up Blink System.""" - from blinkpy import blinkpy conf = config[BLINK_DATA] username = conf[CONF_USERNAME] From e95b8035ed373f6979d7fb48536ac7a9a251f788 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 17:15:20 +0200 Subject: [PATCH 401/639] Move imports in blinksticklight component (#27851) --- homeassistant/components/blinksticklight/light.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/blinksticklight/light.py b/homeassistant/components/blinksticklight/light.py index 5f3cb7ebfd1..197213f7473 100644 --- a/homeassistant/components/blinksticklight/light.py +++ b/homeassistant/components/blinksticklight/light.py @@ -1,15 +1,16 @@ """Support for Blinkstick lights.""" import logging +from blinkstick import blinkstick import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv @@ -33,7 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Blinkstick device specified by serial number.""" - from blinkstick import blinkstick name = config.get(CONF_NAME) serial = config.get(CONF_SERIAL) From 83a709b768b88389f0077ca22aa7a445c5babaac Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Sat, 19 Oct 2019 04:14:54 +1100 Subject: [PATCH 402/639] Move imports in recorder component (#27859) * move imports to top-level in recorder init * move imports to top-level in recorder migration * move imports to top-level in recorder models * move imports to top-level in recorder purge * move imports to top-level in recorder util * fix pylint --- homeassistant/components/recorder/__init__.py | 32 +++++++------------ .../components/recorder/migration.py | 22 ++++--------- homeassistant/components/recorder/models.py | 3 +- homeassistant/components/recorder/purge.py | 6 ++-- homeassistant/components/recorder/util.py | 8 ++--- tests/components/recorder/test_migrate.py | 6 ++-- 6 files changed, 27 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index b36e0a34fa4..10b1d04304f 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -8,8 +8,14 @@ import queue import threading import time from typing import Any, Dict, Optional +from sqlite3 import Connection import voluptuous as vol +from sqlalchemy import exc, create_engine +from sqlalchemy.engine import Engine +from sqlalchemy.event import listens_for +from sqlalchemy.orm import scoped_session, sessionmaker +from sqlalchemy.pool import StaticPool from homeassistant.const import ( ATTR_ENTITY_ID, @@ -23,6 +29,7 @@ from homeassistant.const import ( EVENT_TIME_CHANGED, MATCH_ALL, ) +from homeassistant.components import persistent_notification from homeassistant.core import CoreState, HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entityfilter import generate_filter @@ -31,6 +38,7 @@ import homeassistant.util.dt as dt_util from . import migration, purge from .const import DATA_INSTANCE +from .models import Base, Events, RecorderRuns, States from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -100,11 +108,9 @@ def run_information(hass, point_in_time: Optional[datetime] = None): There is also the run that covers point_in_time. """ - from . import models - ins = hass.data[DATA_INSTANCE] - recorder_runs = models.RecorderRuns + recorder_runs = RecorderRuns if point_in_time is None or point_in_time > ins.recording_start: return ins.run_info @@ -208,10 +214,6 @@ class Recorder(threading.Thread): def run(self): """Start processing events to save.""" - from .models import States, Events - from homeassistant.components import persistent_notification - from sqlalchemy import exc - tries = 1 connected = False @@ -393,18 +395,10 @@ class Recorder(threading.Thread): def _setup_connection(self): """Ensure database is ready to fly.""" - from sqlalchemy import create_engine, event - from sqlalchemy.engine import Engine - from sqlalchemy.orm import scoped_session - from sqlalchemy.orm import sessionmaker - from sqlite3 import Connection - - from . import models - kwargs = {} # pylint: disable=unused-variable - @event.listens_for(Engine, "connect") + @listens_for(Engine, "connect") def set_sqlite_pragma(dbapi_connection, connection_record): """Set sqlite's WAL mode.""" if isinstance(dbapi_connection, Connection): @@ -416,8 +410,6 @@ class Recorder(threading.Thread): dbapi_connection.isolation_level = old_isolation if self.db_url == "sqlite://" or ":memory:" in self.db_url: - from sqlalchemy.pool import StaticPool - kwargs["connect_args"] = {"check_same_thread": False} kwargs["poolclass"] = StaticPool kwargs["pool_reset_on_return"] = None @@ -428,7 +420,7 @@ class Recorder(threading.Thread): self.engine.dispose() self.engine = create_engine(self.db_url, **kwargs) - models.Base.metadata.create_all(self.engine) + Base.metadata.create_all(self.engine) self.get_session = scoped_session(sessionmaker(bind=self.engine)) def _close_connection(self): @@ -439,8 +431,6 @@ class Recorder(threading.Thread): def _setup_run(self): """Log the start of the current run.""" - from .models import RecorderRuns - with session_scope(session=self.get_session()) as session: for run in session.query(RecorderRuns).filter_by(end=None): run.closed_incorrect = True diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index 3de0430d8f3..33a01ea1ac0 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -2,6 +2,11 @@ import logging import os +from sqlalchemy import Table, text +from sqlalchemy.engine import reflection +from sqlalchemy.exc import OperationalError, SQLAlchemyError + +from .models import SchemaChanges, SCHEMA_VERSION, Base from .util import session_scope _LOGGER = logging.getLogger(__name__) @@ -10,8 +15,6 @@ PROGRESS_FILE = ".migration_progress" def migrate_schema(instance): """Check if the schema needs to be upgraded.""" - from .models import SchemaChanges, SCHEMA_VERSION - progress_path = instance.hass.config.path(PROGRESS_FILE) with session_scope(session=instance.get_session()) as session: @@ -60,11 +63,7 @@ def _create_index(engine, table_name, index_name): The index name should match the name given for the index within the table definition described in the models """ - from sqlalchemy import Table - from sqlalchemy.exc import OperationalError - from . import models - - table = Table(table_name, models.Base.metadata) + table = Table(table_name, Base.metadata) _LOGGER.debug("Looking up index for table %s", table_name) # Look up the index object by name from the table is the models index = next(idx for idx in table.indexes if idx.name == index_name) @@ -99,9 +98,6 @@ def _drop_index(engine, table_name, index_name): string here is generated from the method parameters without sanitizing. DO NOT USE THIS FUNCTION IN ANY OPERATION THAT TAKES USER INPUT. """ - from sqlalchemy import text - from sqlalchemy.exc import SQLAlchemyError - _LOGGER.debug("Dropping index %s from table %s", index_name, table_name) success = False @@ -159,9 +155,6 @@ def _drop_index(engine, table_name, index_name): def _add_columns(engine, table_name, columns_def): """Add columns to a table.""" - from sqlalchemy import text - from sqlalchemy.exc import OperationalError - _LOGGER.info( "Adding columns %s to table %s. Note: this can take several " "minutes on large databases and slow computers. Please " @@ -277,9 +270,6 @@ def _inspect_schema_version(engine, session): version 1 are present to make the determination. Eventually this logic can be removed and we can assume a new db is being created. """ - from sqlalchemy.engine import reflection - from .models import SchemaChanges, SCHEMA_VERSION - inspector = reflection.Inspector.from_engine(engine) indexes = inspector.get_indexes("events") diff --git a/homeassistant/components/recorder/models.py b/homeassistant/components/recorder/models.py index 12f4a9065af..b512bfc8204 100644 --- a/homeassistant/components/recorder/models.py +++ b/homeassistant/components/recorder/models.py @@ -15,6 +15,7 @@ from sqlalchemy import ( distinct, ) from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm.session import Session import homeassistant.util.dt as dt_util from homeassistant.core import Context, Event, EventOrigin, State, split_entity_id @@ -164,8 +165,6 @@ class RecorderRuns(Base): # type: ignore Specify point_in_time if you want to know which existed at that point in time inside the run. """ - from sqlalchemy.orm.session import Session - session = Session.object_session(self) assert session is not None, "RecorderRuns need to be persisted" diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 81426d65f06..2ac0b38c694 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -2,7 +2,10 @@ from datetime import timedelta import logging +from sqlalchemy.exc import SQLAlchemyError + import homeassistant.util.dt as dt_util +from .models import Events, States from .util import session_scope @@ -11,9 +14,6 @@ _LOGGER = logging.getLogger(__name__) def purge_old_data(instance, purge_days, repack): """Purge events and states older than purge_days ago.""" - from .models import States, Events - from sqlalchemy.exc import SQLAlchemyError - purge_before = dt_util.utcnow() - timedelta(days=purge_days) _LOGGER.debug("Purging events before %s", purge_before) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 674d687ec14..8cfcafea79d 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -3,6 +3,8 @@ from contextlib import contextmanager import logging import time +from sqlalchemy.exc import OperationalError, SQLAlchemyError + from .const import DATA_INSTANCE _LOGGER = logging.getLogger(__name__) @@ -37,8 +39,6 @@ def session_scope(*, hass=None, session=None): def commit(session, work): """Commit & retry work: Either a model or in a function.""" - import sqlalchemy.exc - for _ in range(0, RETRIES): try: if callable(work): @@ -47,7 +47,7 @@ def commit(session, work): session.add(work) session.commit() return True - except sqlalchemy.exc.OperationalError as err: + except OperationalError as err: _LOGGER.error("Error executing query: %s", err) session.rollback() time.sleep(QUERY_RETRY_WAIT) @@ -59,8 +59,6 @@ def execute(qry): This method also retries a few times in the case of stale connections. """ - from sqlalchemy.exc import SQLAlchemyError - for tryno in range(0, RETRIES): try: timer_start = time.perf_counter() diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index 19b7566c37c..81e0423a723 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -23,9 +23,9 @@ def create_engine_test(*args, **kwargs): async def test_schema_update_calls(hass): """Test that schema migrations occur in correct order.""" - with patch("sqlalchemy.create_engine", new=create_engine_test), patch( - "homeassistant.components.recorder.migration._apply_update" - ) as update: + with patch( + "homeassistant.components.recorder.create_engine", new=create_engine_test + ), patch("homeassistant.components.recorder.migration._apply_update") as update: await async_setup_component( hass, "recorder", {"recorder": {"db_url": "sqlite://"}} ) From a119932ee590b9115afc97ddf56b9a95fd9f2c00 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 11:46:45 -0700 Subject: [PATCH 403/639] Refactor the conversation integration (#27839) * Refactor the conversation integration * Lint --- .../components/conversation/__init__.py | 133 ++++-------------- .../components/conversation/agent.py | 12 ++ .../components/conversation/const.py | 3 + .../components/conversation/default_agent.py | 127 +++++++++++++++++ homeassistant/components/hangouts/__init__.py | 3 +- .../components/shopping_list/__init__.py | 7 - tests/components/conversation/test_init.py | 65 +++------ tests/components/conversation/test_util.py | 55 ++++++++ 8 files changed, 242 insertions(+), 163 deletions(-) create mode 100644 homeassistant/components/conversation/agent.py create mode 100644 homeassistant/components/conversation/const.py create mode 100644 homeassistant/components/conversation/default_agent.py create mode 100644 tests/components/conversation/test_util.py diff --git a/homeassistant/components/conversation/__init__.py b/homeassistant/components/conversation/__init__.py index 9d7d510b10e..798fc926e0f 100644 --- a/homeassistant/components/conversation/__init__.py +++ b/homeassistant/components/conversation/__init__.py @@ -6,15 +6,12 @@ import voluptuous as vol from homeassistant import core from homeassistant.components import http -from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import EVENT_COMPONENT_LOADED -from homeassistant.core import callback from homeassistant.helpers import config_validation as cv, intent from homeassistant.loader import bind_hass -from homeassistant.setup import ATTR_COMPONENT -from .util import create_matcher +from .agent import AbstractConversationAgent +from .default_agent import async_register, DefaultAgent _LOGGER = logging.getLogger(__name__) @@ -22,15 +19,8 @@ ATTR_TEXT = "text" DOMAIN = "conversation" -REGEX_TURN_COMMAND = re.compile(r"turn (?P(?: |\w)+) (?P\w+)") REGEX_TYPE = type(re.compile("")) - -UTTERANCES = { - "cover": { - INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"], - INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"], - } -} +DATA_AGENT = "conversation_agent" SERVICE_PROCESS = "process" @@ -50,137 +40,64 @@ CONFIG_SCHEMA = vol.Schema( ) +async_register = bind_hass(async_register) # pylint: disable=invalid-name + + @core.callback @bind_hass -def async_register(hass, intent_type, utterances): - """Register utterances and any custom intents. - - Registrations don't require conversations to be loaded. They will become - active once the conversation component is loaded. - """ - intents = hass.data.get(DOMAIN) - - if intents is None: - intents = hass.data[DOMAIN] = {} - - conf = intents.get(intent_type) - - if conf is None: - conf = intents[intent_type] = [] - - for utterance in utterances: - if isinstance(utterance, REGEX_TYPE): - conf.append(utterance) - else: - conf.append(create_matcher(utterance)) +def async_set_agent(hass: core.HomeAssistant, agent: AbstractConversationAgent): + """Set the agent to handle the conversations.""" + hass.data[DATA_AGENT] = agent async def async_setup(hass, config): """Register the process service.""" - config = config.get(DOMAIN, {}) - intents = hass.data.get(DOMAIN) - if intents is None: - intents = hass.data[DOMAIN] = {} + async def process(hass, text): + """Process a line of text.""" + agent = hass.data.get(DATA_AGENT) - for intent_type, utterances in config.get("intents", {}).items(): - conf = intents.get(intent_type) + if agent is None: + agent = hass.data[DATA_AGENT] = DefaultAgent(hass) + await agent.async_initialize(config) - if conf is None: - conf = intents[intent_type] = [] + return await agent.async_process(text) - conf.extend(create_matcher(utterance) for utterance in utterances) - - async def process(service): + async def handle_service(service): """Parse text into commands.""" text = service.data[ATTR_TEXT] _LOGGER.debug("Processing: <%s>", text) try: - await _process(hass, text) + await process(hass, text) except intent.IntentHandleError as err: _LOGGER.error("Error processing %s: %s", text, err) hass.services.async_register( - DOMAIN, SERVICE_PROCESS, process, schema=SERVICE_PROCESS_SCHEMA + DOMAIN, SERVICE_PROCESS, handle_service, schema=SERVICE_PROCESS_SCHEMA ) - hass.http.register_view(ConversationProcessView) - - # We strip trailing 's' from name because our state matcher will fail - # if a letter is not there. By removing 's' we can match singular and - # plural names. - - async_register( - hass, - intent.INTENT_TURN_ON, - ["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"], - ) - async_register( - hass, - intent.INTENT_TURN_OFF, - ["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"], - ) - async_register( - hass, - intent.INTENT_TOGGLE, - ["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"], - ) - - @callback - def register_utterances(component): - """Register utterances for a component.""" - if component not in UTTERANCES: - return - for intent_type, sentences in UTTERANCES[component].items(): - async_register(hass, intent_type, sentences) - - @callback - def component_loaded(event): - """Handle a new component loaded.""" - register_utterances(event.data[ATTR_COMPONENT]) - - hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) - - # Check already loaded components. - for component in hass.config.components: - register_utterances(component) + hass.http.register_view(ConversationProcessView(process)) return True -async def _process(hass, text): - """Process a line of text.""" - intents = hass.data.get(DOMAIN, {}) - - for intent_type, matchers in intents.items(): - for matcher in matchers: - match = matcher.match(text) - - if not match: - continue - - response = await hass.helpers.intent.async_handle( - DOMAIN, - intent_type, - {key: {"value": value} for key, value in match.groupdict().items()}, - text, - ) - return response - - class ConversationProcessView(http.HomeAssistantView): """View to retrieve shopping list content.""" url = "/api/conversation/process" name = "api:conversation:process" + def __init__(self, process): + """Initialize the conversation process view.""" + self._process = process + @RequestDataValidator(vol.Schema({vol.Required("text"): str})) async def post(self, request, data): """Send a request for processing.""" hass = request.app["hass"] try: - intent_result = await _process(hass, data["text"]) + intent_result = await self._process(hass, data["text"]) except intent.IntentHandleError as err: intent_result = intent.IntentResponse() intent_result.async_set_speech(str(err)) diff --git a/homeassistant/components/conversation/agent.py b/homeassistant/components/conversation/agent.py new file mode 100644 index 00000000000..eae6402530c --- /dev/null +++ b/homeassistant/components/conversation/agent.py @@ -0,0 +1,12 @@ +"""Agent foundation for conversation integration.""" +from abc import ABC, abstractmethod + +from homeassistant.helpers import intent + + +class AbstractConversationAgent(ABC): + """Abstract conversation agent.""" + + @abstractmethod + async def async_process(self, text: str) -> intent.IntentResponse: + """Process a sentence.""" diff --git a/homeassistant/components/conversation/const.py b/homeassistant/components/conversation/const.py new file mode 100644 index 00000000000..04bfa373061 --- /dev/null +++ b/homeassistant/components/conversation/const.py @@ -0,0 +1,3 @@ +"""Const for conversation integration.""" + +DOMAIN = "conversation" diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py new file mode 100644 index 00000000000..e93afcfaf65 --- /dev/null +++ b/homeassistant/components/conversation/default_agent.py @@ -0,0 +1,127 @@ +"""Standard conversastion implementation for Home Assistant.""" +import logging +import re + +from homeassistant import core +from homeassistant.components.cover import INTENT_CLOSE_COVER, INTENT_OPEN_COVER +from homeassistant.components.shopping_list import INTENT_ADD_ITEM, INTENT_LAST_ITEMS +from homeassistant.const import EVENT_COMPONENT_LOADED +from homeassistant.core import callback +from homeassistant.helpers import intent +from homeassistant.setup import ATTR_COMPONENT + +from .agent import AbstractConversationAgent +from .const import DOMAIN +from .util import create_matcher + +_LOGGER = logging.getLogger(__name__) + +REGEX_TURN_COMMAND = re.compile(r"turn (?P(?: |\w)+) (?P\w+)") +REGEX_TYPE = type(re.compile("")) + +UTTERANCES = { + "cover": { + INTENT_OPEN_COVER: ["Open [the] [a] [an] {name}[s]"], + INTENT_CLOSE_COVER: ["Close [the] [a] [an] {name}[s]"], + }, + "shopping_list": { + INTENT_ADD_ITEM: ["Add [the] [a] [an] {item} to my shopping list"], + INTENT_LAST_ITEMS: ["What is on my shopping list"], + }, +} + + +@core.callback +def async_register(hass, intent_type, utterances): + """Register utterances and any custom intents for the default agent. + + Registrations don't require conversations to be loaded. They will become + active once the conversation component is loaded. + """ + intents = hass.data.setdefault(DOMAIN, {}) + conf = intents.setdefault(intent_type, []) + + for utterance in utterances: + if isinstance(utterance, REGEX_TYPE): + conf.append(utterance) + else: + conf.append(create_matcher(utterance)) + + +class DefaultAgent(AbstractConversationAgent): + """Default agent for conversation agent.""" + + def __init__(self, hass: core.HomeAssistant): + """Initialize the default agent.""" + self.hass = hass + + async def async_initialize(self, config): + """Initialize the default agent.""" + config = config.get(DOMAIN, {}) + intents = self.hass.data.setdefault(DOMAIN, {}) + + for intent_type, utterances in config.get("intents", {}).items(): + conf = intents.get(intent_type) + + if conf is None: + conf = intents[intent_type] = [] + + conf.extend(create_matcher(utterance) for utterance in utterances) + + # We strip trailing 's' from name because our state matcher will fail + # if a letter is not there. By removing 's' we can match singular and + # plural names. + + async_register( + self.hass, + intent.INTENT_TURN_ON, + ["Turn [the] [a] {name}[s] on", "Turn on [the] [a] [an] {name}[s]"], + ) + async_register( + self.hass, + intent.INTENT_TURN_OFF, + ["Turn [the] [a] [an] {name}[s] off", "Turn off [the] [a] [an] {name}[s]"], + ) + async_register( + self.hass, + intent.INTENT_TOGGLE, + ["Toggle [the] [a] [an] {name}[s]", "[the] [a] [an] {name}[s] toggle"], + ) + + @callback + def component_loaded(event): + """Handle a new component loaded.""" + self.register_utterances(event.data[ATTR_COMPONENT]) + + self.hass.bus.async_listen(EVENT_COMPONENT_LOADED, component_loaded) + + # Check already loaded components. + for component in self.hass.config.components: + self.register_utterances(component) + + @callback + def register_utterances(self, component): + """Register utterances for a component.""" + if component not in UTTERANCES: + return + for intent_type, sentences in UTTERANCES[component].items(): + async_register(self.hass, intent_type, sentences) + + async def async_process(self, text) -> intent.IntentResponse: + """Process a sentence.""" + intents = self.hass.data[DOMAIN] + + for intent_type, matchers in intents.items(): + for matcher in matchers: + match = matcher.match(text) + + if not match: + continue + + return await intent.async_handle( + self.hass, + DOMAIN, + intent_type, + {key: {"value": value} for key, value in match.groupdict().items()}, + text, + ) diff --git a/homeassistant/components/hangouts/__init__.py b/homeassistant/components/hangouts/__init__.py index 885ac5d1670..953994d6ac0 100644 --- a/homeassistant/components/hangouts/__init__.py +++ b/homeassistant/components/hangouts/__init__.py @@ -7,6 +7,7 @@ from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import dispatcher, intent import homeassistant.helpers.config_validation as cv +from homeassistant.components.conversation.util import create_matcher # We need an import from .config_flow, without it .config_flow is never loaded. from .intents import HelpIntent @@ -54,8 +55,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Hangouts bot component.""" - from homeassistant.components.conversation import create_matcher - config = config.get(DOMAIN) if config is None: hass.data[DOMAIN] = { diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 3c9cb4391a7..a5e901b8c6e 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -101,13 +101,6 @@ def async_setup(hass, config): hass.http.register_view(UpdateShoppingListItemView) hass.http.register_view(ClearCompletedItemsView) - hass.components.conversation.async_register( - INTENT_ADD_ITEM, ["Add [the] [a] [an] {item} to my shopping list"] - ) - hass.components.conversation.async_register( - INTENT_LAST_ITEMS, ["What is on my shopping list"] - ) - hass.components.frontend.async_register_built_in_panel( "shopping-list", "shopping_list", "mdi:cart" ) diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index d4142f2ce5f..a9116ac0d98 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -263,54 +263,27 @@ async def test_http_api_wrong_data(hass, hass_client): assert resp.status == 400 -def test_create_matcher(): - """Test the create matcher method.""" - # Basic sentence - pattern = conversation.create_matcher("Hello world") - assert pattern.match("Hello world") is not None +async def test_custom_agent(hass, hass_client): + """Test a custom conversation agent.""" - # Match a part - pattern = conversation.create_matcher("Hello {name}") - match = pattern.match("hello world") - assert match is not None - assert match.groupdict()["name"] == "world" - no_match = pattern.match("Hello world, how are you?") - assert no_match is None + class MyAgent(conversation.AbstractConversationAgent): + """Test Agent.""" - # Optional and matching part - pattern = conversation.create_matcher("Turn on [the] {name}") - match = pattern.match("turn on the kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn on kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn off kitchen lights") - assert match is None + async def async_process(self, text): + """Process some text.""" + response = intent.IntentResponse() + response.async_set_speech("Test response") + return response - # Two different optional parts, 1 matching part - pattern = conversation.create_matcher("Turn on [the] [a] {name}") - match = pattern.match("turn on the kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn on kitchen lights") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn on a kitchen light") - assert match is not None - assert match.groupdict()["name"] == "kitchen light" + conversation.async_set_agent(hass, MyAgent()) - # Strip plural - pattern = conversation.create_matcher("Turn {name}[s] on") - match = pattern.match("turn kitchen lights on") - assert match is not None - assert match.groupdict()["name"] == "kitchen light" + assert await async_setup_component(hass, "conversation", {}) - # Optional 2 words - pattern = conversation.create_matcher("Turn [the great] {name} on") - match = pattern.match("turn the great kitchen lights on") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" - match = pattern.match("turn kitchen lights on") - assert match is not None - assert match.groupdict()["name"] == "kitchen lights" + client = await hass_client() + + resp = await client.post("/api/conversation/process", json={"text": "Test Text"}) + assert resp.status == 200 + assert await resp.json() == { + "card": {}, + "speech": {"plain": {"extra_data": None, "speech": "Test response"}}, + } diff --git a/tests/components/conversation/test_util.py b/tests/components/conversation/test_util.py new file mode 100644 index 00000000000..2fa4527e9b1 --- /dev/null +++ b/tests/components/conversation/test_util.py @@ -0,0 +1,55 @@ +"""Test the conversation utils.""" +from homeassistant.components.conversation.util import create_matcher + + +def test_create_matcher(): + """Test the create matcher method.""" + # Basic sentence + pattern = create_matcher("Hello world") + assert pattern.match("Hello world") is not None + + # Match a part + pattern = create_matcher("Hello {name}") + match = pattern.match("hello world") + assert match is not None + assert match.groupdict()["name"] == "world" + no_match = pattern.match("Hello world, how are you?") + assert no_match is None + + # Optional and matching part + pattern = create_matcher("Turn on [the] {name}") + match = pattern.match("turn on the kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn on kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn off kitchen lights") + assert match is None + + # Two different optional parts, 1 matching part + pattern = create_matcher("Turn on [the] [a] {name}") + match = pattern.match("turn on the kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn on kitchen lights") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn on a kitchen light") + assert match is not None + assert match.groupdict()["name"] == "kitchen light" + + # Strip plural + pattern = create_matcher("Turn {name}[s] on") + match = pattern.match("turn kitchen lights on") + assert match is not None + assert match.groupdict()["name"] == "kitchen light" + + # Optional 2 words + pattern = create_matcher("Turn [the great] {name} on") + match = pattern.match("turn the great kitchen lights on") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" + match = pattern.match("turn kitchen lights on") + assert match is not None + assert match.groupdict()["name"] == "kitchen lights" From 103ffacea76179bb63365a12366c725a4674507f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Fri, 18 Oct 2019 22:20:27 +0300 Subject: [PATCH 404/639] Use pre-commit in CI and tox (#27743) --- .pre-commit-config.yaml | 4 +++- .travis.yml | 5 ++++- azure-pipelines-ci.yml | 9 ++++++--- tox.ini | 4 ++-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3773a3213aa..268cff9ea78 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,6 +6,7 @@ repos: args: - --safe - --quiet + files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.8 hooks: @@ -13,6 +14,7 @@ repos: additional_dependencies: - flake8-docstrings==1.3.1 - pydocstyle==4.0.0 + files: ^(homeassistant|script|tests)/.+\.py$ # Using a local "system" mypy instead of the mypy hook, because its # results depend on what is installed. And the mypy hook runs in a # virtualenv of its own, meaning we'd need to install and maintain @@ -26,4 +28,4 @@ repos: language: system types: [python] require_serial: true - exclude: ^script/scaffold/templates/ + files: ^homeassistant/.+\.py$ diff --git a/.travis.yml b/.travis.yml index 0e9e030128e..7b3765716eb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,7 +27,10 @@ matrix: - python: "3.7" env: TOXENV=py37 -cache: pip +cache: + pip: true + directories: + - $HOME/.cache/pre-commit install: pip install -U tox language: python script: travis_wait 50 tox --develop diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 257eac57c2d..f03c5f435f9 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -45,9 +45,10 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + pre-commit install-hooks - script: | . venv/bin/activate - flake8 homeassistant tests script + pre-commit run flake8 --all-files displayName: 'Run flake8' - job: 'Validate' pool: @@ -83,9 +84,10 @@ stages: . venv/bin/activate pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + pre-commit install-hooks - script: | . venv/bin/activate - ./script/check_format + pre-commit run black --all-files displayName: 'Check Black formatting' - stage: 'Tests' @@ -180,7 +182,8 @@ stages: . venv/bin/activate pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt + pre-commit install-hooks - script: | . venv/bin/activate - mypy homeassistant + pre-commit run mypy --all-files displayName: 'Run mypy' diff --git a/tox.ini b/tox.ini index 8c3563dac83..0b0c969d781 100644 --- a/tox.ini +++ b/tox.ini @@ -34,11 +34,11 @@ deps = commands = python -m script.gen_requirements_all validate python -m script.hassfest validate - flake8 {posargs: homeassistant tests script} + pre-commit run flake8 {posargs: --all-files} [testenv:typing] deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - mypy homeassistant + pre-commit run mypy {posargs: --all-files} From 6157be23dce8857963150d8f162978d967f22bd2 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 21:32:14 +0200 Subject: [PATCH 405/639] Move imports in cloudflare integration(#27882) --- homeassistant/components/cloudflare/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cloudflare/__init__.py b/homeassistant/components/cloudflare/__init__.py index 26feff069da..265621b6250 100644 --- a/homeassistant/components/cloudflare/__init__.py +++ b/homeassistant/components/cloudflare/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +from pycfdns import CloudflareUpdater import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_EMAIL, CONF_ZONE @@ -33,7 +34,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Cloudflare component.""" - from pycfdns import CloudflareUpdater cfupdate = CloudflareUpdater() email = config[DOMAIN][CONF_EMAIL] From b6c26cb363adf9630607948643f4af4f612d3329 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 13:06:33 -0700 Subject: [PATCH 406/639] Introduce new OAuth2 config flow helper (#27727) * Refactor the Somfy auth implementation * Typing * Migrate Somfy to OAuth2 flow helper * Add tests * Add more tests * Fix tests * Fix type error * More tests * Remove side effect from constructor * implementation -> auth_implementation * Make get_implementation async * Minor cleanup + Allow picking implementations. * Add support for extra authorize data --- homeassistant/bootstrap.py | 2 +- homeassistant/components/somfy/__init__.py | 63 +-- homeassistant/components/somfy/api.py | 55 +++ homeassistant/components/somfy/config_flow.py | 135 +----- homeassistant/components/somfy/const.py | 2 - homeassistant/components/somfy/manifest.json | 12 +- homeassistant/config_entries.py | 2 +- homeassistant/core.py | 9 +- homeassistant/data_entry_flow.py | 2 +- .../helpers/config_entry_oauth2_flow.py | 420 ++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/common.py | 17 +- tests/components/somfy/test_config_flow.py | 125 ++++-- .../helpers/test_config_entry_oauth2_flow.py | 266 +++++++++++ 15 files changed, 900 insertions(+), 214 deletions(-) create mode 100644 homeassistant/components/somfy/api.py create mode 100644 homeassistant/helpers/config_entry_oauth2_flow.py create mode 100644 tests/helpers/test_config_entry_oauth2_flow.py diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index e399205ec70..6118f4f2bd7 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -260,7 +260,7 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]: domains = set(key.split(" ")[0] for key in config.keys() if key != core.DOMAIN) # Add config entry domains - domains.update(hass.config_entries.async_domains()) # type: ignore + domains.update(hass.config_entries.async_domains()) # Make sure the Hass.io component is loaded if "HASSIO" in os.environ: diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 2c7c71d7a69..cd5960bf6b1 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -4,21 +4,21 @@ Support for Somfy hubs. For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/somfy/ """ +import asyncio import logging from datetime import timedelta -from functools import partial import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant import config_entries +from homeassistant.helpers import config_validation as cv, config_entry_oauth2_flow from homeassistant.components.somfy import config_flow from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TOKEN from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import Throttle +from . import api + API = "api" DEVICES = "devices" @@ -52,19 +52,21 @@ SOMFY_COMPONENTS = ["cover"] async def async_setup(hass, config): """Set up the Somfy component.""" + hass.data[DOMAIN] = {} + if DOMAIN not in config: return True - hass.data[DOMAIN] = {} - - config_flow.register_flow_implementation( - hass, config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET] - ) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) + config_flow.SomfyFlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + "https://accounts.somfy.com/oauth/oauth/v2/auth", + "https://accounts.somfy.com/oauth/oauth/v2/token", + ), ) return True @@ -72,25 +74,18 @@ async def async_setup(hass, config): async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): """Set up Somfy from a config entry.""" - - def token_saver(token): - _LOGGER.debug("Saving updated token") - entry.data[CONF_TOKEN] = token - update_entry = partial( - hass.config_entries.async_update_entry, data={**entry.data} + # Backwards compat + if "auth_implementation" not in entry.data: + hass.config_entries.async_update_entry( + entry, data={**entry.data, "auth_implementation": DOMAIN} ) - hass.add_job(update_entry, entry) - # Force token update. - from pymfy.api.somfy_api import SomfyApi - - hass.data[DOMAIN][API] = SomfyApi( - entry.data["refresh_args"]["client_id"], - entry.data["refresh_args"]["client_secret"], - token=entry.data[CONF_TOKEN], - token_updater=token_saver, + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry ) + hass.data[DOMAIN][API] = api.ConfigEntrySomfyApi(hass, entry, implementation) + await update_all_devices(hass) for component in SOMFY_COMPONENTS: @@ -104,16 +99,22 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload a config entry.""" hass.data[DOMAIN].pop(API, None) + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in SOMFY_COMPONENTS + ] + ) return True class SomfyEntity(Entity): """Representation of a generic Somfy device.""" - def __init__(self, device, api): + def __init__(self, device, somfy_api): """Initialize the Somfy device.""" self.device = device - self.api = api + self.api = somfy_api @property def unique_id(self): diff --git a/homeassistant/components/somfy/api.py b/homeassistant/components/somfy/api.py new file mode 100644 index 00000000000..3e7bcf9deb4 --- /dev/null +++ b/homeassistant/components/somfy/api.py @@ -0,0 +1,55 @@ +"""API for Somfy bound to HASS OAuth.""" +from asyncio import run_coroutine_threadsafe +from functools import partial + +import requests +from pymfy.api import somfy_api + +from homeassistant import core, config_entries +from homeassistant.helpers import config_entry_oauth2_flow + + +class ConfigEntrySomfyApi(somfy_api.AbstractSomfyApi): + """Provide a Somfy API tied into an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize the Config Entry Somfy API.""" + self.hass = hass + self.config_entry = config_entry + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + + def get(self, path): + """Fetch a URL from the Somfy API.""" + return run_coroutine_threadsafe( + self._request("get", path), self.hass.loop + ).result() + + def post(self, path, *, json): + """Post data to the Somfy API.""" + return run_coroutine_threadsafe( + self._request("post", path, json=json), self.hass.loop + ).result() + + async def _request(self, method, path, **kwargs): + """Make a request.""" + await self.session.async_ensure_token_valid() + + return await self.hass.async_add_executor_job( + partial( + requests.request, + method, + f"{self.base_url}{path}", + **kwargs, + headers={ + **kwargs.get("headers", {}), + "authorization": f"Bearer {self.config_entry.data['token']['access_token']}", + }, + ) + ) diff --git a/homeassistant/components/somfy/config_flow.py b/homeassistant/components/somfy/config_flow.py index 9f3c58c8ffb..cb180d4e247 100644 --- a/homeassistant/components/somfy/config_flow.py +++ b/homeassistant/components/somfy/config_flow.py @@ -1,141 +1,28 @@ """Config flow for Somfy.""" -import asyncio import logging -import async_timeout - from homeassistant import config_entries -from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback -from .const import CLIENT_ID, CLIENT_SECRET, DOMAIN - -AUTH_CALLBACK_PATH = "/auth/somfy/callback" -AUTH_CALLBACK_NAME = "auth:somfy:callback" +from homeassistant.helpers import config_entry_oauth2_flow +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -@callback -def register_flow_implementation(hass, client_id, client_secret): - """Register a flow implementation. +@config_entries.HANDLERS.register(DOMAIN) +class SomfyFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Config flow to handle Somfy OAuth2 authentication.""" - client_id: Client id. - client_secret: Client secret. - """ - hass.data[DOMAIN][CLIENT_ID] = client_id - hass.data[DOMAIN][CLIENT_SECRET] = client_secret - - -@config_entries.HANDLERS.register("somfy") -class SomfyFlowHandler(config_entries.ConfigFlow): - """Handle a config flow.""" - - VERSION = 1 + DOMAIN = DOMAIN CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - def __init__(self): - """Instantiate config flow.""" - self.code = None - - async def async_step_import(self, user_input=None): - """Handle external yaml configuration.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_setup") - return await self.async_step_auth() + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) async def async_step_user(self, user_input=None): """Handle a flow start.""" if self.hass.config_entries.async_entries(DOMAIN): return self.async_abort(reason="already_setup") - if DOMAIN not in self.hass.data: - return self.async_abort(reason="missing_configuration") - - return await self.async_step_auth() - - async def async_step_auth(self, user_input=None): - """Create an entry for auth.""" - # Flow has been triggered from Somfy website - if user_input: - return await self.async_step_code(user_input) - - try: - with async_timeout.timeout(10): - url, _ = await self._get_authorization_url() - except asyncio.TimeoutError: - return self.async_abort(reason="authorize_url_timeout") - - return self.async_external_step(step_id="auth", url=url) - - async def _get_authorization_url(self): - """Get Somfy authorization url.""" - from pymfy.api.somfy_api import SomfyApi - - client_id = self.hass.data[DOMAIN][CLIENT_ID] - client_secret = self.hass.data[DOMAIN][CLIENT_SECRET] - redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" - api = SomfyApi(client_id, client_secret, redirect_uri) - - self.hass.http.register_view(SomfyAuthCallbackView()) - # Thanks to the state, we can forward the flow id to Somfy that will - # add it in the callback. - return await self.hass.async_add_executor_job( - api.get_authorization_url, self.flow_id - ) - - async def async_step_code(self, code): - """Received code for authentication.""" - self.code = code - return self.async_external_step_done(next_step_id="creation") - - async def async_step_creation(self, user_input=None): - """Create Somfy api and entries.""" - client_id = self.hass.data[DOMAIN][CLIENT_ID] - client_secret = self.hass.data[DOMAIN][CLIENT_SECRET] - code = self.code - from pymfy.api.somfy_api import SomfyApi - - redirect_uri = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" - api = SomfyApi(client_id, client_secret, redirect_uri) - token = await self.hass.async_add_executor_job(api.request_token, None, code) - _LOGGER.info("Successfully authenticated Somfy") - return self.async_create_entry( - title="Somfy", - data={ - "token": token, - "refresh_args": { - "client_id": client_id, - "client_secret": client_secret, - }, - }, - ) - - -class SomfyAuthCallbackView(HomeAssistantView): - """Somfy Authorization Callback View.""" - - requires_auth = False - url = AUTH_CALLBACK_PATH - name = AUTH_CALLBACK_NAME - - @staticmethod - async def get(request): - """Receive authorization code.""" - from aiohttp import web_response - - if "code" not in request.query or "state" not in request.query: - return web_response.Response( - text="Missing code or state parameter in " + request.url - ) - - hass = request.app["hass"] - hass.async_create_task( - hass.config_entries.flow.async_configure( - flow_id=request.query["state"], user_input=request.query["code"] - ) - ) - - return web_response.Response( - headers={"content-type": "text/html"}, - text="", - ) + return await super().async_step_user(user_input) diff --git a/homeassistant/components/somfy/const.py b/homeassistant/components/somfy/const.py index 99fafb71bff..8765e37e6d6 100644 --- a/homeassistant/components/somfy/const.py +++ b/homeassistant/components/somfy/const.py @@ -1,5 +1,3 @@ """Define constants for the Somfy component.""" DOMAIN = "somfy" -CLIENT_ID = "client_id" -CLIENT_SECRET = "client_secret" diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index 83b50684fda..a34023f76ff 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -3,11 +3,7 @@ "name": "Somfy Open API", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/somfy", - "dependencies": [], - "codeowners": [ - "@tetienne" - ], - "requirements": [ - "pymfy==0.5.2" - ] -} \ No newline at end of file + "dependencies": ["http"], + "codeowners": ["@tetienne"], + "requirements": ["pymfy==0.6.0"] +} diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 8a40cff1bd5..f8c7c7a9da1 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -337,7 +337,7 @@ class ConfigEntry: return False if result: # pylint: disable=protected-access - hass.config_entries._async_schedule_save() # type: ignore + hass.config_entries._async_schedule_save() return result except Exception: # pylint: disable=broad-except _LOGGER.exception( diff --git a/homeassistant/core.py b/homeassistant/core.py index 90d197906cb..ec11b14edaa 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -77,7 +77,8 @@ from homeassistant.util.unit_system import ( # NOQA # Typing imports that create a circular dependency # pylint: disable=using-constant-test if TYPE_CHECKING: - from homeassistant.config_entries import ConfigEntries # noqa + from homeassistant.config_entries import ConfigEntries + from homeassistant.components.http import HomeAssistantHTTP # pylint: disable=invalid-name T = TypeVar("T") @@ -162,6 +163,9 @@ class CoreState(enum.Enum): class HomeAssistant: """Root object of the Home Assistant home automation.""" + http: "HomeAssistantHTTP" = None # type: ignore + config_entries: "ConfigEntries" = None # type: ignore + def __init__(self, loop: Optional[asyncio.events.AbstractEventLoop] = None) -> None: """Initialize new Home Assistant object.""" self.loop: asyncio.events.AbstractEventLoop = (loop or asyncio.get_event_loop()) @@ -186,9 +190,6 @@ class HomeAssistant: self.data: dict = {} self.state = CoreState.not_running self.exit_code = 0 - self.config_entries: Optional[ - ConfigEntries # pylint: disable=used-before-assignment - ] = None # If not None, use to signal end-of-loop self._stopped: Optional[asyncio.Event] = None diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 0bc27498f76..c06c69d9213 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -168,7 +168,7 @@ class FlowHandler: """Handle the configuration flow of a component.""" # Set by flow manager - flow_id: Optional[str] = None + flow_id: str = None # type: ignore hass: Optional[HomeAssistant] = None handler: Optional[Hashable] = None cur_step: Optional[Dict[str, str]] = None diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py new file mode 100644 index 00000000000..043a28cac27 --- /dev/null +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -0,0 +1,420 @@ +"""Config Flow using OAuth2. + +This module exists of the following parts: + - OAuth2 config flow which supports multiple OAuth2 implementations + - OAuth2 implementation that works with local provided client ID/secret + +""" +import asyncio +from abc import ABCMeta, ABC, abstractmethod +import logging +from typing import Optional, Any, Dict, cast +import time + +import async_timeout +from aiohttp import web, client +import jwt +import voluptuous as vol +from yarl import URL + +from homeassistant.auth.util import generate_secret +from homeassistant.core import HomeAssistant, callback +from homeassistant import config_entries +from homeassistant.components.http import HomeAssistantView + +from .aiohttp_client import async_get_clientsession + + +DATA_JWT_SECRET = "oauth2_jwt_secret" +DATA_VIEW_REGISTERED = "oauth2_view_reg" +DATA_IMPLEMENTATIONS = "oauth2_impl" +AUTH_CALLBACK_PATH = "/auth/external/callback" + + +class AbstractOAuth2Implementation(ABC): + """Base class to abstract OAuth2 authentication.""" + + @property + @abstractmethod + def name(self) -> str: + """Name of the implementation.""" + + @property + @abstractmethod + def domain(self) -> str: + """Domain that is providing the implementation.""" + + @abstractmethod + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize. + + This step is called when a config flow is initialized. It should redirect the + user to the vendor website where they can authorize Home Assistant. + + The implementation is responsible to get notified when the user is authorized + and pass this to the specified config flow. Do as little work as possible once + notified. You can do the work inside async_resolve_external_data. This will + give the best UX. + + Pass external data in with: + + ```python + await hass.config_entries.flow.async_configure( + flow_id=flow_id, user_input=external_data + ) + ``` + """ + + @abstractmethod + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve external data to tokens. + + Turn the data that the implementation passed to the config flow as external + step data into tokens. These tokens will be stored as 'token' in the + config entry data. + """ + + async def async_refresh_token(self, token: dict) -> dict: + """Refresh a token and update expires info.""" + new_token = await self._async_refresh_token(token) + new_token["expires_at"] = time.time() + new_token["expires_in"] + return new_token + + @abstractmethod + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh a token.""" + + +class LocalOAuth2Implementation(AbstractOAuth2Implementation): + """Local OAuth2 implementation.""" + + def __init__( + self, + hass: HomeAssistant, + domain: str, + client_id: str, + client_secret: str, + authorize_url: str, + token_url: str, + ): + """Initialize local auth implementation.""" + self.hass = hass + self._domain = domain + self.client_id = client_id + self.client_secret = client_secret + self.authorize_url = authorize_url + self.token_url = token_url + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Configuration.yaml" + + @property + def domain(self) -> str: + """Domain providing the implementation.""" + return self._domain + + @property + def redirect_uri(self) -> str: + """Return the redirect uri.""" + return f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}" # type: ignore + + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize.""" + return str( + URL(self.authorize_url).with_query( + { + "response_type": "code", + "client_id": self.client_id, + "redirect_uri": self.redirect_uri, + "state": _encode_jwt(self.hass, {"flow_id": flow_id}), + } + ) + ) + + async def async_resolve_external_data(self, external_data: Any) -> dict: + """Resolve the authorization code to tokens.""" + return await self._token_request( + { + "grant_type": "authorization_code", + "code": external_data, + "redirect_uri": self.redirect_uri, + } + ) + + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh tokens.""" + new_token = await self._token_request( + { + "grant_type": "refresh_token", + "client_id": self.client_id, + "refresh_token": token["refresh_token"], + } + ) + return {**token, **new_token} + + async def _token_request(self, data: dict) -> dict: + """Make a token request.""" + session = async_get_clientsession(self.hass) + + data["client_id"] = self.client_id + + if self.client_secret is not None: + data["client_secret"] = self.client_secret + + resp = await session.post(self.token_url, data=data) + resp.raise_for_status() + return cast(dict, await resp.json()) + + +class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): + """Handle a config flow.""" + + DOMAIN = "" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN + + def __init__(self) -> None: + """Instantiate config flow.""" + if self.DOMAIN == "": + raise TypeError( + f"Can't instantiate class {self.__class__.__name__} without DOMAIN being set" + ) + + self.external_data: Any = None + self.flow_impl: AbstractOAuth2Implementation = None # type: ignore + + @property + @abstractmethod + def logger(self) -> logging.Logger: + """Return logger.""" + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {} + + async def async_step_pick_implementation(self, user_input: dict = None) -> dict: + """Handle a flow start.""" + assert self.hass + implementations = await async_get_implementations(self.hass, self.DOMAIN) + + if user_input is not None: + self.flow_impl = implementations[user_input["implementation"]] + return await self.async_step_auth() + + if not implementations: + return self.async_abort(reason="missing_configuration") + + if len(implementations) == 1: + # Pick first implementation as we have only one. + self.flow_impl = list(implementations.values())[0] + return await self.async_step_auth() + + return self.async_show_form( + step_id="pick_implementation", + data_schema=vol.Schema( + { + vol.Required( + "implementation", default=list(implementations.keys())[0] + ): vol.In({key: impl.name for key, impl in implementations.items()}) + } + ), + ) + + async def async_step_auth(self, user_input: dict = None) -> dict: + """Create an entry for auth.""" + # Flow has been triggered by external data + if user_input: + self.external_data = user_input + return self.async_external_step_done(next_step_id="creation") + + try: + with async_timeout.timeout(10): + url = await self.flow_impl.async_generate_authorize_url(self.flow_id) + except asyncio.TimeoutError: + return self.async_abort(reason="authorize_url_timeout") + + url = str(URL(url).update_query(self.extra_authorize_data)) + + return self.async_external_step(step_id="auth", url=url) + + async def async_step_creation(self, user_input: dict = None) -> dict: + """Create config entry from external data.""" + token = await self.flow_impl.async_resolve_external_data(self.external_data) + token["expires_at"] = time.time() + token["expires_in"] + + self.logger.info("Successfully authenticated") + + return await self.async_oauth_create_entry( + {"auth_implementation": self.flow_impl.domain, "token": token} + ) + + async def async_oauth_create_entry(self, data: dict) -> dict: + """Create an entry for the flow. + + Ok to override if you want to fetch extra info or even add another step. + """ + return self.async_create_entry(title=self.flow_impl.name, data=data) + + async_step_user = async_step_pick_implementation + async_step_ssdp = async_step_pick_implementation + async_step_zeroconf = async_step_pick_implementation + async_step_homekit = async_step_pick_implementation + + @classmethod + def async_register_implementation( + cls, hass: HomeAssistant, local_impl: LocalOAuth2Implementation + ) -> None: + """Register a local implementation.""" + async_register_implementation(hass, cls.DOMAIN, local_impl) + + +@callback +def async_register_implementation( + hass: HomeAssistant, domain: str, implementation: AbstractOAuth2Implementation +) -> None: + """Register an OAuth2 flow implementation for an integration.""" + if isinstance(implementation, LocalOAuth2Implementation) and not hass.data.get( + DATA_VIEW_REGISTERED, False + ): + hass.http.register_view(OAuth2AuthorizeCallbackView()) # type: ignore + hass.data[DATA_VIEW_REGISTERED] = True + + implementations = hass.data.setdefault(DATA_IMPLEMENTATIONS, {}) + implementations.setdefault(domain, {})[implementation.domain] = implementation + + +async def async_get_implementations( + hass: HomeAssistant, domain: str +) -> Dict[str, AbstractOAuth2Implementation]: + """Return OAuth2 implementations for specified domain.""" + return cast( + Dict[str, AbstractOAuth2Implementation], + hass.data.setdefault(DATA_IMPLEMENTATIONS, {}).get(domain, {}), + ) + + +async def async_get_config_entry_implementation( + hass: HomeAssistant, config_entry: config_entries.ConfigEntry +) -> AbstractOAuth2Implementation: + """Return the implementation for this config entry.""" + implementations = await async_get_implementations(hass, config_entry.domain) + implementation = implementations.get(config_entry.data["auth_implementation"]) + + if implementation is None: + raise ValueError("Implementation not available") + + return implementation + + +class OAuth2AuthorizeCallbackView(HomeAssistantView): + """OAuth2 Authorization Callback View.""" + + requires_auth = False + url = AUTH_CALLBACK_PATH + name = "auth:external:callback" + + async def get(self, request: web.Request) -> web.Response: + """Receive authorization code.""" + if "code" not in request.query or "state" not in request.query: + return web.Response( + text=f"Missing code or state parameter in {request.url}" + ) + + hass = request.app["hass"] + + state = _decode_jwt(hass, request.query["state"]) + + if state is None: + return web.Response(text=f"Invalid state") + + await hass.config_entries.flow.async_configure( + flow_id=state["flow_id"], user_input=request.query["code"] + ) + + return web.Response( + headers={"content-type": "text/html"}, + text="", + ) + + +class OAuth2Session: + """Session to make requests authenticated with OAuth2.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: AbstractOAuth2Implementation, + ): + """Initialize an OAuth2 session.""" + self.hass = hass + self.config_entry = config_entry + self.implementation = implementation + + async def async_ensure_token_valid(self) -> None: + """Ensure that the current token is valid.""" + token = self.config_entry.data["token"] + + if token["expires_at"] > time.time(): + return + + new_token = await self.implementation.async_refresh_token(token) + + self.hass.config_entries.async_update_entry( # type: ignore + self.config_entry, data={**self.config_entry.data, "token": new_token} + ) + + async def async_request( + self, method: str, url: str, **kwargs: Any + ) -> client.ClientResponse: + """Make a request.""" + await self.async_ensure_token_valid() + return await async_oauth2_request( + self.hass, self.config_entry.data["token"], method, url, **kwargs + ) + + +async def async_oauth2_request( + hass: HomeAssistant, token: dict, method: str, url: str, **kwargs: Any +) -> client.ClientResponse: + """Make an OAuth2 authenticated request. + + This method will not refresh tokens. Use OAuth2 session for that. + """ + session = async_get_clientsession(hass) + + return await session.request( + method, + url, + **kwargs, + headers={ + **kwargs.get("headers", {}), + "authorization": f"Bearer {token['access_token']}", + }, + ) + + +@callback +def _encode_jwt(hass: HomeAssistant, data: dict) -> str: + """JWT encode data.""" + secret = hass.data.get(DATA_JWT_SECRET) + + if secret is None: + secret = hass.data[DATA_JWT_SECRET] = generate_secret() + + return jwt.encode(data, secret, algorithm="HS256").decode() + + +@callback +def _decode_jwt(hass: HomeAssistant, encoded: str) -> Optional[dict]: + """JWT encode data.""" + secret = cast(str, hass.data.get(DATA_JWT_SECRET)) + + try: + return jwt.decode(encoded, secret, algorithms=["HS256"]) + except jwt.InvalidTokenError: + return None diff --git a/requirements_all.txt b/requirements_all.txt index 951c9800943..58a927c81ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1304,7 +1304,7 @@ pymailgunner==1.4 pymediaroom==0.6.4 # homeassistant.components.somfy -pymfy==0.5.2 +pymfy==0.6.0 # homeassistant.components.xiaomi_tv pymitv==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c9a0013212c..24122915fb5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -447,7 +447,7 @@ pylitejet==0.1 pymailgunner==1.4 # homeassistant.components.somfy -pymfy==0.5.2 +pymfy==0.6.0 # homeassistant.components.mochad pymochad==0.2.0 diff --git a/tests/common.py b/tests/common.py index 5532e6ccb5c..f40019c5d24 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1015,14 +1015,23 @@ def mock_entity_platform(hass, platform_path, module): hue.light. """ domain, platform_name = platform_path.split(".") - integration_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) + mock_platform(hass, f"{platform_name}.{domain}", module) + + +def mock_platform(hass, platform_path, module=None): + """Mock a platform. + + platform_path is in form hue.config_flow. + """ + domain, platform_name = platform_path.split(".") + integration_cache = hass.data.setdefault(loader.DATA_INTEGRATIONS, {}) module_cache = hass.data.setdefault(loader.DATA_COMPONENTS, {}) - if platform_name not in integration_cache: - mock_integration(hass, MockModule(platform_name)) + if domain not in integration_cache: + mock_integration(hass, MockModule(domain)) _LOGGER.info("Adding mock integration platform: %s", platform_path) - module_cache["{}.{}".format(platform_name, domain)] = module + module_cache[platform_path] = module or Mock() def async_capture_events(hass, event_name): diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index cbc3784e3f5..d42e7b8e367 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -1,19 +1,35 @@ """Tests for the Somfy config flow.""" import asyncio -from unittest.mock import Mock, patch +from unittest.mock import patch -from pymfy.api.somfy_api import SomfyApi +import pytest -from homeassistant import data_entry_flow +from homeassistant import data_entry_flow, setup, config_entries from homeassistant.components.somfy import config_flow, DOMAIN -from homeassistant.components.somfy.config_flow import register_flow_implementation -from tests.common import MockConfigEntry, mock_coro +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry CLIENT_SECRET_VALUE = "5678" CLIENT_ID_VALUE = "1234" -AUTH_URL = "http://somfy.com" + +@pytest.fixture() +async def mock_impl(hass): + """Mock implementation.""" + await setup.async_setup_component(hass, "http", {}) + + impl = config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + CLIENT_ID_VALUE, + CLIENT_SECRET_VALUE, + "https://accounts.somfy.com/oauth/oauth/v2/auth", + "https://accounts.somfy.com/oauth/oauth/v2/token", + ) + config_flow.SomfyFlowHandler.async_register_implementation(hass, impl) + return impl async def test_abort_if_no_configuration(hass): @@ -30,47 +46,84 @@ async def test_abort_if_existing_entry(hass): flow = config_flow.SomfyFlowHandler() flow.hass = hass MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - result = await flow.async_step_import() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_setup" + result = await flow.async_step_user() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_setup" -async def test_full_flow(hass): - """Check classic use case.""" - hass.data[DOMAIN] = {} - register_flow_implementation(hass, CLIENT_ID_VALUE, CLIENT_SECRET_VALUE) - flow = config_flow.SomfyFlowHandler() - flow.hass = hass - hass.config.api = Mock(base_url="https://example.com") - flow._get_authorization_url = Mock(return_value=mock_coro((AUTH_URL, "state"))) - result = await flow.async_step_import() +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "somfy", + { + "somfy": { + "client_id": CLIENT_ID_VALUE, + "client_secret": CLIENT_SECRET_VALUE, + }, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "somfy", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP - assert result["url"] == AUTH_URL - result = await flow.async_step_auth("my_super_code") - assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP_DONE - assert result["step_id"] == "creation" - assert flow.code == "my_super_code" - with patch.object( - SomfyApi, "request_token", return_value={"access_token": "super_token"} - ): - result = await flow.async_step_creation() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["data"]["refresh_args"] == { - "client_id": CLIENT_ID_VALUE, - "client_secret": CLIENT_SECRET_VALUE, + assert result["url"] == ( + "https://accounts.somfy.com/oauth/oauth/v2/auth" + f"?response_type=code&client_id={CLIENT_ID_VALUE}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + "https://accounts.somfy.com/oauth/oauth/v2/token", + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch("homeassistant.components.somfy.api.ConfigEntrySomfyApi"): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == "somfy" + + result["data"]["token"].pop("expires_at") + assert result["data"]["token"] == { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, } - assert result["title"] == "Somfy" - assert result["data"]["token"] == {"access_token": "super_token"} + + assert "somfy" in hass.config.components + entry = hass.config_entries.async_entries("somfy")[0] + assert entry.state == config_entries.ENTRY_STATE_LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED -async def test_abort_if_authorization_timeout(hass): +async def test_abort_if_authorization_timeout(hass, mock_impl): """Check Somfy authorization timeout.""" flow = config_flow.SomfyFlowHandler() flow.hass = hass - flow._get_authorization_url = Mock(side_effect=asyncio.TimeoutError) - result = await flow.async_step_auth() + + with patch.object( + mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + ): + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "authorize_url_timeout" diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py new file mode 100644 index 00000000000..e47dd834bf7 --- /dev/null +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -0,0 +1,266 @@ +"""Tests for the Somfy config flow.""" +import asyncio +import logging +from unittest.mock import patch +import time + +import pytest + +from homeassistant import data_entry_flow, setup, config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import mock_platform, MockConfigEntry + +TEST_DOMAIN = "oauth2_test" +CLIENT_SECRET = "5678" +CLIENT_ID = "1234" +REFRESH_TOKEN = "mock-refresh-token" +ACCESS_TOKEN_1 = "mock-access-token-1" +ACCESS_TOKEN_2 = "mock-access-token-2" +AUTHORIZE_URL = "https://example.como/auth/authorize" +TOKEN_URL = "https://example.como/auth/token" + + +@pytest.fixture +async def local_impl(hass): + """Local implementation.""" + assert await setup.async_setup_component(hass, "http", {}) + return config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, TEST_DOMAIN, CLIENT_ID, CLIENT_SECRET, AUTHORIZE_URL, TOKEN_URL + ) + + +@pytest.fixture +def flow_handler(hass): + """Return a registered config flow.""" + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + class TestFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Test flow handler.""" + + DOMAIN = TEST_DOMAIN + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": "read write"} + + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}): + yield TestFlowHandler + + +class MockOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implementation): + """Mock implementation for testing.""" + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Mock" + + @property + def domain(self) -> str: + """Domain that is providing the implementation.""" + return "test" + + async def async_generate_authorize_url(self, flow_id: str) -> str: + """Generate a url for the user to authorize.""" + return "http://example.com/auth" + + async def async_resolve_external_data(self, external_data) -> dict: + """Resolve external data to tokens.""" + return external_data + + async def _async_refresh_token(self, token: dict) -> dict: + """Refresh a token.""" + raise NotImplementedError() + + +def test_inherit_enforces_domain_set(): + """Test we enforce setting DOMAIN.""" + + class TestFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler): + """Test flow handler.""" + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + with patch.dict(config_entries.HANDLERS, {TEST_DOMAIN: TestFlowHandler}): + with pytest.raises(TypeError): + TestFlowHandler() + + +async def test_abort_if_no_implementation(hass, flow_handler): + """Check flow abort when no implementations.""" + flow = flow_handler() + flow.hass = hass + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "missing_configuration" + + +async def test_abort_if_authorization_timeout(hass, flow_handler, local_impl): + """Check timeout generating authorization url.""" + flow_handler.async_register_implementation(hass, local_impl) + + flow = flow_handler() + flow.hass = hass + + with patch.object( + local_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + ): + result = await flow.async_step_user() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "authorize_url_timeout" + + +async def test_full_flow( + hass, flow_handler, local_impl, aiohttp_client, aioclient_mock +): + """Check full flow.""" + hass.config.api.base_url = "https://example.com" + flow_handler.async_register_implementation(hass, local_impl) + config_entry_oauth2_flow.async_register_implementation( + hass, TEST_DOMAIN, MockOAuth2Implementation() + ) + + result = await hass.config_entries.flow.async_init( + TEST_DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "pick_implementation" + + # Pick implementation + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"implementation": TEST_DOMAIN} + ) + + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{AUTHORIZE_URL}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}&scope=read+write" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + TOKEN_URL, + json={ + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "type": "bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["data"]["auth_implementation"] == TEST_DOMAIN + + result["data"]["token"].pop("expires_at") + assert result["data"]["token"] == { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "type": "bearer", + "expires_in": 60, + } + + entry = hass.config_entries.async_entries(TEST_DOMAIN)[0] + + assert ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + is local_impl + ) + + +async def test_local_refresh_token(hass, local_impl, aioclient_mock): + """Test we can refresh token.""" + aioclient_mock.post( + TOKEN_URL, json={"access_token": ACCESS_TOKEN_2, "expires_in": 100} + ) + + new_tokens = await local_impl.async_refresh_token( + { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "type": "bearer", + "expires_in": 60, + } + ) + new_tokens.pop("expires_at") + + assert new_tokens == { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_2, + "type": "bearer", + "expires_in": 100, + } + + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": REFRESH_TOKEN, + } + + +async def test_oauth_session(hass, flow_handler, local_impl, aioclient_mock): + """Test the OAuth2 session helper.""" + flow_handler.async_register_implementation(hass, local_impl) + + aioclient_mock.post( + TOKEN_URL, json={"access_token": ACCESS_TOKEN_2, "expires_in": 100} + ) + + aioclient_mock.post("https://example.com", status=201) + + config_entry = MockConfigEntry( + domain=TEST_DOMAIN, + data={ + "auth_implementation": TEST_DOMAIN, + "token": { + "refresh_token": REFRESH_TOKEN, + "access_token": ACCESS_TOKEN_1, + "expires_in": 10, + "expires_at": 0, # Forces a refresh, + "token_type": "bearer", + "random_other_data": "should_stay", + }, + }, + ) + + now = time.time() + session = config_entry_oauth2_flow.OAuth2Session(hass, config_entry, local_impl) + resp = await session.async_request("post", "https://example.com") + assert resp.status == 201 + + # Refresh token, make request + assert len(aioclient_mock.mock_calls) == 2 + + assert ( + aioclient_mock.mock_calls[1][3]["authorization"] == f"Bearer {ACCESS_TOKEN_2}" + ) + + assert config_entry.data["token"]["refresh_token"] == REFRESH_TOKEN + assert config_entry.data["token"]["access_token"] == ACCESS_TOKEN_2 + assert config_entry.data["token"]["expires_in"] == 100 + assert config_entry.data["token"]["random_other_data"] == "should_stay" + assert round(config_entry.data["token"]["expires_at"] - now) == 100 From 2a95180d3b7d06cfef4e66969e6963c180e6eed0 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Fri, 18 Oct 2019 23:54:56 +0200 Subject: [PATCH 407/639] Move imports in fritzbox, fritz device tracker, fritzdect, fritzbox netmonitor (#27746) * Moved imports to top-level in fritzbox_netmonitor component * Moved imports to top-level in fritz, fritzbox and fritzdect --- homeassistant/components/fritz/device_tracker.py | 2 +- homeassistant/components/fritzbox/__init__.py | 4 ++-- .../components/fritzbox_netmonitor/sensor.py | 8 ++++---- homeassistant/components/fritzdect/switch.py | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/fritz/device_tracker.py b/homeassistant/components/fritz/device_tracker.py index c2b1e3ab54e..ab4deec96f7 100644 --- a/homeassistant/components/fritz/device_tracker.py +++ b/homeassistant/components/fritz/device_tracker.py @@ -4,13 +4,13 @@ import logging from fritzconnection import FritzHosts # pylint: disable=import-error import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index a053bc6c7ca..40aa3a881d1 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -1,9 +1,9 @@ """Support for AVM Fritz!Box smarthome devices.""" import logging +from pyfritzhome import Fritzhome, LoginError import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -12,6 +12,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -52,7 +53,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the fritzbox component.""" - from pyfritzhome import Fritzhome, LoginError fritz_list = [] diff --git a/homeassistant/components/fritzbox_netmonitor/sensor.py b/homeassistant/components/fritzbox_netmonitor/sensor.py index 92a29e37c51..0a82c5e29c3 100644 --- a/homeassistant/components/fritzbox_netmonitor/sensor.py +++ b/homeassistant/components/fritzbox_netmonitor/sensor.py @@ -1,18 +1,18 @@ """Support for monitoring an AVM Fritz!Box router.""" -import logging from datetime import timedelta -from requests.exceptions import RequestException +import logging from fritzconnection import FritzStatus # pylint: disable=import-error from fritzconnection.fritzconnection import ( # pylint: disable=import-error FritzConnectionException, ) +from requests.exceptions import RequestException import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, CONF_HOST, STATE_UNAVAILABLE -from homeassistant.helpers.entity import Entity +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/fritzdect/switch.py b/homeassistant/components/fritzdect/switch.py index dcb700d6636..cc629c54dc3 100644 --- a/homeassistant/components/fritzdect/switch.py +++ b/homeassistant/components/fritzdect/switch.py @@ -1,20 +1,21 @@ """Support for FRITZ!DECT Switches.""" import logging -from requests.exceptions import RequestException, HTTPError - +from fritzhome.fritz import FritzBox +from requests.exceptions import HTTPError, RequestException import voluptuous as vol -from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, - POWER_WATT, ENERGY_KILO_WATT_HOUR, + POWER_WATT, + TEMP_CELSIUS, ) import homeassistant.helpers.config_validation as cv -from homeassistant.const import TEMP_CELSIUS, ATTR_TEMPERATURE _LOGGER = logging.getLogger(__name__) @@ -42,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Add all switches connected to Fritz Box.""" - from fritzhome.fritz import FritzBox host = config.get(CONF_HOST) username = config.get(CONF_USERNAME) From c333daab107c57b5000cb6bbb155fb7c4404ac78 Mon Sep 17 00:00:00 2001 From: bouni Date: Fri, 18 Oct 2019 23:58:00 +0200 Subject: [PATCH 408/639] Move imports in cppm_tracker component (#27889) --- .../components/cppm_tracker/device_tracker.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/cppm_tracker/device_tracker.py b/homeassistant/components/cppm_tracker/device_tracker.py index c1c62a26dd9..1bb723091d4 100644 --- a/homeassistant/components/cppm_tracker/device_tracker.py +++ b/homeassistant/components/cppm_tracker/device_tracker.py @@ -1,15 +1,17 @@ """Support for ClearPass Policy Manager.""" -import logging from datetime import timedelta +import logging +from clearpasspy import ClearPass import voluptuous as vol -import homeassistant.helpers.config_validation as cv + from homeassistant.components.device_tracker import ( + DOMAIN, PLATFORM_SCHEMA, DeviceScanner, - DOMAIN, ) -from homeassistant.const import CONF_HOST, CONF_API_KEY +from homeassistant.const import CONF_API_KEY, CONF_HOST +import homeassistant.helpers.config_validation as cv SCAN_INTERVAL = timedelta(seconds=120) @@ -30,7 +32,6 @@ _LOGGER = logging.getLogger(__name__) def get_scanner(hass, config): """Initialize Scanner.""" - from clearpasspy import ClearPass data = { "server": config[DOMAIN][CONF_HOST], From dc30119d2042a3992f9dd3f6b7234e20da6e2691 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:00:00 +0200 Subject: [PATCH 409/639] Move imports in concord232 component (#27887) --- .../components/concord232/alarm_control_panel.py | 8 ++++---- homeassistant/components/concord232/binary_sensor.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/concord232/alarm_control_panel.py b/homeassistant/components/concord232/alarm_control_panel.py index e86ec02040e..37bbf052838 100644 --- a/homeassistant/components/concord232/alarm_control_panel.py +++ b/homeassistant/components/concord232/alarm_control_panel.py @@ -2,22 +2,23 @@ import datetime import logging +from concord232 import client as concord232_client import requests import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm -import homeassistant.helpers.config_validation as cv from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA from homeassistant.const import ( + CONF_CODE, CONF_HOST, + CONF_MODE, CONF_NAME, CONF_PORT, - CONF_CODE, - CONF_MODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -60,7 +61,6 @@ class Concord232Alarm(alarm.AlarmControlPanel): def __init__(self, url, name, code, mode): """Initialize the Concord232 alarm panel.""" - from concord232 import client as concord232_client self._state = None self._name = name diff --git a/homeassistant/components/concord232/binary_sensor.py b/homeassistant/components/concord232/binary_sensor.py index 1a406d743b7..2d119e2cf86 100644 --- a/homeassistant/components/concord232/binary_sensor.py +++ b/homeassistant/components/concord232/binary_sensor.py @@ -2,13 +2,14 @@ import datetime import logging +from concord232 import client as concord232_client import requests import voluptuous as vol from homeassistant.components.binary_sensor import ( - BinarySensorDevice, - PLATFORM_SCHEMA, DEVICE_CLASSES, + PLATFORM_SCHEMA, + BinarySensorDevice, ) from homeassistant.const import CONF_HOST, CONF_PORT import homeassistant.helpers.config_validation as cv @@ -42,7 +43,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Concord232 binary sensor platform.""" - from concord232 import client as concord232_client host = config.get(CONF_HOST) port = config.get(CONF_PORT) From 93db814b15a976ddc25236e4f16382968f2816ef Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:01:03 +0200 Subject: [PATCH 410/639] Move imports in comfoconnect component (#27886) --- .../components/comfoconnect/__init__.py | 13 ++++++------- homeassistant/components/comfoconnect/fan.py | 17 ++++++++--------- homeassistant/components/comfoconnect/sensor.py | 17 +++++++++-------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/comfoconnect/__init__.py b/homeassistant/components/comfoconnect/__init__.py index 22e9d95bbd8..aef4bf1deeb 100644 --- a/homeassistant/components/comfoconnect/__init__.py +++ b/homeassistant/components/comfoconnect/__init__.py @@ -1,6 +1,12 @@ """Support to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging +from pycomfoconnect import ( + SENSOR_TEMPERATURE_EXTRACT, + SENSOR_TEMPERATURE_OUTDOOR, + Bridge, + ComfoConnect, +) import voluptuous as vol from homeassistant.const import ( @@ -56,7 +62,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the ComfoConnect bridge.""" - from pycomfoconnect import Bridge conf = config[DOMAIN] host = conf.get(CONF_HOST) @@ -97,7 +102,6 @@ class ComfoConnectBridge: def __init__(self, hass, bridge, name, token, friendly_name, pin): """Initialize the ComfoConnect bridge.""" - from pycomfoconnect import ComfoConnect self.data = {} self.name = name @@ -125,11 +129,6 @@ class ComfoConnectBridge: """Call function for sensor updates.""" _LOGGER.debug("Got value from bridge: %d = %d", var, value) - from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, - SENSOR_TEMPERATURE_OUTDOOR, - ) - if var in [SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR]: self.data[var] = value / 10 else: diff --git a/homeassistant/components/comfoconnect/fan.py b/homeassistant/components/comfoconnect/fan.py index 6c90ab8cba1..bbb4b0176bf 100644 --- a/homeassistant/components/comfoconnect/fan.py +++ b/homeassistant/components/comfoconnect/fan.py @@ -1,6 +1,14 @@ """Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging +from pycomfoconnect import ( + CMD_FAN_MODE_AWAY, + CMD_FAN_MODE_HIGH, + CMD_FAN_MODE_LOW, + CMD_FAN_MODE_MEDIUM, + SENSOR_FAN_SPEED_MODE, +) + from homeassistant.components.fan import ( SPEED_HIGH, SPEED_LOW, @@ -30,7 +38,6 @@ class ComfoConnectFan(FanEntity): def __init__(self, hass, name, ccb: ComfoConnectBridge) -> None: """Initialize the ComfoConnect fan.""" - from pycomfoconnect import SENSOR_FAN_SPEED_MODE self._ccb = ccb self._name = name @@ -64,7 +71,6 @@ class ComfoConnectFan(FanEntity): @property def speed(self): """Return the current fan mode.""" - from pycomfoconnect import SENSOR_FAN_SPEED_MODE try: speed = self._ccb.data[SENSOR_FAN_SPEED_MODE] @@ -91,13 +97,6 @@ class ComfoConnectFan(FanEntity): """Set fan speed.""" _LOGGER.debug("Changing fan speed to %s.", speed) - from pycomfoconnect import ( - CMD_FAN_MODE_AWAY, - CMD_FAN_MODE_LOW, - CMD_FAN_MODE_MEDIUM, - CMD_FAN_MODE_HIGH, - ) - if speed == SPEED_OFF: self._ccb.comfoconnect.cmd_rmi_request(CMD_FAN_MODE_AWAY) elif speed == SPEED_LOW: diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 4099804d413..06d0506e2cf 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -1,6 +1,15 @@ """Platform to control a Zehnder ComfoAir Q350/450/600 ventilation unit.""" import logging +from pycomfoconnect import ( + SENSOR_FAN_EXHAUST_FLOW, + SENSOR_FAN_SUPPLY_FLOW, + SENSOR_HUMIDITY_EXTRACT, + SENSOR_HUMIDITY_OUTDOOR, + SENSOR_TEMPERATURE_EXTRACT, + SENSOR_TEMPERATURE_OUTDOOR, +) + from homeassistant.const import CONF_RESOURCES, TEMP_CELSIUS from homeassistant.helpers.dispatcher import dispatcher_connect from homeassistant.helpers.entity import Entity @@ -24,14 +33,6 @@ SENSOR_TYPES = {} def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ComfoConnect fan platform.""" - from pycomfoconnect import ( - SENSOR_TEMPERATURE_EXTRACT, - SENSOR_HUMIDITY_EXTRACT, - SENSOR_TEMPERATURE_OUTDOOR, - SENSOR_HUMIDITY_OUTDOOR, - SENSOR_FAN_SUPPLY_FLOW, - SENSOR_FAN_EXHAUST_FLOW, - ) global SENSOR_TYPES SENSOR_TYPES = { From b11dc0f50fa6a580961dc0642f6d2936f3813f92 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:04:37 +0200 Subject: [PATCH 411/639] Move imports in coinmarketcap component (#27885) --- homeassistant/components/coinmarketcap/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/coinmarketcap/sensor.py b/homeassistant/components/coinmarketcap/sensor.py index fbe05187684..ca166aa793a 100644 --- a/homeassistant/components/coinmarketcap/sensor.py +++ b/homeassistant/components/coinmarketcap/sensor.py @@ -1,13 +1,14 @@ """Details about crypto currencies from CoinMarketCap.""" -import logging from datetime import timedelta +import logging from urllib.error import HTTPError +from coinmarketcap import Market import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_DISPLAY_CURRENCY +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -159,6 +160,5 @@ class CoinMarketCapData: def update(self): """Get the latest data from coinmarketcap.com.""" - from coinmarketcap import Market self.ticker = Market().ticker(self.currency_id, convert=self.display_currency) From 2e416168cf6b06c6b80cc13ca78df4a85fa901a9 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:05:42 +0200 Subject: [PATCH 412/639] Move imports in coinbase component (#27884) --- homeassistant/components/coinbase/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/coinbase/__init__.py b/homeassistant/components/coinbase/__init__.py index 6eca0616ca8..67869e6b88c 100644 --- a/homeassistant/components/coinbase/__init__.py +++ b/homeassistant/components/coinbase/__init__.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +from coinbase.wallet.client import Client +from coinbase.wallet.error import AuthenticationError import voluptuous as vol from homeassistant.const import CONF_API_KEY @@ -79,7 +81,6 @@ class CoinbaseData: def __init__(self, api_key, api_secret): """Init the coinbase data object.""" - from coinbase.wallet.client import Client self.client = Client(api_key, api_secret) self.update() @@ -87,7 +88,6 @@ class CoinbaseData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from coinbase.""" - from coinbase.wallet.error import AuthenticationError try: self.accounts = self.client.get_accounts() From d7a8a635ba5b8826aaa46ff200fd0f5067797a44 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:14:49 +0200 Subject: [PATCH 413/639] Move imports in ciscospark component (#27879) --- homeassistant/components/ciscospark/notify.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/ciscospark/notify.py b/homeassistant/components/ciscospark/notify.py index 67609766366..e765aff05f6 100644 --- a/homeassistant/components/ciscospark/notify.py +++ b/homeassistant/components/ciscospark/notify.py @@ -1,16 +1,16 @@ """Cisco Spark platform for notify component.""" import logging +from ciscosparkapi import CiscoSparkAPI, SparkApiError import voluptuous as vol -from homeassistant.const import CONF_TOKEN -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_TOKEN +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -33,7 +33,6 @@ class CiscoSparkNotificationService(BaseNotificationService): def __init__(self, token, default_room): """Initialize the service.""" - from ciscosparkapi import CiscoSparkAPI self._default_room = default_room self._token = token @@ -41,7 +40,6 @@ class CiscoSparkNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from ciscosparkapi import SparkApiError try: title = "" From 7ed5616faac0004008548a9977a9bb25a1bb7222 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:23:17 +0200 Subject: [PATCH 414/639] Move imports in cisco_webex_teams component (#27878) --- homeassistant/components/cisco_webex_teams/notify.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cisco_webex_teams/notify.py b/homeassistant/components/cisco_webex_teams/notify.py index a77f5673df7..6f80fa138d4 100644 --- a/homeassistant/components/cisco_webex_teams/notify.py +++ b/homeassistant/components/cisco_webex_teams/notify.py @@ -2,11 +2,12 @@ import logging import voluptuous as vol +from webexteamssdk import ApiError, WebexTeamsAPI, exceptions from homeassistant.components.notify import ( + ATTR_TITLE, PLATFORM_SCHEMA, BaseNotificationService, - ATTR_TITLE, ) from homeassistant.const import CONF_TOKEN import homeassistant.helpers.config_validation as cv @@ -22,7 +23,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the CiscoWebexTeams notification service.""" - from webexteamssdk import WebexTeamsAPI, exceptions client = WebexTeamsAPI(access_token=config[CONF_TOKEN]) try: @@ -45,7 +45,6 @@ class CiscoWebexTeamsNotificationService(BaseNotificationService): def send_message(self, message="", **kwargs): """Send a message to a user.""" - from webexteamssdk import ApiError title = "" if kwargs.get(ATTR_TITLE) is not None: From b2b140e8d0b0809d60968e4b73dc19cf9ec39a74 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:26:58 +0200 Subject: [PATCH 415/639] Move imports in cmus component (#27883) --- homeassistant/components/cmus/media_player.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cmus/media_player.py b/homeassistant/components/cmus/media_player.py index dbaa763c461..3daf0bac828 100644 --- a/homeassistant/components/cmus/media_player.py +++ b/homeassistant/components/cmus/media_player.py @@ -1,9 +1,10 @@ """Support for interacting with and controlling the cmus music player.""" import logging +from pycmus import exceptions, remote import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -57,7 +58,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discover_info=None): """Set up the CMUS platform.""" - from pycmus import exceptions host = config.get(CONF_HOST) password = config.get(CONF_PASSWORD) @@ -78,7 +78,6 @@ class CmusDevice(MediaPlayerDevice): # pylint: disable=no-member def __init__(self, server, password, port, name): """Initialize the CMUS device.""" - from pycmus import remote if server: self.cmus = remote.PyCmus(server=server, password=password, port=port) From 1d8e366278c7f0e041e2da2a6bd2ae026c1b7dba Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:39:37 +0200 Subject: [PATCH 416/639] Move imports in cloud component (#27881) --- homeassistant/components/cloud/__init__.py | 8 ++++---- homeassistant/components/cloud/http_api.py | 23 ++++++++++------------ 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 71550fc37b1..a2c79fdc0a7 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -1,6 +1,7 @@ """Component to integrate the Home Assistant cloud.""" import logging +from hass_nabucasa import Cloud import voluptuous as vol from homeassistant.auth.const import GROUP_ID_ADMIN @@ -20,25 +21,26 @@ from homeassistant.loader import bind_hass from homeassistant.util.aiohttp import MockRequest from . import http_api +from .client import CloudClient from .const import ( CONF_ACME_DIRECTORY_SERVER, CONF_ALEXA, + CONF_ALEXA_ACCESS_TOKEN_URL, CONF_ALIASES, CONF_CLOUDHOOK_CREATE_URL, CONF_COGNITO_CLIENT_ID, CONF_ENTITY_CONFIG, CONF_FILTER, CONF_GOOGLE_ACTIONS, + CONF_GOOGLE_ACTIONS_REPORT_STATE_URL, CONF_GOOGLE_ACTIONS_SYNC_URL, CONF_RELAYER, CONF_REMOTE_API_URL, CONF_SUBSCRIPTION_INFO_URL, CONF_USER_POOL_ID, - CONF_GOOGLE_ACTIONS_REPORT_STATE_URL, DOMAIN, MODE_DEV, MODE_PROD, - CONF_ALEXA_ACCESS_TOKEN_URL, ) from .prefs import CloudPreferences @@ -166,8 +168,6 @@ def is_cloudhook_request(request): async def async_setup(hass, config): """Initialize the Home Assistant cloud.""" - from hass_nabucasa import Cloud - from .client import CloudClient # Process configs if DOMAIN in config: diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index f243eab8fd0..97c96b0a3e8 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -3,33 +3,34 @@ import asyncio from functools import wraps import logging -import attr import aiohttp import async_timeout +import attr +from hass_nabucasa import Cloud, auth +from hass_nabucasa.const import STATE_DISCONNECTED import voluptuous as vol -from hass_nabucasa import Cloud -from homeassistant.core import callback -from homeassistant.components.http import HomeAssistantView -from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components import websocket_api -from homeassistant.components.websocket_api import const as ws_const from homeassistant.components.alexa import ( entities as alexa_entities, errors as alexa_errors, ) from homeassistant.components.google_assistant import helpers as google_helpers +from homeassistant.components.http import HomeAssistantView +from homeassistant.components.http.data_validator import RequestDataValidator +from homeassistant.components.websocket_api import const as ws_const +from homeassistant.core import callback from .const import ( DOMAIN, - REQUEST_TIMEOUT, + PREF_ALEXA_REPORT_STATE, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, + PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_SECURE_DEVICES_PIN, + REQUEST_TIMEOUT, InvalidTrustedNetworks, InvalidTrustedProxies, - PREF_ALEXA_REPORT_STATE, - PREF_GOOGLE_REPORT_STATE, RequireRelink, ) @@ -104,8 +105,6 @@ async def async_setup(hass): hass.http.register_view(CloudResendConfirmView) hass.http.register_view(CloudForgotPasswordView) - from hass_nabucasa import auth - _CLOUD_ERRORS.update( { auth.UserNotFound: (400, "User does not exist."), @@ -320,7 +319,6 @@ def _require_cloud_login(handler): @websocket_api.async_response async def websocket_subscription(hass, connection, msg): """Handle request for account info.""" - from hass_nabucasa.const import STATE_DISCONNECTED cloud = hass.data[DOMAIN] @@ -417,7 +415,6 @@ async def websocket_hook_delete(hass, connection, msg): def _account_data(cloud): """Generate the auth data JSON response.""" - from hass_nabucasa.const import STATE_DISCONNECTED if not cloud.is_logged_in: return {"logged_in": False, "cloud": STATE_DISCONNECTED} From 8e3d21081868a7599392a4b5f0f0ff7f21137862 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 19 Oct 2019 00:41:11 +0200 Subject: [PATCH 417/639] Add remove function to hue sensors (#27652) * Add remove function to sensors * Fix + comments * Update light.py --- homeassistant/components/hue/helpers.py | 33 +++++++++++++++++++ homeassistant/components/hue/light.py | 36 ++++++--------------- homeassistant/components/hue/sensor_base.py | 13 ++++++-- 3 files changed, 53 insertions(+), 29 deletions(-) create mode 100644 homeassistant/components/hue/helpers.py diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py new file mode 100644 index 00000000000..388046bb8cb --- /dev/null +++ b/homeassistant/components/hue/helpers.py @@ -0,0 +1,33 @@ +"""Helper functions for Philips Hue.""" +from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg + +from .const import DOMAIN + + +async def remove_devices(hass, config_entry, api_ids, current): + """Get items that are removed from api.""" + removed_items = [] + + for item_id in current: + if item_id in api_ids: + continue + + # Device is removed from Hue, so we remove it from Home Assistant + entity = current[item_id] + removed_items.append(item_id) + await entity.async_remove() + ent_registry = await get_ent_reg(hass) + if entity.entity_id in ent_registry.entities: + ent_registry.async_remove(entity.entity_id) + dev_registry = await get_dev_reg(hass) + device = dev_registry.async_get_device( + identifiers={(DOMAIN, entity.device_id)}, connections=set() + ) + if device is not None: + dev_registry.async_update_device( + device.id, remove_config_entry_id=config_entry.entry_id + ) + + for item_id in removed_items: + del current[item_id] diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 5a3379f71ce..dcae1cf4f5d 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -8,9 +8,6 @@ import random import aiohue import async_timeout -from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg - from homeassistant.components import hue from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -32,6 +29,7 @@ from homeassistant.components.light import ( Light, ) from homeassistant.util import color +from .helpers import remove_devices SCAN_INTERVAL = timedelta(seconds=5) @@ -226,7 +224,6 @@ async def async_update_items( bridge.available = True new_items = [] - removed_items = [] for item_id in api: if item_id not in current: @@ -238,31 +235,11 @@ async def async_update_items( elif item_id not in progress_waiting: current[item_id].async_schedule_update_ha_state() - for item_id in current: - if item_id in api: - continue - - # Device is removed from Hue, so we remove it from Home Assistant - entity = current[item_id] - removed_items.append(item_id) - await entity.async_remove() - ent_registry = await get_ent_reg(hass) - if entity.entity_id in ent_registry.entities: - ent_registry.async_remove(entity.entity_id) - dev_registry = await get_dev_reg(hass) - device = dev_registry.async_get_device( - identifiers={(hue.DOMAIN, entity.unique_id)}, connections=set() - ) - dev_registry.async_update_device( - device.id, remove_config_entry_id=config_entry.entry_id - ) + await remove_devices(hass, config_entry, api, current) if new_items: async_add_entities(new_items) - for item_id in removed_items: - del current[item_id] - class HueLight(Light): """Representation of a Hue light.""" @@ -300,9 +277,14 @@ class HueLight(Light): @property def unique_id(self): - """Return the ID of this Hue light.""" + """Return the unique ID of this Hue light.""" return self.light.uniqueid + @property + def device_id(self): + """Return the ID of this Hue light.""" + return self.unique_id + @property def name(self): """Return the name of the Hue light.""" @@ -384,7 +366,7 @@ class HueLight(Light): return None return { - "identifiers": {(hue.DOMAIN, self.unique_id)}, + "identifiers": {(hue.DOMAIN, self.device_id)}, "name": self.name, "manufacturer": self.light.manufacturername, # productname added in Hue Bridge API 1.24 diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 96b9b8bf5d6..3f202d38bc5 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -11,6 +11,7 @@ from homeassistant.exceptions import NoEntitySpecifiedError from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow +from .helpers import remove_devices CURRENT_SENSORS = "current_sensors" SENSOR_MANAGER_FORMAT = "{}_sensor_manager" @@ -34,7 +35,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities, binary=False sm_key = SENSOR_MANAGER_FORMAT.format(config_entry.data["host"]) manager = hass.data[hue.DOMAIN].get(sm_key) if manager is None: - manager = SensorManager(hass, bridge) + manager = SensorManager(hass, bridge, config_entry) hass.data[hue.DOMAIN][sm_key] = manager manager.register_component(binary, async_add_entities) @@ -50,7 +51,7 @@ class SensorManager: SCAN_INTERVAL = timedelta(seconds=5) sensor_config_map = {} - def __init__(self, hass, bridge): + def __init__(self, hass, bridge, config_entry): """Initialize the sensor manager.""" import aiohue from .binary_sensor import HuePresence, PRESENCE_NAME_FORMAT @@ -63,6 +64,7 @@ class SensorManager: self.hass = hass self.bridge = bridge + self.config_entry = config_entry self._component_add_entities = {} self._started = False @@ -194,6 +196,13 @@ class SensorManager: else: new_sensors.append(current[api[item_id].uniqueid]) + await remove_devices( + self.hass, + self.config_entry, + [value.uniqueid for value in api.values()], + current, + ) + async_add_sensor_entities = self._component_add_entities.get(False) async_add_binary_entities = self._component_add_entities.get(True) if new_sensors and async_add_sensor_entities: From 535da96d4d85759c9adf96947380922bc345ceb9 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Fri, 18 Oct 2019 15:56:59 -0700 Subject: [PATCH 418/639] Move imports to top for hikvisioncam (#27895) --- homeassistant/components/hikvisioncam/switch.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hikvisioncam/switch.py b/homeassistant/components/hikvisioncam/switch.py index 05bce5f4eac..020b894c0f7 100644 --- a/homeassistant/components/hikvisioncam/switch.py +++ b/homeassistant/components/hikvisioncam/switch.py @@ -1,20 +1,22 @@ """Support turning on/off motion detection on Hikvision cameras.""" import logging +import hikvision.api +from hikvision.error import HikvisionError, MissingParamError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_HOST, + CONF_NAME, CONF_PASSWORD, - CONF_USERNAME, CONF_PORT, + CONF_USERNAME, STATE_OFF, STATE_ON, ) -from homeassistant.helpers.entity import ToggleEntity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import ToggleEntity # This is the last working version, please test before updating @@ -38,9 +40,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up Hikvision camera.""" - import hikvision.api - from hikvision.error import HikvisionError, MissingParamError - host = config.get(CONF_HOST) port = config.get(CONF_PORT) name = config.get(CONF_NAME) From 29ef49fdd9c1b7429d570cae03de22332748ac56 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:57:59 +0200 Subject: [PATCH 419/639] Move imports in coolmaster component (#27888) --- homeassistant/components/coolmaster/climate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 8a319c655f6..71115a9eebb 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -2,16 +2,17 @@ import logging +from pycoolmasternet import CoolMasterNet import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_OFF, - HVAC_MODE_HEAT_COOL, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -70,7 +71,6 @@ def _build_entity(device, supported_modes): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the CoolMasterNet climate platform.""" - from pycoolmasternet import CoolMasterNet supported_modes = config.get(CONF_SUPPORTED_MODES) host = config[CONF_HOST] From 8cf443110a0fc9d902560d4676cc5bb3b02b677f Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:59:07 +0200 Subject: [PATCH 420/639] Move imports in cisco_mobility_express component (#27877) --- .../components/cisco_mobility_express/device_tracker.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cisco_mobility_express/device_tracker.py b/homeassistant/components/cisco_mobility_express/device_tracker.py index ca24fcb5c52..702ebdfa611 100644 --- a/homeassistant/components/cisco_mobility_express/device_tracker.py +++ b/homeassistant/components/cisco_mobility_express/device_tracker.py @@ -1,9 +1,9 @@ """Support for Cisco Mobility Express.""" import logging +from ciscomobilityexpress.ciscome import CiscoMobilityExpress import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.device_tracker import ( DOMAIN, PLATFORM_SCHEMA, @@ -11,11 +11,12 @@ from homeassistant.components.device_tracker import ( ) from homeassistant.const import ( CONF_HOST, - CONF_USERNAME, CONF_PASSWORD, CONF_SSL, + CONF_USERNAME, CONF_VERIFY_SSL, ) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_scanner(hass, config): """Validate the configuration and return a Cisco ME scanner.""" - from ciscomobilityexpress.ciscome import CiscoMobilityExpress config = config[DOMAIN] From dc5d38128c44e12aa840b685a46e65831b4a1878 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 00:59:58 +0200 Subject: [PATCH 421/639] Move imports in cast component (#27875) --- homeassistant/components/cast/config_flow.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cast/config_flow.py b/homeassistant/components/cast/config_flow.py index c3c21944d02..5c2b6dca932 100644 --- a/homeassistant/components/cast/config_flow.py +++ b/homeassistant/components/cast/config_flow.py @@ -1,12 +1,14 @@ """Config flow for Cast.""" -from homeassistant.helpers import config_entry_flow +from pychromecast.discovery import discover_chromecasts + from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + from .const import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - from pychromecast.discovery import discover_chromecasts return await hass.async_add_executor_job(discover_chromecasts) From d78f14b20a9e8f0704ad0faaaec92be754ea03fc Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 01:01:59 +0200 Subject: [PATCH 422/639] Move imports in canary component (#27874) --- homeassistant/components/canary/__init__.py | 10 +++++----- homeassistant/components/canary/alarm_control_panel.py | 10 ++-------- homeassistant/components/canary/camera.py | 6 ++---- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index f23b6ad46c9..a8a45f5b946 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -1,13 +1,14 @@ """Support for Canary devices.""" -import logging from datetime import timedelta +import logging -import voluptuous as vol +from canary.api import Api from requests import ConnectTimeout, HTTPError +import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_TIMEOUT +from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -67,7 +68,6 @@ class CanaryData: def __init__(self, username, password, timeout): """Init the Canary data object.""" - from canary.api import Api self._api = Api(username, password, timeout) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index 42b5048bc1d..856ecb9f3a2 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -1,6 +1,8 @@ """Support for Canary alarm.""" import logging +from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT + from homeassistant.components.alarm_control_panel import AlarmControlPanel from homeassistant.const import ( STATE_ALARM_ARMED_AWAY, @@ -42,11 +44,6 @@ class CanaryAlarm(AlarmControlPanel): @property def state(self): """Return the state of the device.""" - from canary.api import ( - LOCATION_MODE_AWAY, - LOCATION_MODE_HOME, - LOCATION_MODE_NIGHT, - ) location = self._data.get_location(self._location_id) @@ -75,18 +72,15 @@ class CanaryAlarm(AlarmControlPanel): def alarm_arm_home(self, code=None): """Send arm home command.""" - from canary.api import LOCATION_MODE_HOME self._data.set_location_mode(self._location_id, LOCATION_MODE_HOME) def alarm_arm_away(self, code=None): """Send arm away command.""" - from canary.api import LOCATION_MODE_AWAY self._data.set_location_mode(self._location_id, LOCATION_MODE_AWAY) def alarm_arm_night(self, code=None): """Send arm night command.""" - from canary.api import LOCATION_MODE_NIGHT self._data.set_location_mode(self._location_id, LOCATION_MODE_NIGHT) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 8a6d27b8916..7ed1e62ab8a 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -3,6 +3,8 @@ import asyncio from datetime import timedelta import logging +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, Camera @@ -81,8 +83,6 @@ class CanaryCamera(Camera): """Return a still image response from the camera.""" self.renew_live_stream_session() - from haffmpeg.tools import ImageFrame, IMAGE_JPEG - ffmpeg = ImageFrame(self._ffmpeg.binary, loop=self.hass.loop) image = await asyncio.shield( ffmpeg.get_image( @@ -98,8 +98,6 @@ class CanaryCamera(Camera): if self._live_stream_session is None: return - from haffmpeg.camera import CameraMjpeg - stream = CameraMjpeg(self._ffmpeg.binary, loop=self.hass.loop) await stream.open_camera( self._live_stream_session.live_stream_url, extra_cmd=self._ffmpeg_arguments From 422885b7fd5fc58772baff2987f0a317335dfce3 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 01:02:54 +0200 Subject: [PATCH 423/639] Move imports in buienradar component (#27873) --- homeassistant/components/buienradar/sensor.py | 64 +++++++++---------- .../components/buienradar/weather.py | 27 ++++---- 2 files changed, 43 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index ef65db74f16..300bcbf2243 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -5,6 +5,34 @@ import logging import aiohttp import async_timeout +from buienradar.buienradar import parse_data +from buienradar.constants import ( + ATTRIBUTION, + CONDCODE, + CONDITION, + CONTENT, + DATA, + DETAILED, + EXACT, + EXACTNL, + FORECAST, + HUMIDITY, + IMAGE, + MEASURED, + MESSAGE, + PRECIPITATION_FORECAST, + PRESSURE, + STATIONNAME, + STATUS_CODE, + SUCCESS, + TEMPERATURE, + TIMEFRAME, + VISIBILITY, + WINDAZIMUTH, + WINDGUST, + WINDSPEED, +) +from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -22,6 +50,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util +from .weather import DEFAULT_TIMEFRAME + _LOGGER = logging.getLogger(__name__) MEASURED_LABEL = "Measured" @@ -183,7 +213,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the buienradar sensor.""" - from .weather import DEFAULT_TIMEFRAME latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -216,7 +245,6 @@ class BrSensor(Entity): def __init__(self, sensor_type, client_name, coordinates): """Initialize the sensor.""" - from buienradar.constants import PRECIPITATION_FORECAST, CONDITION self.client_name = client_name self._name = SENSOR_TYPES[sensor_type][0] @@ -247,23 +275,6 @@ class BrSensor(Entity): def load_data(self, data): """Load the sensor with relevant data.""" # Find sensor - from buienradar.constants import ( - ATTRIBUTION, - CONDITION, - CONDCODE, - DETAILED, - EXACT, - EXACTNL, - FORECAST, - IMAGE, - MEASURED, - PRECIPITATION_FORECAST, - STATIONNAME, - TIMEFRAME, - VISIBILITY, - WINDGUST, - WINDSPEED, - ) # Check if we have a new measurement, # otherwise we do not have to update the sensor @@ -421,7 +432,6 @@ class BrSensor(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - from buienradar.constants import PRECIPITATION_FORECAST if self.type.startswith(PRECIPITATION_FORECAST): result = {ATTR_ATTRIBUTION: self._attribution} @@ -488,7 +498,6 @@ class BrData: async def get_data(self, url): """Load data from specified url.""" - from buienradar.constants import CONTENT, MESSAGE, STATUS_CODE, SUCCESS _LOGGER.debug("Calling url: %s...", url) result = {SUCCESS: False, MESSAGE: None} @@ -515,9 +524,6 @@ class BrData: async def async_update(self, *_): """Update the data from buienradar.""" - from buienradar.constants import CONTENT, DATA, MESSAGE, STATUS_CODE, SUCCESS - from buienradar.buienradar import parse_data - from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url content = await self.get_data(JSON_FEED_URL) @@ -576,28 +582,24 @@ class BrData: @property def attribution(self): """Return the attribution.""" - from buienradar.constants import ATTRIBUTION return self.data.get(ATTRIBUTION) @property def stationname(self): """Return the name of the selected weatherstation.""" - from buienradar.constants import STATIONNAME return self.data.get(STATIONNAME) @property def condition(self): """Return the condition.""" - from buienradar.constants import CONDITION return self.data.get(CONDITION) @property def temperature(self): """Return the temperature, or None.""" - from buienradar.constants import TEMPERATURE try: return float(self.data.get(TEMPERATURE)) @@ -607,7 +609,6 @@ class BrData: @property def pressure(self): """Return the pressure, or None.""" - from buienradar.constants import PRESSURE try: return float(self.data.get(PRESSURE)) @@ -617,7 +618,6 @@ class BrData: @property def humidity(self): """Return the humidity, or None.""" - from buienradar.constants import HUMIDITY try: return int(self.data.get(HUMIDITY)) @@ -627,7 +627,6 @@ class BrData: @property def visibility(self): """Return the visibility, or None.""" - from buienradar.constants import VISIBILITY try: return int(self.data.get(VISIBILITY)) @@ -637,7 +636,6 @@ class BrData: @property def wind_speed(self): """Return the windspeed, or None.""" - from buienradar.constants import WINDSPEED try: return float(self.data.get(WINDSPEED)) @@ -647,7 +645,6 @@ class BrData: @property def wind_bearing(self): """Return the wind bearing, or None.""" - from buienradar.constants import WINDAZIMUTH try: return int(self.data.get(WINDAZIMUTH)) @@ -657,6 +654,5 @@ class BrData: @property def forecast(self): """Return the forecast data.""" - from buienradar.constants import FORECAST return self.data.get(FORECAST) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index d8ae448c981..745bf12ffd8 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -1,18 +1,28 @@ """Support for Buienradar.nl weather service.""" import logging +from buienradar.constants import ( + CONDCODE, + CONDITION, + DATETIME, + MAX_TEMP, + MIN_TEMP, + RAIN, + WINDAZIMUTH, + WINDSPEED, +) import voluptuous as vol from homeassistant.components.weather import ( ATTR_FORECAST_CONDITION, + ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, - PLATFORM_SCHEMA, - WeatherEntity, - ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_WIND_BEARING, ATTR_FORECAST_WIND_SPEED, + PLATFORM_SCHEMA, + WeatherEntity, ) from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS from homeassistant.helpers import config_validation as cv @@ -110,7 +120,6 @@ class BrWeather(WeatherEntity): @property def condition(self): """Return the current condition.""" - from buienradar.constants import CONDCODE if self._data and self._data.condition: ccode = self._data.condition.get(CONDCODE) @@ -161,16 +170,6 @@ class BrWeather(WeatherEntity): @property def forecast(self): """Return the forecast array.""" - from buienradar.constants import ( - CONDITION, - CONDCODE, - RAIN, - DATETIME, - MIN_TEMP, - MAX_TEMP, - WINDAZIMUTH, - WINDSPEED, - ) if not self._forecast: return None From 6df34a0128cfd6297fab73c23608331b39cc79a1 Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 01:04:10 +0200 Subject: [PATCH 424/639] Move imports in channels component (#27876) --- homeassistant/components/channels/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/channels/media_player.py b/homeassistant/components/channels/media_player.py index 6c3e18cdb05..6d978a5451e 100644 --- a/homeassistant/components/channels/media_player.py +++ b/homeassistant/components/channels/media_player.py @@ -1,9 +1,10 @@ """Support for interfacing with an instance of getchannels.com.""" import logging +from pychannels import Channels import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( DOMAIN, MEDIA_TYPE_CHANNEL, @@ -124,7 +125,6 @@ class ChannelsPlayer(MediaPlayerDevice): def __init__(self, name, host, port): """Initialize the Channels app.""" - from pychannels import Channels self._name = name self._host = host From 065c6f4c9cfa6a434cd73d3610bf36a093896038 Mon Sep 17 00:00:00 2001 From: Heine Furubotten Date: Sat, 19 Oct 2019 01:05:36 +0200 Subject: [PATCH 425/639] Move imports for nilu component (#27896) --- homeassistant/components/nilu/air_quality.py | 48 ++++++++------------ 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/nilu/air_quality.py b/homeassistant/components/nilu/air_quality.py index 8d3d61befd5..8e851592de3 100644 --- a/homeassistant/components/nilu/air_quality.py +++ b/homeassistant/components/nilu/air_quality.py @@ -2,6 +2,22 @@ from datetime import timedelta import logging +from niluclient import ( + CO, + CO2, + NO, + NO2, + NOX, + OZONE, + PM1, + PM10, + PM25, + POLLUTION_INDEX, + SO2, + create_location_client, + create_station_client, + lookup_stations_in_area, +) import voluptuous as vol from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity @@ -95,8 +111,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NILU air quality sensor.""" - import niluclient as nilu - name = config.get(CONF_NAME) area = config.get(CONF_AREA) stations = config.get(CONF_STATION) @@ -105,15 +119,15 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensors = [] if area: - stations = nilu.lookup_stations_in_area(area) + stations = lookup_stations_in_area(area) elif not area and not stations: latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) - location_client = nilu.create_location_client(latitude, longitude) + location_client = create_location_client(latitude, longitude) stations = location_client.station_names for station in stations: - client = NiluData(nilu.create_station_client(station)) + client = NiluData(create_station_client(station)) client.update() if client.data.sensors: sensors.append(NiluSensor(client, name, show_on_map)) @@ -178,71 +192,51 @@ class NiluSensor(AirQualityEntity): @property def carbon_monoxide(self) -> str: """Return the CO (carbon monoxide) level.""" - from niluclient import CO - return self.get_component_state(CO) @property def carbon_dioxide(self) -> str: """Return the CO2 (carbon dioxide) level.""" - from niluclient import CO2 - return self.get_component_state(CO2) @property def nitrogen_oxide(self) -> str: """Return the N2O (nitrogen oxide) level.""" - from niluclient import NOX - return self.get_component_state(NOX) @property def nitrogen_monoxide(self) -> str: """Return the NO (nitrogen monoxide) level.""" - from niluclient import NO - return self.get_component_state(NO) @property def nitrogen_dioxide(self) -> str: """Return the NO2 (nitrogen dioxide) level.""" - from niluclient import NO2 - return self.get_component_state(NO2) @property def ozone(self) -> str: """Return the O3 (ozone) level.""" - from niluclient import OZONE - return self.get_component_state(OZONE) @property def particulate_matter_2_5(self) -> str: """Return the particulate matter 2.5 level.""" - from niluclient import PM25 - return self.get_component_state(PM25) @property def particulate_matter_10(self) -> str: """Return the particulate matter 10 level.""" - from niluclient import PM10 - return self.get_component_state(PM10) @property def particulate_matter_0_1(self) -> str: """Return the particulate matter 0.1 level.""" - from niluclient import PM1 - return self.get_component_state(PM1) @property def sulphur_dioxide(self) -> str: """Return the SO2 (sulphur dioxide) level.""" - from niluclient import SO2 - return self.get_component_state(SO2) def get_component_state(self, component_name: str) -> str: @@ -254,14 +248,12 @@ class NiluSensor(AirQualityEntity): def update(self) -> None: """Update the sensor.""" - import niluclient as nilu - self._api.update() sensors = self._api.data.sensors.values() if sensors: max_index = max([s.pollution_index for s in sensors]) self._max_aqi = max_index - self._attrs[ATTR_POLLUTION_INDEX] = nilu.POLLUTION_INDEX[self._max_aqi] + self._attrs[ATTR_POLLUTION_INDEX] = POLLUTION_INDEX[self._max_aqi] self._attrs[ATTR_AREA] = self._api.data.area From 9e8c391c814d2a2bc6b2acc94074be5b5616b771 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 19 Oct 2019 00:32:15 +0000 Subject: [PATCH 426/639] [ci skip] Translation update --- .../components/abode/.translations/fr.json | 14 ++++++- .../components/abode/.translations/it.json | 22 ++++++++++ .../components/abode/.translations/pl.json | 12 ++++++ .../components/abode/.translations/pt-BR.json | 12 ++++++ .../components/abode/.translations/pt.json | 15 +++++++ .../components/adguard/.translations/pt.json | 12 ++++++ .../components/airly/.translations/pt.json | 14 +++++++ .../alarm_control_panel/.translations/da.json | 7 ++++ .../alarm_control_panel/.translations/en.json | 11 +++++ .../alarm_control_panel/.translations/es.json | 11 +++++ .../alarm_control_panel/.translations/it.json | 11 +++++ .../alarm_control_panel/.translations/lb.json | 11 +++++ .../alarm_control_panel/.translations/no.json | 11 +++++ .../.translations/pt-BR.json | 7 ++++ .../alarm_control_panel/.translations/pt.json | 9 ++++ .../alarm_control_panel/.translations/ru.json | 7 ++++ .../alarm_control_panel/.translations/sl.json | 11 +++++ .../.translations/zh-Hant.json | 11 +++++ .../components/axis/.translations/da.json | 1 + .../components/axis/.translations/it.json | 1 + .../components/axis/.translations/lb.json | 1 + .../components/axis/.translations/no.json | 1 + .../components/axis/.translations/ru.json | 1 + .../components/axis/.translations/sl.json | 1 + .../axis/.translations/zh-Hant.json | 1 + .../binary_sensor/.translations/lv.json | 8 ++++ .../binary_sensor/.translations/pt.json | 41 +++++++++++++++++++ .../components/cover/.translations/da.json | 10 +++++ .../components/cover/.translations/it.json | 10 +++++ .../components/cover/.translations/pt.json | 10 +++++ .../components/deconz/.translations/da.json | 1 + .../components/deconz/.translations/fr.json | 1 + .../components/deconz/.translations/it.json | 1 + .../components/deconz/.translations/pt.json | 5 +++ .../components/deconz/.translations/sl.json | 1 + .../components/ecobee/.translations/pt.json | 11 +++++ .../components/heos/.translations/pt.json | 3 +- .../components/light/.translations/lv.json | 8 ++++ .../components/lock/.translations/da.json | 12 ++++++ .../components/lock/.translations/en.json | 5 +++ .../components/lock/.translations/it.json | 8 ++++ .../components/lock/.translations/lb.json | 5 +++ .../components/lock/.translations/no.json | 5 +++ .../components/lock/.translations/ru.json | 5 +++ .../components/lock/.translations/sl.json | 5 +++ .../lock/.translations/zh-Hant.json | 5 +++ .../components/met/.translations/pt.json | 12 ++++++ .../components/neato/.translations/pt-BR.json | 5 +++ .../opentherm_gw/.translations/da.json | 11 +++++ .../opentherm_gw/.translations/en.json | 8 ++-- .../opentherm_gw/.translations/fr.json | 10 +++++ .../opentherm_gw/.translations/lb.json | 11 +++++ .../opentherm_gw/.translations/no.json | 11 +++++ .../opentherm_gw/.translations/pl.json | 10 +++++ .../opentherm_gw/.translations/pt.json | 12 ++++++ .../opentherm_gw/.translations/ru.json | 11 +++++ .../opentherm_gw/.translations/sl.json | 11 +++++ .../opentherm_gw/.translations/zh-Hant.json | 11 +++++ .../components/plex/.translations/pt.json | 21 ++++++++++ .../components/sensor/.translations/hu.json | 26 ++++++++++++ .../sensor/.translations/moon.hu.json | 8 ++-- .../components/sensor/.translations/pt.json | 21 ++++++++++ .../components/soma/.translations/da.json | 10 +++++ .../components/soma/.translations/de.json | 10 +++++ .../components/soma/.translations/es.json | 8 ++++ .../components/soma/.translations/fr.json | 10 +++++ .../components/soma/.translations/it.json | 10 +++++ .../components/soma/.translations/pl.json | 7 ++++ .../components/soma/.translations/pt-BR.json | 12 ++++++ .../components/soma/.translations/pt.json | 12 ++++++ .../components/soma/.translations/sl.json | 10 +++++ .../soma/.translations/zh-Hant.json | 10 +++++ .../components/switch/.translations/lv.json | 12 ++++++ .../transmission/.translations/pt.json | 12 ++++++ .../components/unifi/.translations/pt-BR.json | 6 +++ .../components/unifi/.translations/pt.json | 23 +++++++++++ .../components/wwlln/.translations/pt.json | 12 ++++++ .../components/zha/.translations/pt.json | 8 ++++ 78 files changed, 735 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/abode/.translations/it.json create mode 100644 homeassistant/components/abode/.translations/pl.json create mode 100644 homeassistant/components/abode/.translations/pt-BR.json create mode 100644 homeassistant/components/abode/.translations/pt.json create mode 100644 homeassistant/components/adguard/.translations/pt.json create mode 100644 homeassistant/components/airly/.translations/pt.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/da.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/en.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/es.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/it.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/lb.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/no.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/pt-BR.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/pt.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/ru.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/sl.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/zh-Hant.json create mode 100644 homeassistant/components/binary_sensor/.translations/lv.json create mode 100644 homeassistant/components/binary_sensor/.translations/pt.json create mode 100644 homeassistant/components/cover/.translations/da.json create mode 100644 homeassistant/components/cover/.translations/it.json create mode 100644 homeassistant/components/cover/.translations/pt.json create mode 100644 homeassistant/components/ecobee/.translations/pt.json create mode 100644 homeassistant/components/light/.translations/lv.json create mode 100644 homeassistant/components/lock/.translations/da.json create mode 100644 homeassistant/components/lock/.translations/it.json create mode 100644 homeassistant/components/met/.translations/pt.json create mode 100644 homeassistant/components/neato/.translations/pt-BR.json create mode 100644 homeassistant/components/opentherm_gw/.translations/pt.json create mode 100644 homeassistant/components/plex/.translations/pt.json create mode 100644 homeassistant/components/sensor/.translations/hu.json create mode 100644 homeassistant/components/sensor/.translations/pt.json create mode 100644 homeassistant/components/soma/.translations/pt-BR.json create mode 100644 homeassistant/components/soma/.translations/pt.json create mode 100644 homeassistant/components/switch/.translations/lv.json create mode 100644 homeassistant/components/transmission/.translations/pt.json create mode 100644 homeassistant/components/wwlln/.translations/pt.json diff --git a/homeassistant/components/abode/.translations/fr.json b/homeassistant/components/abode/.translations/fr.json index c2e4b241b90..c0d9b0b577b 100644 --- a/homeassistant/components/abode/.translations/fr.json +++ b/homeassistant/components/abode/.translations/fr.json @@ -1,12 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "Une seule configuration de Abode est autoris\u00e9e." + }, + "error": { + "connection_error": "Impossible de se connecter \u00e0 Abode.", + "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9.", + "invalid_credentials": "Informations d'identification invalides." + }, "step": { "user": { "data": { "password": "Mot de passe", "username": "Adresse e-mail" - } + }, + "title": "Remplissez vos informations de connexion Abode" } - } + }, + "title": "Abode" } } \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/it.json b/homeassistant/components/abode/.translations/it.json new file mode 100644 index 00000000000..af51aca8af9 --- /dev/null +++ b/homeassistant/components/abode/.translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\u00c8 consentita una sola configurazione di Abode." + }, + "error": { + "connection_error": "Impossibile connettersi ad Abode.", + "identifier_exists": "Account gi\u00e0 registrato", + "invalid_credentials": "Credenziali non valide" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Indirizzo email" + }, + "title": "Inserisci le tue informazioni di accesso Abode" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/pl.json b/homeassistant/components/abode/.translations/pl.json new file mode 100644 index 00000000000..09fbdc93241 --- /dev/null +++ b/homeassistant/components/abode/.translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Adres e-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/pt-BR.json b/homeassistant/components/abode/.translations/pt-BR.json new file mode 100644 index 00000000000..7a117a81993 --- /dev/null +++ b/homeassistant/components/abode/.translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "Endere\u00e7o de e-mail" + } + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/.translations/pt.json b/homeassistant/components/abode/.translations/pt.json new file mode 100644 index 00000000000..512bf59906c --- /dev/null +++ b/homeassistant/components/abode/.translations/pt.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "identifier_exists": "Conta j\u00e1 registada" + }, + "step": { + "user": { + "data": { + "username": "Endere\u00e7o de e-mail" + } + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/pt.json b/homeassistant/components/adguard/.translations/pt.json new file mode 100644 index 00000000000..f681da4210f --- /dev/null +++ b/homeassistant/components/adguard/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/pt.json b/homeassistant/components/airly/.translations/pt.json new file mode 100644 index 00000000000..d99bcb90733 --- /dev/null +++ b/homeassistant/components/airly/.translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "" + } + }, + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/da.json b/homeassistant/components/alarm_control_panel/.translations/da.json new file mode 100644 index 00000000000..74e02e10de4 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/da.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "trigger": "Udl\u00f8s {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/en.json b/homeassistant/components/alarm_control_panel/.translations/en.json new file mode 100644 index 00000000000..b8eeb1d2e8c --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/en.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Arm {entity_name} away", + "arm_home": "Arm {entity_name} home", + "arm_night": "Arm {entity_name} night", + "disarm": "Disarm {entity_name}", + "trigger": "Trigger {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/es.json b/homeassistant/components/alarm_control_panel/.translations/es.json new file mode 100644 index 00000000000..a704080a2b4 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/es.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armar {entity_name} exterior", + "arm_home": "Armar {entity_name} casa", + "arm_night": "Armar {entity_name}", + "disarm": "Desarmar {entity_name}", + "trigger": "Lanzar {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/it.json b/homeassistant/components/alarm_control_panel/.translations/it.json new file mode 100644 index 00000000000..e39967e9dac --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/it.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armare {entity_name} uscito", + "arm_home": "Armare {entity_name} casa", + "arm_night": "Armare {entity_name} notte", + "disarm": "Disarmare {entity_name}", + "trigger": "Attivazione {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/lb.json b/homeassistant/components/alarm_control_panel/.translations/lb.json new file mode 100644 index 00000000000..ff265a52c38 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/lb.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} fir \u00ebnnerwee uschalten", + "arm_home": "{entity_name} fir doheem uschalten", + "arm_night": "{entity_name} fir Nuecht uschalten", + "disarm": "{entity_name} entsch\u00e4rfen", + "trigger": "{entity_name} ausl\u00e9isen" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/no.json b/homeassistant/components/alarm_control_panel/.translations/no.json new file mode 100644 index 00000000000..93833f33d41 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/no.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Aktiver {entity_name} borte", + "arm_home": "Aktiver {entity_name} hjemme", + "arm_night": "Aktiver {entity_name} natt", + "disarm": "Deaktiver {entity_name}", + "trigger": "Utl\u00f8ser {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pt-BR.json b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json new file mode 100644 index 00000000000..1f7c994330d --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "trigger": "Disparar {entidade_nome}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pt.json b/homeassistant/components/alarm_control_panel/.translations/pt.json new file mode 100644 index 00000000000..90b9b1d43d5 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/pt.json @@ -0,0 +1,9 @@ +{ + "device_automation": { + "action_type": { + "arm_home": "Armar casa {entity_name}", + "arm_night": "Armar noite {entity_name}", + "disarm": "Desarmar {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/ru.json b/homeassistant/components/alarm_control_panel/.translations/ru.json new file mode 100644 index 00000000000..acea0ae7551 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/ru.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/sl.json b/homeassistant/components/alarm_control_panel/.translations/sl.json new file mode 100644 index 00000000000..9bf01fc62de --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/sl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Vklju\u010di {entity_name} zdoma", + "arm_home": "Vklju\u010di {entity_name} doma", + "arm_night": "Vklju\u010di {entity_name} no\u010d", + "disarm": "Razoro\u017ei {entity_name}", + "trigger": "Spro\u017ei {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json new file mode 100644 index 00000000000..c52288802d1 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "\u8a2d\u5b9a {entity_name} \u5916\u51fa\u6a21\u5f0f", + "arm_home": "\u8a2d\u5b9a {entity_name} \u8fd4\u5bb6\u6a21\u5f0f", + "arm_night": "\u8a2d\u5b9a {entity_name} \u591c\u9593\u6a21\u5f0f", + "disarm": "\u89e3\u9664 {entity_name}", + "trigger": "\u89f8\u767c {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/da.json b/homeassistant/components/axis/.translations/da.json index 2d728468fc7..c169f85f280 100644 --- a/homeassistant/components/axis/.translations/da.json +++ b/homeassistant/components/axis/.translations/da.json @@ -12,6 +12,7 @@ "device_unavailable": "Enheden er ikke tilg\u00e6ngelig", "faulty_credentials": "Ugyldige legitimationsoplysninger" }, + "flow_title": "Axis enhed: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/it.json b/homeassistant/components/axis/.translations/it.json index e979af08836..3f303140c68 100644 --- a/homeassistant/components/axis/.translations/it.json +++ b/homeassistant/components/axis/.translations/it.json @@ -12,6 +12,7 @@ "device_unavailable": "Il dispositivo non \u00e8 disponibile", "faulty_credentials": "Credenziali utente non valide" }, + "flow_title": "Dispositivo Axis: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/lb.json b/homeassistant/components/axis/.translations/lb.json index 281eaa7c881..24ee0e24125 100644 --- a/homeassistant/components/axis/.translations/lb.json +++ b/homeassistant/components/axis/.translations/lb.json @@ -12,6 +12,7 @@ "device_unavailable": "Apparat ass net erreechbar", "faulty_credentials": "Ong\u00eblteg Login Informatioune" }, + "flow_title": "Axis Apparat: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/no.json b/homeassistant/components/axis/.translations/no.json index 29022e39745..190737e5a76 100644 --- a/homeassistant/components/axis/.translations/no.json +++ b/homeassistant/components/axis/.translations/no.json @@ -12,6 +12,7 @@ "device_unavailable": "Enheten er ikke tilgjengelig", "faulty_credentials": "Ugyldig brukerlegitimasjon" }, + "flow_title": "Akse-enhet: {Name} ({Host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index ae5f0851c44..1128ad30cf5 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -12,6 +12,7 @@ "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." }, + "flow_title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/sl.json b/homeassistant/components/axis/.translations/sl.json index 205e901553e..5ffa02e19f7 100644 --- a/homeassistant/components/axis/.translations/sl.json +++ b/homeassistant/components/axis/.translations/sl.json @@ -12,6 +12,7 @@ "device_unavailable": "Naprava ni na voljo", "faulty_credentials": "Napa\u010dni uporabni\u0161ki podatki" }, + "flow_title": "OS naprava: {Name} ({Host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json index c0d0df02135..6c78fc2166c 100644 --- a/homeassistant/components/axis/.translations/zh-Hant.json +++ b/homeassistant/components/axis/.translations/zh-Hant.json @@ -12,6 +12,7 @@ "device_unavailable": "\u8a2d\u5099\u7121\u6cd5\u4f7f\u7528", "faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548" }, + "flow_title": "Axis \u8a2d\u5099\uff1a{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/.translations/lv.json b/homeassistant/components/binary_sensor/.translations/lv.json new file mode 100644 index 00000000000..7668dfa5ac8 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/lv.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} tika izsl\u0113gta", + "turned_on": "{entity_name} tika iesl\u0113gta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/binary_sensor/.translations/pt.json b/homeassistant/components/binary_sensor/.translations/pt.json new file mode 100644 index 00000000000..aa16576d2c1 --- /dev/null +++ b/homeassistant/components/binary_sensor/.translations/pt.json @@ -0,0 +1,41 @@ +{ + "device_automation": { + "condition_type": { + "is_bat_low": "a bateria {entity_name} est\u00e1 baixa", + "is_cold": "{entity_name} est\u00e1 frio", + "is_connected": "{entity_name} est\u00e1 ligado", + "is_gas": "{entity_name} est\u00e1 a detectar g\u00e1s", + "is_hot": "{entity_name} est\u00e1 quente", + "is_light": "{entity_name} est\u00e1 a detectar luz", + "is_locked": "{entity_name} est\u00e1 fechado", + "is_moist": "{entity_name} est\u00e1 h\u00famido", + "is_motion": "{entity_name} est\u00e1 a detectar movimento", + "is_moving": "{entity_name} est\u00e1 a mexer", + "is_not_open": "{entity_name} est\u00e1 fechada", + "is_off": "{entity_name} est\u00e1 desligado", + "is_on": "{entity_name} est\u00e1 ligado", + "is_vibration": "{entity_name} est\u00e1 a detectar vibra\u00e7\u00f5es" + }, + "trigger_type": { + "closed": "{entity_name} est\u00e1 fechado", + "moist": "ficou h\u00famido {entity_name}", + "not_opened": "fechado {entity_name}", + "not_plugged_in": "{entity_name} desligado", + "not_powered": "{entity_name} n\u00e3o alimentado", + "not_present": "ausente {entity_name}", + "not_unsafe": "ficou seguro {entity_name}", + "occupied": "ficou ocupado {entity_name}", + "opened": "{entity_name} aberto", + "plugged_in": "{entity_name} ligado", + "powered": "{entity_name} alimentado", + "present": "{entity_name} presente", + "problem": "foi detectado problema em {entity_name}", + "smoke": "foi detectado fumo em {entity_name}", + "sound": "foram detectadas sons em {entity_name}", + "turned_off": "foi desligado {entity_name}", + "turned_on": "foi ligado {entity_name}", + "unsafe": "ficou inseguro {entity_name}", + "vibration": "foram detectadas vibra\u00e7\u00f5es em {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/da.json b/homeassistant/components/cover/.translations/da.json new file mode 100644 index 00000000000..e603723b564 --- /dev/null +++ b/homeassistant/components/cover/.translations/da.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} er lukket", + "is_closing": "{entity_name} lukker", + "is_open": "{entity_name} er \u00e5ben", + "is_opening": "{entity_name} \u00e5bnes" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/it.json b/homeassistant/components/cover/.translations/it.json new file mode 100644 index 00000000000..6a25c0f3f2f --- /dev/null +++ b/homeassistant/components/cover/.translations/it.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \u00e8 chiuso", + "is_closing": "{entity_name} si sta chiudendo", + "is_open": "{entity_name} \u00e8 aperto", + "is_opening": "{entity_name} si sta aprendo" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/pt.json b/homeassistant/components/cover/.translations/pt.json new file mode 100644 index 00000000000..cb9f85c4a93 --- /dev/null +++ b/homeassistant/components/cover/.translations/pt.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est\u00e1 fechada", + "is_closing": "{entity_name} est\u00e1 a fechar", + "is_open": "{entity_name} est\u00e1 aberta", + "is_opening": "{entity_name} est\u00e1 a abrir" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index 6b74c09107a..ec9c4dc35b1 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -11,6 +11,7 @@ "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8gle" }, + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 3729f7f556a..d1fc7fa7286 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -11,6 +11,7 @@ "error": { "no_key": "Impossible d'obtenir une cl\u00e9 d'API" }, + "flow_title": "Passerelle deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index 1f0b344a32d..975d69a450f 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -11,6 +11,7 @@ "error": { "no_key": "Impossibile ottenere una API key" }, + "flow_title": "Gateway Zigbee deCONZ ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index 47f5bb7db59..63a66595ace 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -29,5 +29,10 @@ } }, "title": "Gateway Zigbee deCONZ" + }, + "device_automation": { + "trigger_subtype": { + "left": "Esquerda" + } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 0717bcfc39f..217007c07d4 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -11,6 +11,7 @@ "error": { "no_key": "Klju\u010da API ni mogo\u010de dobiti" }, + "flow_title": "deCONZ Zigbee prehod ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/ecobee/.translations/pt.json b/homeassistant/components/ecobee/.translations/pt.json new file mode 100644 index 00000000000..20bba0ede4b --- /dev/null +++ b/homeassistant/components/ecobee/.translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Chave da API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/pt.json b/homeassistant/components/heos/.translations/pt.json index 33c83fdc738..099d1978436 100644 --- a/homeassistant/components/heos/.translations/pt.json +++ b/homeassistant/components/heos/.translations/pt.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "access_token": "Servidor" + "access_token": "Servidor", + "host": "Servidor" } } }, diff --git a/homeassistant/components/light/.translations/lv.json b/homeassistant/components/light/.translations/lv.json new file mode 100644 index 00000000000..7668dfa5ac8 --- /dev/null +++ b/homeassistant/components/light/.translations/lv.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "trigger_type": { + "turned_off": "{entity_name} tika izsl\u0113gta", + "turned_on": "{entity_name} tika iesl\u0113gta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/da.json b/homeassistant/components/lock/.translations/da.json new file mode 100644 index 00000000000..de4f603ac43 --- /dev/null +++ b/homeassistant/components/lock/.translations/da.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "lock": "L\u00e5s {entity_name}", + "open": "\u00c5ben {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} er l\u00e5st", + "is_unlocked": "{entity_name} er l\u00e5st op" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/en.json b/homeassistant/components/lock/.translations/en.json index a4b69197a91..a9800eecadd 100644 --- a/homeassistant/components/lock/.translations/en.json +++ b/homeassistant/components/lock/.translations/en.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Lock {entity_name}", + "open": "Open {entity_name}", + "unlock": "Unlock {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} is locked", "is_unlocked": "{entity_name} is unlocked" diff --git a/homeassistant/components/lock/.translations/it.json b/homeassistant/components/lock/.translations/it.json new file mode 100644 index 00000000000..f56ef71060b --- /dev/null +++ b/homeassistant/components/lock/.translations/it.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "condition_type": { + "is_locked": "{entity_name} \u00e8 bloccato", + "is_unlocked": "{entity_name} \u00e8 sbloccato" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/lb.json b/homeassistant/components/lock/.translations/lb.json index 4526b8fb674..90dd7e6087a 100644 --- a/homeassistant/components/lock/.translations/lb.json +++ b/homeassistant/components/lock/.translations/lb.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "{entity_name} sp\u00e4ren", + "open": "{entity_name} opmaachen", + "unlock": "{entity_name} entsp\u00e4ren" + }, "condition_type": { "is_locked": "{entity_name} ass gespaart", "is_unlocked": "{entity_name} ass entspaart" diff --git a/homeassistant/components/lock/.translations/no.json b/homeassistant/components/lock/.translations/no.json index c60fc52ce53..28c809a82d1 100644 --- a/homeassistant/components/lock/.translations/no.json +++ b/homeassistant/components/lock/.translations/no.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "L\u00e5s {entity_name}", + "open": "\u00c5pne {entity_name}", + "unlock": "L\u00e5s opp {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} er l\u00e5st", "is_unlocked": "{entity_name} er l\u00e5st opp" diff --git a/homeassistant/components/lock/.translations/ru.json b/homeassistant/components/lock/.translations/ru.json index f74df838ae5..1610668721f 100644 --- a/homeassistant/components/lock/.translations/ru.json +++ b/homeassistant/components/lock/.translations/ru.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c {entity_name}", + "open": "\u041e\u0442\u043a\u0440\u044b\u0442\u044c {entity_name}", + "unlock": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", "is_unlocked": "{entity_name} \u0432 \u0440\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" diff --git a/homeassistant/components/lock/.translations/sl.json b/homeassistant/components/lock/.translations/sl.json index 3c3fd5defbc..d2e32499d2e 100644 --- a/homeassistant/components/lock/.translations/sl.json +++ b/homeassistant/components/lock/.translations/sl.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Zakleni {entity_name}", + "open": "Odpri {entity_name}", + "unlock": "Odkleni {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} je/so zaklenjen/a", "is_unlocked": "{entity_name} je/so odklenjen/a" diff --git a/homeassistant/components/lock/.translations/zh-Hant.json b/homeassistant/components/lock/.translations/zh-Hant.json index a423c4331b7..7c8abb76e16 100644 --- a/homeassistant/components/lock/.translations/zh-Hant.json +++ b/homeassistant/components/lock/.translations/zh-Hant.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "\u4e0a\u9396 {entity_name}", + "open": "\u958b\u555f {entity_name}", + "unlock": "\u89e3\u9396 {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} \u5df2\u4e0a\u9396", "is_unlocked": "{entity_name} \u5df2\u89e3\u9396" diff --git a/homeassistant/components/met/.translations/pt.json b/homeassistant/components/met/.translations/pt.json new file mode 100644 index 00000000000..c7081cd694a --- /dev/null +++ b/homeassistant/components/met/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/.translations/pt-BR.json b/homeassistant/components/neato/.translations/pt-BR.json new file mode 100644 index 00000000000..8c4c45f9c89 --- /dev/null +++ b/homeassistant/components/neato/.translations/pt-BR.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "" + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json index b8abb48af4e..152e38a5bba 100644 --- a/homeassistant/components/opentherm_gw/.translations/da.json +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -16,5 +16,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Gulvtemperatur", + "precision": "Pr\u00e6cision" + }, + "description": "Indstillinger for OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json index 4aba4ed047a..a7e143505a8 100644 --- a/homeassistant/components/opentherm_gw/.translations/en.json +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -23,11 +23,11 @@ "options": { "step": { "init": { - "description": "Options for the OpenTherm Gateway", "data": { - "floor_temperature": "Floor Temperature", - "precision": "Precision" - } + "floor_temperature": "Floor Temperature", + "precision": "Precision" + }, + "description": "Options for the OpenTherm Gateway" } } } diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json index f5f25da48bd..cfdc6b9a738 100644 --- a/homeassistant/components/opentherm_gw/.translations/fr.json +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -19,5 +19,15 @@ } }, "title": "Passerelle OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "precision": "Pr\u00e9cision" + }, + "description": "Options pour la passerelle OpenTherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/lb.json b/homeassistant/components/opentherm_gw/.translations/lb.json index ec1f719a6cc..505815dcb4d 100644 --- a/homeassistant/components/opentherm_gw/.translations/lb.json +++ b/homeassistant/components/opentherm_gw/.translations/lb.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Buedem Temperatur", + "precision": "Pr\u00e4zisioun" + }, + "description": "Optioune fir OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json index 6104aa7de72..9eb4444cbf1 100644 --- a/homeassistant/components/opentherm_gw/.translations/no.json +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Etasje Temperatur", + "precision": "Presisjon" + }, + "description": "Alternativer for OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index 32e5cde82cb..3d4c643b848 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -19,5 +19,15 @@ } }, "title": "Bramka OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura pod\u0142ogi", + "precision": "Precyzja" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pt.json b/homeassistant/components/opentherm_gw/.translations/pt.json new file mode 100644 index 00000000000..960e3a9cf5c --- /dev/null +++ b/homeassistant/components/opentherm_gw/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "id": "", + "name": "Nome" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json index 718322ec171..f38dd669d24 100644 --- a/homeassistant/components/opentherm_gw/.translations/ru.json +++ b/homeassistant/components/opentherm_gw/.translations/ru.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", + "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0430 Opentherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/sl.json b/homeassistant/components/opentherm_gw/.translations/sl.json index 5de551d5d0c..426459237aa 100644 --- a/homeassistant/components/opentherm_gw/.translations/sl.json +++ b/homeassistant/components/opentherm_gw/.translations/sl.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Prehod" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura nadstropja", + "precision": "Natan\u010dnost" + }, + "description": "Mo\u017enosti za prehod OpenTherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json index 648f156e864..0d2842ce767 100644 --- a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json @@ -19,5 +19,16 @@ } }, "title": "OpenTherm \u9598\u9053\u5668" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", + "precision": "\u6e96\u78ba\u5ea6" + }, + "description": "OpenTherm \u9598\u9053\u5668\u9078\u9805" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/pt.json b/homeassistant/components/plex/.translations/pt.json new file mode 100644 index 00000000000..4312910653f --- /dev/null +++ b/homeassistant/components/plex/.translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "manual_setup": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Mostrar todos os controles" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/hu.json b/homeassistant/components/sensor/.translations/hu.json new file mode 100644 index 00000000000..78ea3e5e89b --- /dev/null +++ b/homeassistant/components/sensor/.translations/hu.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} akku szint", + "is_humidity": "{entity_name} p\u00e1ratartalom", + "is_illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s", + "is_power": "{entity_name} teljes\u00edtm\u00e9ny", + "is_pressure": "{entity_name} nyom\u00e1s", + "is_signal_strength": "{entity_name} jeler\u0151ss\u00e9g", + "is_temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet", + "is_timestamp": "{entity_name} id\u0151b\u00e9lyeg", + "is_value": "{entity_name} \u00e9rt\u00e9k" + }, + "trigger_type": { + "battery_level": "{entity_name} akku szint", + "humidity": "{entity_name} p\u00e1ratartalom", + "illuminance": "{entity_name} megvil\u00e1g\u00edt\u00e1s", + "power": "{entity_name} teljes\u00edtm\u00e9ny", + "pressure": "{entity_name} nyom\u00e1s", + "signal_strength": "{entity_name} jeler\u0151ss\u00e9g", + "temperature": "{entity_name} h\u0151m\u00e9rs\u00e9klet", + "timestamp": "{entity_name} id\u0151b\u00e9lyeg", + "value": "{entity_name} \u00e9rt\u00e9k" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/moon.hu.json b/homeassistant/components/sensor/.translations/moon.hu.json index 0fcd02a6961..fff9f51f50d 100644 --- a/homeassistant/components/sensor/.translations/moon.hu.json +++ b/homeassistant/components/sensor/.translations/moon.hu.json @@ -4,9 +4,9 @@ "full_moon": "Telihold", "last_quarter": "Utols\u00f3 negyed", "new_moon": "\u00dajhold", - "waning_crescent": "Fogy\u00f3 Hold (sarl\u00f3)", - "waning_gibbous": "Fogy\u00f3 Hold", - "waxing_crescent": "N\u00f6v\u0151 Hold (sarl\u00f3)", - "waxing_gibbous": "N\u00f6v\u0151 Hold" + "waning_crescent": "Fogy\u00f3 holdsarl\u00f3", + "waning_gibbous": "Fogy\u00f3 hold", + "waxing_crescent": "N\u00f6v\u0151 holdsarl\u00f3", + "waxing_gibbous": "N\u00f6v\u0151 hold" } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/pt.json b/homeassistant/components/sensor/.translations/pt.json new file mode 100644 index 00000000000..801b22f0c45 --- /dev/null +++ b/homeassistant/components/sensor/.translations/pt.json @@ -0,0 +1,21 @@ +{ + "device_automation": { + "condition_type": { + "is_humidity": "humidade {entity_name}", + "is_power": "pot\u00eancia {entity_name}", + "is_timestamp": "momento temporal de {entity_name}", + "is_value": "valor {entity_name}" + }, + "trigger_type": { + "battery_level": "n\u00edvel da bateria {entity_name}", + "humidity": "humidade {entity_name}", + "illuminance": "ilumin\u00e2ncia {entity_name}", + "power": "pot\u00eancia {entity_name}", + "pressure": "press\u00e3o {entity_name}", + "signal_strength": "for\u00e7a do sinal de {entity_name}", + "temperature": "temperatura de {entity_name}", + "timestamp": "momento temporal de {entity_name}", + "value": "valor {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/da.json b/homeassistant/components/soma/.translations/da.json index a82da0ce24d..557eeab55b1 100644 --- a/homeassistant/components/soma/.translations/da.json +++ b/homeassistant/components/soma/.translations/da.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Godkendt med Soma." }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt", + "port": "Port" + }, + "description": "Indtast forbindelsesindstillinger for din SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/de.json b/homeassistant/components/soma/.translations/de.json index 838d46a6d42..cb08613c07b 100644 --- a/homeassistant/components/soma/.translations/de.json +++ b/homeassistant/components/soma/.translations/de.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Erfolgreich bei Soma authentifiziert." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "description": "Bitte gib die Verbindungsinformationen f\u00fcr SOMA Connect ein.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json index 8126b6ea5ae..b539130ea59 100644 --- a/homeassistant/components/soma/.translations/es.json +++ b/homeassistant/components/soma/.translations/es.json @@ -8,6 +8,14 @@ "create_entry": { "default": "Autenticado con \u00e9xito con Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + } + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/fr.json b/homeassistant/components/soma/.translations/fr.json index e990fb98dc2..a758ab0f615 100644 --- a/homeassistant/components/soma/.translations/fr.json +++ b/homeassistant/components/soma/.translations/fr.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s avec Soma." }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + }, + "description": "Veuillez entrer les param\u00e8tres de connexion de votre SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/it.json b/homeassistant/components/soma/.translations/it.json index ce8e950dacc..1398b2a66be 100644 --- a/homeassistant/components/soma/.translations/it.json +++ b/homeassistant/components/soma/.translations/it.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Autenticato con successo con Soma." }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "description": "Inserisci le impostazioni di connessione del tuo SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json index 0ed881853b8..dc843f29fd5 100644 --- a/homeassistant/components/soma/.translations/pl.json +++ b/homeassistant/components/soma/.translations/pl.json @@ -8,6 +8,13 @@ "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Soma" }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pt-BR.json b/homeassistant/components/soma/.translations/pt-BR.json new file mode 100644 index 00000000000..da05e3b43ae --- /dev/null +++ b/homeassistant/components/soma/.translations/pt-BR.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pt.json b/homeassistant/components/soma/.translations/pt.json new file mode 100644 index 00000000000..f681da4210f --- /dev/null +++ b/homeassistant/components/soma/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/sl.json b/homeassistant/components/soma/.translations/sl.json index 7dd523f366c..b3075208d2c 100644 --- a/homeassistant/components/soma/.translations/sl.json +++ b/homeassistant/components/soma/.translations/sl.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Uspe\u0161no overjen s Soma." }, + "step": { + "user": { + "data": { + "host": "Gostitelj", + "port": "Vrata" + }, + "description": "Prosimo, vnesite nastavitve povezave za va\u0161 SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/zh-Hant.json b/homeassistant/components/soma/.translations/zh-Hant.json index 3d28389ff91..893abe82ee1 100644 --- a/homeassistant/components/soma/.translations/zh-Hant.json +++ b/homeassistant/components/soma/.translations/zh-Hant.json @@ -8,6 +8,16 @@ "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Soma \u8a2d\u5099\u3002" }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "description": "\u8acb\u8f38\u5165 SOMA Connect \u9023\u7dda\u8a2d\u5b9a\u3002", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/switch/.translations/lv.json b/homeassistant/components/switch/.translations/lv.json new file mode 100644 index 00000000000..784a9a37afa --- /dev/null +++ b/homeassistant/components/switch/.translations/lv.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "condition_type": { + "turn_off": "{entity_name} tika izsl\u0113gta", + "turn_on": "{entity_name} tika iesl\u0113gta" + }, + "trigger_type": { + "turned_off": "{entity_name} tika izsl\u0113gta", + "turned_on": "{entity_name} tika iesl\u0113gta" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/pt.json b/homeassistant/components/transmission/.translations/pt.json new file mode 100644 index 00000000000..f681da4210f --- /dev/null +++ b/homeassistant/components/transmission/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Servidor", + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/pt-BR.json b/homeassistant/components/unifi/.translations/pt-BR.json index 113eaa000fe..a57bb33ee7a 100644 --- a/homeassistant/components/unifi/.translations/pt-BR.json +++ b/homeassistant/components/unifi/.translations/pt-BR.json @@ -32,6 +32,12 @@ "track_devices": "Rastrear dispositivos de rede (dispositivos Ubiquiti)", "track_wired_clients": "Incluir clientes de rede com fio" } + }, + "init": { + "data": { + "one": "um", + "other": "uns" + } } } } diff --git a/homeassistant/components/unifi/.translations/pt.json b/homeassistant/components/unifi/.translations/pt.json index 6730a3d258e..c602a58660b 100644 --- a/homeassistant/components/unifi/.translations/pt.json +++ b/homeassistant/components/unifi/.translations/pt.json @@ -22,5 +22,28 @@ } }, "title": "Controlador UniFi" + }, + "options": { + "step": { + "device_tracker": { + "data": { + "detection_time": "Tempo em segundos desde a \u00faltima vez que foi visto at\u00e9 ser considerado afastado", + "track_clients": "Acompanhar clientes da rede", + "track_devices": "Acompanhar dispositivos de rede (dispositivos Ubiquiti)", + "track_wired_clients": "Incluir clientes da rede cablada" + } + }, + "init": { + "data": { + "one": "Vazio", + "other": "Vazios" + } + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Criar sensores de uso de largura de banda para clientes da rede" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/wwlln/.translations/pt.json b/homeassistant/components/wwlln/.translations/pt.json new file mode 100644 index 00000000000..c7081cd694a --- /dev/null +++ b/homeassistant/components/wwlln/.translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/pt.json b/homeassistant/components/zha/.translations/pt.json index 8606a04e197..0c86dc95d09 100644 --- a/homeassistant/components/zha/.translations/pt.json +++ b/homeassistant/components/zha/.translations/pt.json @@ -16,5 +16,13 @@ } }, "title": "ZHA" + }, + "device_automation": { + "action_type": { + "warn": "Avisar" + }, + "trigger_subtype": { + "left": "Esquerda" + } } } \ No newline at end of file From d98114d2ab78c95b9fc63e4c3debe53106d3bcb9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 18:08:54 -0700 Subject: [PATCH 427/639] Guard cloud check (#27901) * Guard cloud check * Fix pos args --- .../components/owntracks/config_flow.py | 5 +++- .../components/smartthings/smartapp.py | 28 ++++++++++++++----- homeassistant/helpers/config_entry_flow.py | 5 +++- .../components/owntracks/test_config_flow.py | 1 + .../smartthings/test_config_flow.py | 2 ++ tests/components/smartthings/test_init.py | 1 + 6 files changed, 33 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 67553ef608f..343a6d90b52 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -78,7 +78,10 @@ class OwnTracksFlow(config_entries.ConfigFlow): async def _get_webhook_id(self): """Generate webhook ID.""" webhook_id = self.hass.components.webhook.async_generate_id() - if self.hass.components.cloud.async_active_subscription(): + if ( + "cloud" in self.hass.config.components + and self.hass.components.cloud.async_active_subscription() + ): webhook_url = await self.hass.components.cloud.async_create_cloudhook( webhook_id ) diff --git a/homeassistant/components/smartthings/smartapp.py b/homeassistant/components/smartthings/smartapp.py index d205c1d245c..ecd4da5dcab 100644 --- a/homeassistant/components/smartthings/smartapp.py +++ b/homeassistant/components/smartthings/smartapp.py @@ -22,7 +22,7 @@ from pysmartthings import ( SubscriptionEntity, ) -from homeassistant.components import cloud, webhook +from homeassistant.components import webhook from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( @@ -88,7 +88,10 @@ async def validate_installed_app(api, installed_app_id: str): def validate_webhook_requirements(hass: HomeAssistantType) -> bool: """Ensure HASS is setup properly to receive webhooks.""" - if cloud.async_active_subscription(hass): + if ( + "cloud" in hass.config.components + and hass.components.cloud.async_active_subscription() + ): return True if hass.data[DOMAIN][CONF_CLOUDHOOK_URL] is not None: return True @@ -102,7 +105,11 @@ def get_webhook_url(hass: HomeAssistantType) -> str: Return the cloudhook if available, otherwise local webhook. """ cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if cloud.async_active_subscription(hass) and cloudhook_url is not None: + if ( + "cloud" in hass.config.components + and hass.components.cloud.async_active_subscription() + and cloudhook_url is not None + ): return cloudhook_url return webhook.async_generate_url(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) @@ -222,10 +229,11 @@ async def setup_smartapp_endpoint(hass: HomeAssistantType): cloudhook_url = config.get(CONF_CLOUDHOOK_URL) if ( cloudhook_url is None - and cloud.async_active_subscription(hass) + and "cloud" in hass.config.components + and hass.components.cloud.async_active_subscription() and not hass.config_entries.async_entries(DOMAIN) ): - cloudhook_url = await cloud.async_create_cloudhook( + cloudhook_url = await hass.components.cloud.async_create_cloudhook( hass, config[CONF_WEBHOOK_ID] ) config[CONF_CLOUDHOOK_URL] = cloudhook_url @@ -273,8 +281,14 @@ async def unload_smartapp_endpoint(hass: HomeAssistantType): return # Remove the cloudhook if it was created cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] - if cloudhook_url and cloud.async_is_logged_in(hass): - await cloud.async_delete_cloudhook(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) + if ( + cloudhook_url + and "cloud" in hass.config.components + and hass.components.cloud.async_is_logged_in() + ): + await hass.components.cloud.async_delete_cloudhook( + hass, hass.data[DOMAIN][CONF_WEBHOOK_ID] + ) # Remove cloudhook from storage store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) await store.async_save( diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 88aae3721b1..374ef795846 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -124,7 +124,10 @@ class WebhookFlowHandler(config_entries.ConfigFlow): webhook_id = self.hass.components.webhook.async_generate_id() - if self.hass.components.cloud.async_active_subscription(): + if ( + "cloud" in self.hass.config.components + and self.hass.components.cloud.async_active_subscription() + ): webhook_url = await self.hass.components.cloud.async_create_cloudhook( webhook_id ) diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 76863c61698..c4e2a54f69a 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -43,6 +43,7 @@ async def test_config_flow_unload(hass): async def test_with_cloud_sub(hass): """Test creating a config flow while subscribed.""" + hass.config.components.add("cloud") with patch( "homeassistant.components.cloud.async_active_subscription", return_value=True ), patch( diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index fce0129a7bf..521f1c6a6a8 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -205,6 +205,8 @@ async def test_cloudhook_app_created_then_show_wait_form( hass, app, app_oauth_client, smartthings_mock ): """Test SmartApp is created with a cloudhoko and shows wait form.""" + hass.config.components.add("cloud") + # Unload the endpoint so we can reload it under the cloud. await smartapp.unload_smartapp_endpoint(hass) diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 9749ab9bb71..b8cd65f5a0b 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -268,6 +268,7 @@ async def test_remove_entry(hass, config_entry, smartthings_mock): async def test_remove_entry_cloudhook(hass, config_entry, smartthings_mock): """Test that the installed app, app, and cloudhook are removed up.""" + hass.config.components.add("cloud") # Arrange config_entry.add_to_hass(hass) hass.data[DOMAIN][CONF_CLOUDHOOK_URL] = "https://test.cloud" From 914ceea72d4dd7afd10ab4fdb0fbe0d3d38b3188 Mon Sep 17 00:00:00 2001 From: foreign-sub <51928805+foreign-sub@users.noreply.github.com> Date: Sat, 19 Oct 2019 03:09:41 +0200 Subject: [PATCH 428/639] Bump keyring to 19.2.0 (#27899) --- homeassistant/scripts/keyring.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/scripts/keyring.py b/homeassistant/scripts/keyring.py index 6ca422b595b..0c5623a50ad 100644 --- a/homeassistant/scripts/keyring.py +++ b/homeassistant/scripts/keyring.py @@ -8,7 +8,7 @@ from homeassistant.util.yaml import _SECRET_NAMESPACE # mypy: allow-untyped-defs -REQUIREMENTS = ["keyring==17.1.1", "keyrings.alt==3.1.1"] +REQUIREMENTS = ["keyring==19.2.0", "keyrings.alt==3.1.1"] def run(args): diff --git a/requirements_all.txt b/requirements_all.txt index 58a927c81ab..80be965c415 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -721,7 +721,7 @@ kaiterra-async-client==0.0.2 keba-kecontact==0.2.0 # homeassistant.scripts.keyring -keyring==17.1.1 +keyring==19.2.0 # homeassistant.scripts.keyring keyrings.alt==3.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24122915fb5..99a292d82f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -267,7 +267,7 @@ influxdb==5.2.3 jsonpath==0.75 # homeassistant.scripts.keyring -keyring==17.1.1 +keyring==19.2.0 # homeassistant.scripts.keyring keyrings.alt==3.1.1 From 6391a68fd59fccec64c7f68a3daf2584f4e66a2e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 18:11:54 -0700 Subject: [PATCH 429/639] Better header check for OAuth2 helper (#27897) --- homeassistant/helpers/config_entry_oauth2_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 043a28cac27..7fb954378ee 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -392,7 +392,7 @@ async def async_oauth2_request( url, **kwargs, headers={ - **kwargs.get("headers", {}), + **(kwargs.get("headers") or {}), "authorization": f"Bearer {token['access_token']}", }, ) From 7ed659151c010f67af43e82e891b9cb9f74ffcaf Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 19 Oct 2019 04:49:08 +0200 Subject: [PATCH 430/639] Vacuum reproduce state (#27868) * Vacuum reproduce state * Add missing states * Improved process * Fix tests --- .../components/vacuum/reproduce_state.py | 101 +++++++++++++ .../components/vacuum/test_reproduce_state.py | 139 ++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 homeassistant/components/vacuum/reproduce_state.py create mode 100644 tests/components/vacuum/test_reproduce_state.py diff --git a/homeassistant/components/vacuum/reproduce_state.py b/homeassistant/components/vacuum/reproduce_state.py new file mode 100644 index 00000000000..485ffef0c9f --- /dev/null +++ b/homeassistant/components/vacuum/reproduce_state.py @@ -0,0 +1,101 @@ +"""Reproduce an Vacuum state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_FAN_SPEED, + DOMAIN, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + SERVICE_STOP, + STATE_CLEANING, + STATE_DOCKED, + STATE_RETURNING, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES_TOGGLE = {STATE_ON, STATE_OFF} +VALID_STATES_STATE = { + STATE_CLEANING, + STATE_DOCKED, + STATE_IDLE, + STATE_RETURNING, + STATE_PAUSED, +} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES_TOGGLE and state.state not in VALID_STATES_STATE: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and cur_state.attributes.get( + ATTR_FAN_SPEED + ) == state.attributes.get(ATTR_FAN_SPEED): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if cur_state.state != state.state: + # Wrong state + if state.state == STATE_ON: + service = SERVICE_TURN_ON + elif state.state == STATE_OFF: + service = SERVICE_TURN_OFF + elif state.state == STATE_CLEANING: + service = SERVICE_START + elif state.state == STATE_DOCKED or state.state == STATE_RETURNING: + service = SERVICE_RETURN_TO_BASE + elif state.state == STATE_IDLE: + service = SERVICE_STOP + elif state.state == STATE_PAUSED: + service = SERVICE_PAUSE + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + if cur_state.attributes.get(ATTR_FAN_SPEED) != state.attributes.get(ATTR_FAN_SPEED): + # Wrong fan speed + service_data["fan_speed"] = state.attributes[ATTR_FAN_SPEED] + await hass.services.async_call( + DOMAIN, SERVICE_SET_FAN_SPEED, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Vacuum states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/vacuum/test_reproduce_state.py b/tests/components/vacuum/test_reproduce_state.py new file mode 100644 index 00000000000..d5a7051e6a6 --- /dev/null +++ b/tests/components/vacuum/test_reproduce_state.py @@ -0,0 +1,139 @@ +"""Test reproduce state for Vacuum.""" +from homeassistant.components.vacuum import ( + ATTR_FAN_SPEED, + SERVICE_PAUSE, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START, + SERVICE_STOP, + STATE_CLEANING, + STATE_DOCKED, + STATE_RETURNING, +) +from homeassistant.const import ( + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_IDLE, + STATE_OFF, + STATE_ON, + STATE_PAUSED, +) +from homeassistant.core import State + +from tests.common import async_mock_service + +FAN_SPEED_LOW = "low" +FAN_SPEED_HIGH = "high" + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Vacuum states.""" + hass.states.async_set("vacuum.entity_off", STATE_OFF, {}) + hass.states.async_set("vacuum.entity_on", STATE_ON, {}) + hass.states.async_set( + "vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_LOW} + ) + hass.states.async_set("vacuum.entity_cleaning", STATE_CLEANING, {}) + hass.states.async_set("vacuum.entity_docked", STATE_DOCKED, {}) + hass.states.async_set("vacuum.entity_idle", STATE_IDLE, {}) + hass.states.async_set("vacuum.entity_returning", STATE_RETURNING, {}) + hass.states.async_set("vacuum.entity_paused", STATE_PAUSED, {}) + + turn_on_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_ON) + turn_off_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_OFF) + start_calls = async_mock_service(hass, "vacuum", SERVICE_START) + pause_calls = async_mock_service(hass, "vacuum", SERVICE_PAUSE) + stop_calls = async_mock_service(hass, "vacuum", SERVICE_STOP) + return_calls = async_mock_service(hass, "vacuum", SERVICE_RETURN_TO_BASE) + fan_speed_calls = async_mock_service(hass, "vacuum", SERVICE_SET_FAN_SPEED) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("vacuum.entity_off", STATE_OFF), + State("vacuum.entity_on", STATE_ON), + State("vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_LOW}), + State("vacuum.entity_cleaning", STATE_CLEANING), + State("vacuum.entity_docked", STATE_DOCKED), + State("vacuum.entity_idle", STATE_IDLE), + State("vacuum.entity_returning", STATE_RETURNING), + State("vacuum.entity_paused", STATE_PAUSED), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(stop_calls) == 0 + assert len(return_calls) == 0 + assert len(fan_speed_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("vacuum.entity_off", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(turn_on_calls) == 0 + assert len(turn_off_calls) == 0 + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(stop_calls) == 0 + assert len(return_calls) == 0 + assert len(fan_speed_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("vacuum.entity_off", STATE_ON), + State("vacuum.entity_on", STATE_OFF), + State("vacuum.entity_on_fan", STATE_ON, {ATTR_FAN_SPEED: FAN_SPEED_HIGH}), + State("vacuum.entity_cleaning", STATE_PAUSED), + State("vacuum.entity_docked", STATE_CLEANING), + State("vacuum.entity_idle", STATE_DOCKED), + State("vacuum.entity_returning", STATE_CLEANING), + State("vacuum.entity_paused", STATE_IDLE), + # Should not raise + State("vacuum.non_existing", STATE_ON), + ], + blocking=True, + ) + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "vacuum" + assert turn_on_calls[0].data == {"entity_id": "vacuum.entity_off"} + + assert len(turn_off_calls) == 1 + assert turn_off_calls[0].domain == "vacuum" + assert turn_off_calls[0].data == {"entity_id": "vacuum.entity_on"} + + assert len(start_calls) == 2 + entities = [ + {"entity_id": "vacuum.entity_docked"}, + {"entity_id": "vacuum.entity_returning"}, + ] + for call in start_calls: + assert call.domain == "vacuum" + assert call.data in entities + entities.remove(call.data) + + assert len(pause_calls) == 1 + assert pause_calls[0].domain == "vacuum" + assert pause_calls[0].data == {"entity_id": "vacuum.entity_cleaning"} + + assert len(stop_calls) == 1 + assert stop_calls[0].domain == "vacuum" + assert stop_calls[0].data == {"entity_id": "vacuum.entity_paused"} + + assert len(return_calls) == 1 + assert return_calls[0].domain == "vacuum" + assert return_calls[0].data == {"entity_id": "vacuum.entity_idle"} + + assert len(fan_speed_calls) == 1 + assert fan_speed_calls[0].domain == "vacuum" + assert fan_speed_calls[0].data == { + "entity_id": "vacuum.entity_on_fan", + ATTR_FAN_SPEED: FAN_SPEED_HIGH, + } From 435cbb7f7eb91886f5ea4e51757cb9a2170ef8f7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 19 Oct 2019 05:51:53 +0200 Subject: [PATCH 431/639] Azure pytest parallel (#27864) * Azure pytest parallel * Update azure-pipelines-ci.yml * Remove test that does nothing --- azure-pipelines-ci.yml | 8 ++++---- tests/components/ps4/test_media_player.py | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index f03c5f435f9..82708151ad6 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -108,13 +108,13 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt' + keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt | "v2"' build: | set -e python -m venv venv . venv/bin/activate - pip install -U pip setuptools pytest-azurepipelines -c homeassistant/package_constraints.txt + pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. # Find offending deps with `pipdeptree -r -p typing` @@ -127,7 +127,7 @@ stages: set -e . venv/bin/activate - pytest --timeout=9 --durations=10 -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 -n 2 --dist loadfile -qq -o console_output_style=count -p no:sugar tests script/check_dirty displayName: 'Run pytest for python $(python.container)' condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) @@ -135,7 +135,7 @@ stages: set -e . venv/bin/activate - pytest --timeout=9 --durations=10 --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests + pytest --timeout=9 --durations=10 -n 2 --dist loadfile --cov homeassistant --cov-report html -qq -o console_output_style=count -p no:sugar tests codecov --token $(codecovToken) script/check_dirty displayName: 'Run pytest for python $(python.container) / coverage' diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index d6eeb31695c..7bf93e37777 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -169,11 +169,6 @@ async def mock_ddp_response(hass, mock_status_data, games=None): await hass.async_block_till_done() -async def test_async_setup_platform_does_nothing(): - """Test setup platform does nothing (Uses config entries only).""" - await ps4.media_player.async_setup_platform(None, None, None) - - async def test_media_player_is_setup_correctly_with_entry(hass): """Test entity is setup correctly with entry correctly.""" mock_entity_id = await setup_mock_component(hass) From 2110fea02bbda5cb9ef5565146b0e2f2e6101f36 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Fri, 18 Oct 2019 20:57:36 -0700 Subject: [PATCH 432/639] Move import for htu21d component (#27908) --- homeassistant/components/htu21d/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/htu21d/sensor.py b/homeassistant/components/htu21d/sensor.py index f94b11d5ada..954ba60abbf 100644 --- a/homeassistant/components/htu21d/sensor.py +++ b/homeassistant/components/htu21d/sensor.py @@ -3,11 +3,13 @@ from datetime import timedelta from functools import partial import logging +from i2csense.htu21d import HTU21D # pylint: disable=import-error +import smbus # pylint: disable=import-error import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, TEMP_FAHRENHEIT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle from homeassistant.util.temperature import celsius_to_fahrenheit @@ -34,9 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the HTU21D sensor.""" - import smbus # pylint: disable=import-error - from i2csense.htu21d import HTU21D # pylint: disable=import-error - name = config.get(CONF_NAME) bus_number = config.get(CONF_I2C_BUS) temp_unit = hass.config.units.temperature_unit From 0cd55d6716244e260464c74d4e9a382fea46fa83 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Fri, 18 Oct 2019 20:57:47 -0700 Subject: [PATCH 433/639] Move imports for hp_ilo components (#27906) --- homeassistant/components/hp_ilo/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/hp_ilo/sensor.py b/homeassistant/components/hp_ilo/sensor.py index cf95c21a8d1..04c715dc010 100644 --- a/homeassistant/components/hp_ilo/sensor.py +++ b/homeassistant/components/hp_ilo/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import hpilo import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -180,8 +181,6 @@ class HpIloData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from HP iLO.""" - import hpilo - try: self.data = hpilo.Ilo( hostname=self._host, From 00521b5e802f25c41d439dab83ea0af42f95c232 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 20:57:54 -0700 Subject: [PATCH 434/639] Fix flaky integration test (#27905) --- tests/components/integration/test_sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index 48d4178e992..c65ca720235 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -200,4 +200,4 @@ async def test_suffix(hass): assert state is not None # Testing a network speed sensor at 1000 bytes/s over 10s = 10kbytes - assert round(float(state.state), config["sensor"]["round"]) == 10.0 + assert round(float(state.state)) == 10 From 1e727f339fd3328b7930a2700dc9f6ffd6423a77 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Fri, 18 Oct 2019 20:58:07 -0700 Subject: [PATCH 435/639] Move imports in harmony component (#27904) --- homeassistant/components/harmony/remote.py | 27 ++++++---------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index b78f276bf28..118af7fe34a 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -3,6 +3,12 @@ import asyncio import json import logging +import aioharmony.exceptions as aioexc +from aioharmony.harmonyapi import ( + ClientCallbackType, + HarmonyAPI as HarmonyClient, + SendCommandDevice, +) import voluptuous as vol from homeassistant.components import remote @@ -23,8 +29,8 @@ from homeassistant.const import ( CONF_PORT, EVENT_HOMEASSISTANT_STOP, ) -import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv from homeassistant.util import slugify _LOGGER = logging.getLogger(__name__) @@ -165,8 +171,6 @@ class HarmonyRemote(remote.RemoteDevice): def __init__(self, name, host, port, activity, out_path, delay_secs): """Initialize HarmonyRemote class.""" - from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient - self._name = name self.host = host self.port = port @@ -180,8 +184,6 @@ class HarmonyRemote(remote.RemoteDevice): async def async_added_to_hass(self): """Complete the initialization.""" - from aioharmony.harmonyapi import ClientCallbackType - _LOGGER.debug("%s: Harmony Hub added", self._name) # Register the callbacks self._client.callbacks = ClientCallbackType( @@ -195,8 +197,6 @@ class HarmonyRemote(remote.RemoteDevice): # activity await self.new_config() - import aioharmony.exceptions as aioexc - async def shutdown(_): """Close connection on shutdown.""" _LOGGER.debug("%s: Closing Harmony Hub", self._name) @@ -234,8 +234,6 @@ class HarmonyRemote(remote.RemoteDevice): async def connect(self): """Connect to the Harmony HUB.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Connecting", self._name) try: if not await self._client.connect(): @@ -284,8 +282,6 @@ class HarmonyRemote(remote.RemoteDevice): async def async_turn_on(self, **kwargs): """Start an activity from the Harmony device.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Turn On", self.name) activity = kwargs.get(ATTR_ACTIVITY, self._default_activity) @@ -314,8 +310,6 @@ class HarmonyRemote(remote.RemoteDevice): async def async_turn_off(self, **kwargs): """Start the PowerOff activity.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Turn Off", self.name) try: await self._client.power_off() @@ -325,9 +319,6 @@ class HarmonyRemote(remote.RemoteDevice): # pylint: disable=arguments-differ async def async_send_command(self, command, **kwargs): """Send a list of commands to one device.""" - from aioharmony.harmonyapi import SendCommandDevice - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Send Command", self.name) device = kwargs.get(ATTR_DEVICE) if device is None: @@ -390,8 +381,6 @@ class HarmonyRemote(remote.RemoteDevice): async def change_channel(self, channel): """Change the channel using Harmony remote.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Changing channel to %s", self.name, channel) try: await self._client.change_channel(channel) @@ -400,8 +389,6 @@ class HarmonyRemote(remote.RemoteDevice): async def sync(self): """Sync the Harmony device with the web service.""" - import aioharmony.exceptions as aioexc - _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name) try: await self._client.sync() From 37b23e9205350a456c28255f1171e238f7fdf141 Mon Sep 17 00:00:00 2001 From: Brig Lamoreaux Date: Fri, 18 Oct 2019 20:58:15 -0700 Subject: [PATCH 436/639] Move imports to top for harman_kardon_avr (#27903) --- .../components/harman_kardon_avr/media_player.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/harman_kardon_avr/media_player.py b/homeassistant/components/harman_kardon_avr/media_player.py index 01948943adf..fd7cddcaed9 100644 --- a/homeassistant/components/harman_kardon_avr/media_player.py +++ b/homeassistant/components/harman_kardon_avr/media_player.py @@ -1,18 +1,19 @@ """Support for interface with an Harman/Kardon or JBL AVR.""" import logging +import hkavr import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_STEP, - SUPPORT_TURN_ON, - SUPPORT_SELECT_SOURCE, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_OFF, STATE_ON +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -38,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discover_info=None): """Set up the AVR platform.""" - import hkavr - name = config[CONF_NAME] host = config[CONF_HOST] port = config[CONF_PORT] From 63031173548e70f320fffc5d4ef3fb8b8cbc35c8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 18 Oct 2019 20:58:43 -0700 Subject: [PATCH 437/639] Dont create coroutine until acting on it (#27907) --- .../google_assistant/report_state.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index b842a552714..aacb90e9d2b 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -49,23 +49,23 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig {"devices": {"states": {changed_entity: entity_data}}} ) - async_call_later( - hass, INITIAL_REPORT_DELAY, _async_report_all_states(hass, google_config) - ) + async def inital_report(_now): + """Report initially all states.""" + entities = {} + + for entity in async_get_entities(hass, google_config): + if not entity.should_expose(): + continue + + try: + entities[entity.entity_id] = entity.query_serialize() + except SmartHomeError: + continue + + await google_config.async_report_state({"devices": {"states": entities}}) + + async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener ) - - -async def _async_report_all_states(hass: HomeAssistant, google_config: AbstractConfig): - """Report all states.""" - entities = {} - - for entity in async_get_entities(hass, google_config): - if not entity.should_expose(): - continue - - entities[entity.entity_id] = entity.query_serialize() - - await google_config.async_report_state({"devices": {"states": entities}}) From 2bd9f5680dcfc38e11a77884af36f76ed72ea9bf Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sat, 19 Oct 2019 07:37:44 +0200 Subject: [PATCH 438/639] Report state (#27759) * Add report state config * Add initial steps for local report state * Use owner of system as user_id * First working prototype * Only report state if requested * Add some good logging and adjust constant name * Move jwt generation out to non member * Move cache out to caller * Remove todo about cache * Move getting token out of class * Add timeout on calls * Validate config dependency * Support using service key to do sync call when api_key is not set * Make sure timezone is fixed for datetime dummy * Use exact expire_in time * Support renewing token on 401 * Test retry on 401 * No need to declare dummy key twice * Correct some docs on functions * Add test for token expiry --- .../components/google_assistant/__init__.py | 25 +++ .../components/google_assistant/const.py | 7 + .../components/google_assistant/http.py | 141 +++++++++++++++- .../components/google_assistant/test_http.py | 157 ++++++++++++++++++ 4 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 tests/components/google_assistant/test_http.py diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index a1252d67fff..ebf906b6f2a 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -28,9 +28,13 @@ from .const import ( CONF_ENTITY_CONFIG, CONF_EXPOSE, CONF_ALIASES, + CONF_REPORT_STATE, CONF_ROOM_HINT, CONF_ALLOW_UNLOCK, CONF_SECURE_DEVICES_PIN, + CONF_SERVICE_ACCOUNT, + CONF_CLIENT_EMAIL, + CONF_PRIVATE_KEY, ) from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401 from .const import EVENT_QUERY_RECEIVED # noqa: F401 @@ -47,6 +51,24 @@ ENTITY_SCHEMA = vol.Schema( } ) +GOOGLE_SERVICE_ACCOUNT = vol.Schema( + { + vol.Required(CONF_PRIVATE_KEY): cv.string, + vol.Required(CONF_CLIENT_EMAIL): cv.string, + }, + extra=vol.ALLOW_EXTRA, +) + + +def _check_report_state(data): + if data[CONF_REPORT_STATE]: + if CONF_SERVICE_ACCOUNT not in data: + raise vol.Invalid( + "If report state is enabled, a service account must exist" + ) + return data + + GOOGLE_ASSISTANT_SCHEMA = vol.All( cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version="0.95"), vol.Schema( @@ -63,9 +85,12 @@ GOOGLE_ASSISTANT_SCHEMA = vol.All( vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean, # str on purpose, makes sure it is configured correctly. vol.Optional(CONF_SECURE_DEVICES_PIN): str, + vol.Optional(CONF_REPORT_STATE, default=False): cv.boolean, + vol.Optional(CONF_SERVICE_ACCOUNT): GOOGLE_SERVICE_ACCOUNT, }, extra=vol.PREVENT_EXTRA, ), + _check_report_state, ) CONFIG_SCHEMA = vol.Schema({DOMAIN: GOOGLE_ASSISTANT_SCHEMA}, extra=vol.ALLOW_EXTRA) diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 54abd54caaf..03253e244fe 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -32,6 +32,10 @@ CONF_API_KEY = "api_key" CONF_ROOM_HINT = "room" CONF_ALLOW_UNLOCK = "allow_unlock" CONF_SECURE_DEVICES_PIN = "secure_devices_pin" +CONF_REPORT_STATE = "report_state" +CONF_SERVICE_ACCOUNT = "service_account" +CONF_CLIENT_EMAIL = "client_email" +CONF_PRIVATE_KEY = "private_key" DEFAULT_EXPOSE_BY_DEFAULT = True DEFAULT_EXPOSED_DOMAINS = [ @@ -72,7 +76,10 @@ TYPE_ALARM = PREFIX_TYPES + "SECURITYSYSTEM" SERVICE_REQUEST_SYNC = "request_sync" HOMEGRAPH_URL = "https://homegraph.googleapis.com/" +HOMEGRAPH_SCOPE = "https://www.googleapis.com/auth/homegraph" +HOMEGRAPH_TOKEN_URL = "https://accounts.google.com/o/oauth2/token" REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + "v1/devices:requestSync" +REPORT_STATE_BASE_URL = HOMEGRAPH_URL + "v1/devices:reportStateAndNotification" # Error codes used for SmartHomeError class # https://developers.google.com/actions/reference/smarthome/errors-exceptions diff --git a/homeassistant/components/google_assistant/http.py b/homeassistant/components/google_assistant/http.py index aea226348b8..90fa1ced157 100644 --- a/homeassistant/components/google_assistant/http.py +++ b/homeassistant/components/google_assistant/http.py @@ -1,20 +1,38 @@ """Support for Google Actions Smart Home Control.""" +import asyncio +from datetime import timedelta import logging +from uuid import uuid4 +import jwt +from aiohttp import ClientResponseError, ClientError from aiohttp.web import Request, Response # Typing imports from homeassistant.components.http import HomeAssistantView -from homeassistant.core import callback +from homeassistant.core import callback, ServiceCall from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.util import dt as dt_util from .const import ( GOOGLE_ASSISTANT_API_ENDPOINT, + CONF_API_KEY, CONF_EXPOSE_BY_DEFAULT, CONF_EXPOSED_DOMAINS, CONF_ENTITY_CONFIG, CONF_EXPOSE, + CONF_REPORT_STATE, CONF_SECURE_DEVICES_PIN, + CONF_SERVICE_ACCOUNT, + CONF_CLIENT_EMAIL, + CONF_PRIVATE_KEY, + DOMAIN, + HOMEGRAPH_TOKEN_URL, + HOMEGRAPH_SCOPE, + REPORT_STATE_BASE_URL, + REQUEST_SYNC_BASE_URL, + SERVICE_REQUEST_SYNC, ) from .smart_home import async_handle_message from .helpers import AbstractConfig @@ -22,6 +40,35 @@ from .helpers import AbstractConfig _LOGGER = logging.getLogger(__name__) +def _get_homegraph_jwt(time, iss, key): + now = int(time.timestamp()) + + jwt_raw = { + "iss": iss, + "scope": HOMEGRAPH_SCOPE, + "aud": HOMEGRAPH_TOKEN_URL, + "iat": now, + "exp": now + 3600, + } + return jwt.encode(jwt_raw, key, algorithm="RS256").decode("utf-8") + + +async def _get_homegraph_token(hass, jwt_signed): + headers = { + "Authorization": "Bearer {}".format(jwt_signed), + "Content-Type": "application/x-www-form-urlencoded", + } + data = { + "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", + "assertion": jwt_signed, + } + + session = async_get_clientsession(hass) + async with session.post(HOMEGRAPH_TOKEN_URL, headers=headers, data=data) as res: + res.raise_for_status() + return await res.json() + + class GoogleConfig(AbstractConfig): """Config for manual setup of Google.""" @@ -29,6 +76,8 @@ class GoogleConfig(AbstractConfig): """Initialize the config.""" super().__init__(hass) self._config = config + self._access_token = None + self._access_token_renew = None @property def enabled(self): @@ -50,6 +99,12 @@ class GoogleConfig(AbstractConfig): """Return entity config.""" return self._config.get(CONF_SECURE_DEVICES_PIN) + @property + def should_report_state(self): + """Return if states should be proactively reported.""" + # pylint: disable=no-self-use + return self._config.get(CONF_REPORT_STATE) + def should_expose(self, state) -> bool: """Return if entity should be exposed.""" expose_by_default = self._config.get(CONF_EXPOSE_BY_DEFAULT) @@ -79,11 +134,93 @@ class GoogleConfig(AbstractConfig): """If an entity should have 2FA checked.""" return True + async def _async_update_token(self, force=False): + if CONF_SERVICE_ACCOUNT not in self._config: + _LOGGER.error("Trying to get homegraph api token without service account") + return + + now = dt_util.utcnow() + if not self._access_token or now > self._access_token_renew or force: + token = await _get_homegraph_token( + self.hass, + _get_homegraph_jwt( + now, + self._config[CONF_SERVICE_ACCOUNT][CONF_CLIENT_EMAIL], + self._config[CONF_SERVICE_ACCOUNT][CONF_PRIVATE_KEY], + ), + ) + self._access_token = token["access_token"] + self._access_token_renew = now + timedelta(seconds=token["expires_in"]) + + async def async_call_homegraph_api(self, url, data): + """Call a homegraph api with authenticaiton.""" + session = async_get_clientsession(self.hass) + + async def _call(): + headers = { + "Authorization": "Bearer {}".format(self._access_token), + "X-GFE-SSL": "yes", + } + async with session.post(url, headers=headers, json=data) as res: + _LOGGER.debug( + "Response on %s with data %s was %s", url, data, await res.text() + ) + res.raise_for_status() + + try: + await self._async_update_token() + try: + await _call() + except ClientResponseError as error: + if error.status == 401: + _LOGGER.warning( + "Request for %s unauthorized, renewing token and retrying", url + ) + await self._async_update_token(True) + await _call() + else: + raise + except ClientResponseError as error: + _LOGGER.error("Request for %s failed: %d", url, error.status) + except (asyncio.TimeoutError, ClientError): + _LOGGER.error("Could not contact %s", url) + + async def async_report_state(self, message): + """Send a state report to Google.""" + data = { + "requestId": uuid4().hex, + "agentUserId": (await self.hass.auth.async_get_owner()).id, + "payload": message, + } + await self.async_call_homegraph_api(REPORT_STATE_BASE_URL, data) + @callback def async_register_http(hass, cfg): """Register HTTP views for Google Assistant.""" - hass.http.register_view(GoogleAssistantView(GoogleConfig(hass, cfg))) + config = GoogleConfig(hass, cfg) + hass.http.register_view(GoogleAssistantView(config)) + if config.should_report_state: + config.async_enable_report_state() + + async def request_sync_service_handler(call: ServiceCall): + """Handle request sync service calls.""" + agent_user_id = call.data.get("agent_user_id") or call.context.user_id + + if agent_user_id is None: + _LOGGER.warning( + "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." + ) + return + await config.async_call_homegraph_api( + REQUEST_SYNC_BASE_URL, {"agentUserId": agent_user_id} + ) + + # Register service only if api key is provided + if CONF_API_KEY not in cfg and CONF_SERVICE_ACCOUNT in cfg: + hass.services.async_register( + DOMAIN, SERVICE_REQUEST_SYNC, request_sync_service_handler + ) class GoogleAssistantView(HomeAssistantView): diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py new file mode 100644 index 00000000000..4b26bbeba7f --- /dev/null +++ b/tests/components/google_assistant/test_http.py @@ -0,0 +1,157 @@ +"""Test Google http services.""" +from datetime import datetime, timezone, timedelta +from asynctest import patch, ANY + +from homeassistant.components.google_assistant.http import ( + GoogleConfig, + _get_homegraph_jwt, + _get_homegraph_token, +) +from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA +from homeassistant.components.google_assistant.const import ( + REPORT_STATE_BASE_URL, + HOMEGRAPH_TOKEN_URL, +) +from homeassistant.auth.models import User + +DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA( + { + "project_id": "1234", + "service_account": { + "private_key": "-----BEGIN PRIVATE KEY-----\nMIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAKYscIlwm7soDsHAz6L6YvUkCvkrX19rS6yeYOmovvhoK5WeYGWUsd8V72zmsyHB7XO94YgJVjvxfzn5K8bLePjFzwoSJjZvhBJ/ZQ05d8VmbvgyWUoPdG9oEa4fZ/lCYrXoaFdTot2xcJvrb/ZuiRl4s4eZpNeFYvVK/Am7UeFPAgMBAAECgYAUetOfzLYUudofvPCaKHu7tKZ5kQPfEa0w6BAPnBF1Mfl1JiDBRDMryFtKs6AOIAVwx00dY/Ex0BCbB3+Cr58H7t4NaPTJxCpmR09pK7o17B7xAdQv8+SynFNud9/5vQ5AEXMOLNwKiU7wpXT6Z7ZIibUBOR7ewsWgsHCDpN1iqQJBAOMODPTPSiQMwRAUHIc6GPleFSJnIz2PAoG3JOG9KFAL6RtIc19lob2ZXdbQdzKtjSkWo+O5W20WDNAl1k32h6MCQQC7W4ZCIY67mPbL6CxXfHjpSGF4Dr9VWJ7ZrKHr6XUoOIcEvsn/pHvWonjMdy93rQMSfOE8BKd/I1+GHRmNVgplAkAnSo4paxmsZVyfeKt7Jy2dMY+8tVZe17maUuQaAE7Sk00SgJYegwrbMYgQnWCTL39HBfj0dmYA2Zj8CCAuu6O7AkEAryFiYjaUAO9+4iNoL27+ZrFtypeeadyov7gKs0ZKaQpNyzW8A+Zwi7TbTeSqzic/E+z/bOa82q7p/6b7141xsQJBANCAcIwMcVb6KVCHlQbOtKspo5Eh4ZQi8bGl+IcwbQ6JSxeTx915IfAldgbuU047wOB04dYCFB2yLDiUGVXTifU=\n-----END PRIVATE KEY-----\n", + "client_email": "dummy@dummy.iam.gserviceaccount.com", + }, + } +) +MOCK_TOKEN = {"access_token": "dummtoken", "expires_in": 3600} +MOCK_JSON = {"devices": {}} +MOCK_URL = "https://dummy" +MOCK_HEADER = { + "Authorization": "Bearer {}".format(MOCK_TOKEN["access_token"]), + "X-GFE-SSL": "yes", +} + + +async def test_get_jwt(hass): + """Test signing of key.""" + + jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJkdW1teUBkdW1teS5pYW0uZ3NlcnZpY2VhY2NvdW50LmNvbSIsInNjb3BlIjoiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vYXV0aC9ob21lZ3JhcGgiLCJhdWQiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vby9vYXV0aDIvdG9rZW4iLCJpYXQiOjE1NzEwMTEyMDAsImV4cCI6MTU3MTAxNDgwMH0.gG06SmY-zSvFwSrdFfqIdC6AnC22rwz-d2F2UDeWbywjdmFL_1zceL-OOLBwjD8MJr6nR0kmN_Osu7ml9-EzzZjJqsRUxMjGn2G8nSYHbv16R4FYIp62Ibvt6Jj_wdFobEPoy_5OJ28P5Hdu0giGMlFBJMy0Tc6MgEDZA-cwOBw" + res = _get_homegraph_jwt( + datetime(2019, 10, 14, tzinfo=timezone.utc), + DUMMY_CONFIG["service_account"]["client_email"], + DUMMY_CONFIG["service_account"]["private_key"], + ) + assert res == jwt + + +async def test_get_access_token(hass, aioclient_mock): + """Test the function to get access token.""" + jwt = "dummyjwt" + + aioclient_mock.post( + HOMEGRAPH_TOKEN_URL, + status=200, + json={"access_token": "1234", "expires_in": 3600}, + ) + + await _get_homegraph_token(hass, jwt) + assert aioclient_mock.call_count == 1 + assert aioclient_mock.mock_calls[0][3] == { + "Authorization": "Bearer {}".format(jwt), + "Content-Type": "application/x-www-form-urlencoded", + } + + +async def test_update_access_token(hass): + """Test the function to update access token when expired.""" + jwt = "dummyjwt" + + config = GoogleConfig(hass, DUMMY_CONFIG) + + base_time = datetime(2019, 10, 14, tzinfo=timezone.utc) + with patch( + "homeassistant.components.google_assistant.http._get_homegraph_token" + ) as mock_get_token, patch( + "homeassistant.components.google_assistant.http._get_homegraph_jwt" + ) as mock_get_jwt, patch( + "homeassistant.core.dt_util.utcnow" + ) as mock_utcnow: + mock_utcnow.return_value = base_time + mock_get_jwt.return_value = jwt + mock_get_token.return_value = MOCK_TOKEN + + await config._async_update_token() + mock_get_token.assert_called_once() + + mock_get_token.reset_mock() + + mock_utcnow.return_value = base_time + timedelta(seconds=3600) + await config._async_update_token() + mock_get_token.assert_not_called() + + mock_get_token.reset_mock() + + mock_utcnow.return_value = base_time + timedelta(seconds=3601) + await config._async_update_token() + mock_get_token.assert_called_once() + + +async def test_call_homegraph_api(hass, aioclient_mock, hass_storage): + """Test the function to call the homegraph api.""" + config = GoogleConfig(hass, DUMMY_CONFIG) + with patch( + "homeassistant.components.google_assistant.http._get_homegraph_token" + ) as mock_get_token: + mock_get_token.return_value = MOCK_TOKEN + + aioclient_mock.post(MOCK_URL, status=200, json={}) + + await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + + assert mock_get_token.call_count == 1 + assert aioclient_mock.call_count == 1 + + call = aioclient_mock.mock_calls[0] + assert call[2] == MOCK_JSON + assert call[3] == MOCK_HEADER + + +async def test_call_homegraph_api_retry(hass, aioclient_mock, hass_storage): + """Test the that the calls get retried with new token on 401.""" + config = GoogleConfig(hass, DUMMY_CONFIG) + with patch( + "homeassistant.components.google_assistant.http._get_homegraph_token" + ) as mock_get_token: + mock_get_token.return_value = MOCK_TOKEN + + aioclient_mock.post(MOCK_URL, status=401, json={}) + + await config.async_call_homegraph_api(MOCK_URL, MOCK_JSON) + + assert mock_get_token.call_count == 2 + assert aioclient_mock.call_count == 2 + + call = aioclient_mock.mock_calls[0] + assert call[2] == MOCK_JSON + assert call[3] == MOCK_HEADER + call = aioclient_mock.mock_calls[1] + assert call[2] == MOCK_JSON + assert call[3] == MOCK_HEADER + + +async def test_report_state(hass, aioclient_mock, hass_storage): + """Test the report state function.""" + config = GoogleConfig(hass, DUMMY_CONFIG) + message = {"devices": {}} + owner = User(name="Test User", perm_lookup=None, groups=[], is_owner=True) + + with patch.object(config, "async_call_homegraph_api") as mock_call, patch.object( + hass.auth, "async_get_owner" + ) as mock_get_owner: + mock_get_owner.return_value = owner + + await config.async_report_state(message) + mock_call.assert_called_once_with( + REPORT_STATE_BASE_URL, + {"requestId": ANY, "agentUserId": owner.id, "payload": message}, + ) From 1ec01b5e6cf0891b13f5733c56ef1c9c6842ec02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 19 Oct 2019 13:03:52 +0300 Subject: [PATCH 439/639] Upgrade pylint to 2.4.3 and astroid to 2.3.2 (#27912) https://pylint.readthedocs.io/en/latest/whatsnew/changelog.html#what-s-new-in-pylint-2-4-3 --- requirements_test.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index b6d5bdd7ee9..e491d5ea42a 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -12,8 +12,8 @@ mock-open==1.3.1 mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 -pylint==2.4.2 -astroid==2.3.1 +pylint==2.4.3 +astroid==2.3.2 pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 99a292d82f6..c2013c75a06 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -13,8 +13,8 @@ mock-open==1.3.1 mypy==0.730 pre-commit==1.18.3 pydocstyle==4.0.1 -pylint==2.4.2 -astroid==2.3.1 +pylint==2.4.3 +astroid==2.3.2 pytest-aiohttp==0.3.0 pytest-cov==2.8.1 pytest-sugar==0.9.2 From 1f96a7becf8479403250e29c565767e620d6c5ee Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 19 Oct 2019 12:31:40 +0200 Subject: [PATCH 440/639] Update azure-pipelines-ci.yml --- azure-pipelines-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 82708151ad6..f1abf2ff9db 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -108,7 +108,7 @@ stages: steps: - template: templates/azp-step-cache.yaml@azure parameters: - keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt | "v2"' + keyfile: 'requirements_test_all.txt | homeassistant/package_constraints.txt' build: | set -e python -m venv venv From 1c0814d6f67e8b5ef4bea3f490377898e775c582 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sat, 19 Oct 2019 13:42:49 +0200 Subject: [PATCH 441/639] Run pylint parallel (#27919) --- azure-pipelines-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index f1abf2ff9db..1ca834b6213 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -167,7 +167,7 @@ stages: displayName: 'Install Home Assistant' - script: | . venv/bin/activate - pylint homeassistant + pylint -j 2 homeassistant displayName: 'Run pylint' - job: 'Mypy' pool: From f2617fd74a46ebcede264d8309437f177c1bfde5 Mon Sep 17 00:00:00 2001 From: guillempages Date: Sat, 19 Oct 2019 14:40:42 +0200 Subject: [PATCH 442/639] Split homematic color and effect support (#27299) * [homematic] Split color and effect support There are homematic devices (like HmIP-BSL) that support color but do not support effects. Split the support, so that color can be supported even if effects are not. * Make effect fully independent of color If a device supports effects for e.g. just brightness, it shouldn't be coupled to the color --- homeassistant/components/homematic/light.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 971a8a9cac0..32fa0bb358e 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -54,9 +54,12 @@ class HMLight(HMDevice, Light): @property def supported_features(self): """Flag supported features.""" + features = SUPPORT_BRIGHTNESS if "COLOR" in self._hmdevice.WRITENODE: - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_EFFECT - return SUPPORT_BRIGHTNESS + features |= SUPPORT_COLOR + if "PROGRAM" in self._hmdevice.WRITENODE: + features |= SUPPORT_EFFECT + return features @property def hs_color(self): @@ -110,4 +113,6 @@ class HMLight(HMDevice, Light): self._data[self._state] = None if self.supported_features & SUPPORT_COLOR: - self._data.update({"COLOR": None, "PROGRAM": None}) + self._data.update({"COLOR": None}) + if self.supported_features & SUPPORT_EFFECT: + self._data.update({"PROGRAM": None}) From eb48898687504e2d439b0d4c14cb2a3a0ffcf7c2 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 19 Oct 2019 17:44:40 +0200 Subject: [PATCH 443/639] Add climate profiles to Homematic IP Cloud (#27772) * Add climate service to Homematic IP Cloud to select the active profile * Add profiles ass presets * fix spelling * Re-Add PRESET_NONE for selection * Boost is a manual mode * Fixes based on review * Fixes after review --- .../components/homematicip_cloud/__init__.py | 61 +++++++++++++----- .../components/homematicip_cloud/climate.py | 63 ++++++++++++++++--- .../homematicip_cloud/services.yaml | 21 +++++-- .../homematicip_cloud/test_climate.py | 55 +++++++++++++++- tests/fixtures/homematicip_cloud.json | 6 +- 5 files changed, 170 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 139565bf249..9a3191ac168 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -1,14 +1,16 @@ """Support for HomematicIP Cloud devices.""" import logging +from homematicip.aio.group import AsyncHeatingGroup import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME +from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.config_validation import comp_entity_ids from homeassistant.helpers.typing import ConfigType from .config_flow import configured_haps @@ -25,6 +27,7 @@ from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 _LOGGER = logging.getLogger(__name__) +ATTR_CLIMATE_PROFILE_INDEX = "climate_profile_index" ATTR_DURATION = "duration" ATTR_ENDTIME = "endtime" ATTR_TEMPERATURE = "temperature" @@ -35,6 +38,7 @@ SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD = "activate_eco_mode_with_period" SERVICE_ACTIVATE_VACATION = "activate_vacation" SERVICE_DEACTIVATE_ECO_MODE = "deactivate_eco_mode" SERVICE_DEACTIVATE_VACATION = "deactivate_vacation" +SERVICE_SET_ACTIVE_CLIMATE_PROFILE = "set_active_climate_profile" CONFIG_SCHEMA = vol.Schema( { @@ -86,6 +90,13 @@ SCHEMA_DEACTIVATE_VACATION = vol.Schema( {vol.Optional(ATTR_ACCESSPOINT_ID): vol.All(str, vol.Length(min=24, max=24))} ) +SCHEMA_SET_ACTIVE_CLIMATE_PROFILE = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): comp_entity_ids, + vol.Required(ATTR_CLIMATE_PROFILE_INDEX): cv.positive_int, + } +) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" @@ -117,9 +128,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.activate_absence_with_duration(duration) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_absence_with_duration(duration) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_absence_with_duration(duration) hass.services.async_register( DOMAIN, @@ -138,9 +148,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.activate_absence_with_period(endtime) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_absence_with_period(endtime) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_absence_with_period(endtime) hass.services.async_register( DOMAIN, @@ -160,9 +169,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.activate_vacation(endtime, temperature) else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.activate_vacation(endtime, temperature) + for hap in hass.data[DOMAIN].values(): + await hap.home.activate_vacation(endtime, temperature) hass.services.async_register( DOMAIN, @@ -180,9 +188,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.deactivate_absence() else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.deactivate_absence() + for hap in hass.data[DOMAIN].values(): + await hap.home.deactivate_absence() hass.services.async_register( DOMAIN, @@ -200,9 +207,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if home: await home.deactivate_vacation() else: - for hapid in hass.data[DOMAIN]: - home = hass.data[DOMAIN][hapid].home - await home.deactivate_vacation() + for hap in hass.data[DOMAIN].values(): + await hap.home.deactivate_vacation() hass.services.async_register( DOMAIN, @@ -211,6 +217,29 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: schema=SCHEMA_DEACTIVATE_VACATION, ) + async def _set_active_climate_profile(service): + """Service to set the active climate profile.""" + entity_id_list = service.data[ATTR_ENTITY_ID] + climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1 + + for hap in hass.data[DOMAIN].values(): + if entity_id_list != "all": + for entity_id in entity_id_list: + group = hap.hmip_device_by_entity_id.get(entity_id) + if group: + await group.set_active_profile(climate_profile_index) + else: + for group in hap.home.groups: + if isinstance(group, AsyncHeatingGroup): + await group.set_active_profile(climate_profile_index) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_ACTIVE_CLIMATE_PROFILE, + _set_active_climate_profile, + schema=SCHEMA_SET_ACTIVE_CLIMATE_PROFILE, + ) + def _get_home(hapid: str): """Return a HmIP home.""" hap = hass.data[DOMAIN].get(hapid) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index b8c055dda1f..f1f414169f6 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -4,7 +4,7 @@ from typing import Awaitable from homematicip.aio.device import AsyncHeatingThermostat, AsyncHeatingThermostatCompact from homematicip.aio.group import AsyncHeatingGroup -from homematicip.base.enums import AbsenceType +from homematicip.base.enums import AbsenceType, GroupType from homematicip.functionalHomes import IndoorClimateHome from homeassistant.components.climate import ClimateDevice @@ -25,6 +25,9 @@ from homeassistant.core import HomeAssistant from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .hap import HomematicipHAP +HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2} +COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5} + _LOGGER = logging.getLogger(__name__) HMIP_AUTOMATIC_CM = "AUTOMATIC" @@ -54,7 +57,7 @@ async def async_setup_entry( class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """Representation of a HomematicIP heating group.""" - def __init__(self, hap: HomematicipHAP, device) -> None: + def __init__(self, hap: HomematicipHAP, device: AsyncHeatingGroup) -> None: """Initialize heating group.""" device.modelType = "HmIP-Heating-Group" self._simple_heating = None @@ -107,7 +110,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): Need to be one of HVAC_MODE_*. """ if self._device.boostMode: - return HVAC_MODE_AUTO + return HVAC_MODE_HEAT if self._device.controlMode == HMIP_MANUAL_CM: return HVAC_MODE_HEAT @@ -129,6 +132,8 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """ if self._device.boostMode: return PRESET_BOOST + if self.hvac_mode == HVAC_MODE_HEAT: + return PRESET_NONE if self._device.controlMode == HMIP_ECO_CM: absence_type = self._home.get_functionalHome(IndoorClimateHome).absenceType if absence_type == AbsenceType.VACATION: @@ -140,15 +145,15 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): ]: return PRESET_ECO - return PRESET_NONE + if self._device.activeProfile: + return self._device.activeProfile.name @property def preset_modes(self): - """Return a list of available preset modes. - - Requires SUPPORT_PRESET_MODE. - """ - return [PRESET_NONE, PRESET_BOOST] + """Return a list of available preset modes incl profiles.""" + presets = [PRESET_NONE, PRESET_BOOST] + presets.extend(self._device_profile_names) + return presets @property def min_temp(self) -> float: @@ -180,6 +185,46 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): await self._device.set_boost(False) if preset_mode == PRESET_BOOST: await self._device.set_boost() + if preset_mode in self._device_profile_names: + profile_idx = self._get_profile_idx_by_name(preset_mode) + await self.async_set_hvac_mode(HVAC_MODE_AUTO) + await self._device.set_active_profile(profile_idx) + + @property + def _device_profiles(self): + """Return the relevant profiles of the device.""" + return [ + profile + for profile in self._device.profiles + if profile.visible + and profile.name != "" + and profile.index in self._relevant_profile_group + ] + + @property + def _device_profile_names(self): + """Return a collection of profile names.""" + return [profile.name for profile in self._device_profiles] + + def _get_profile_idx_by_name(self, profile_name): + """Return a profile index by name.""" + relevant_index = self._relevant_profile_group + index_name = [ + profile.index + for profile in self._device_profiles + if profile.name == profile_name + ] + + return relevant_index[index_name[0]] + + @property + def _relevant_profile_group(self): + """Return the relevant profile groups.""" + return ( + HEATING_PROFILES + if self._device.groupType == GroupType.HEATING + else COOLING_PROFILES + ) def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml index cf93b3065ee..f426c9b5d22 100644 --- a/homeassistant/components/homematicip_cloud/services.yaml +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -7,7 +7,7 @@ activate_eco_mode_with_duration: description: The duration of eco mode in minutes. example: 60 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx activate_eco_mode_with_period: @@ -17,7 +17,7 @@ activate_eco_mode_with_period: description: The time when the eco mode should automatically be disabled. example: 2019-02-17 14:00 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx activate_vacation: @@ -30,20 +30,31 @@ activate_vacation: description: the set temperature during the vacation mode. example: 18.5 accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx deactivate_eco_mode: description: Deactivates the eco mode immediately. fields: accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx deactivate_vacation: description: Deactivates the vacation mode immediately. fields: accesspoint_id: - description: The ID of the Homematic IP Access Point + description: The ID of the Homematic IP Access Point (optional) example: 3014xxxxxxxxxxxxxxxxxxxx +set_active_climate_profile: + description: Set the active climate profile index. + fields: + entity_id: + description: The ID of the climte entity. Use 'all' keyword to switch the profile for all entities. + example: climate.livingroom + climate_profile_index: + description: The index of the climate profile (1 based) + example: 1 + + diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index bdfd26319e6..80e4e74e451 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -49,8 +49,13 @@ async def test_hmip_heating_group(hass, default_mock_hap): assert ha_state.attributes["max_temp"] == 30.0 assert ha_state.attributes["temperature"] == 5.0 assert ha_state.attributes["current_humidity"] == 47 - assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - assert ha_state.attributes[ATTR_PRESET_MODES] == [PRESET_NONE, PRESET_BOOST] + assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" + assert ha_state.attributes[ATTR_PRESET_MODES] == [ + PRESET_NONE, + PRESET_BOOST, + "STD", + "Winter", + ] service_call_counter = len(hmip_device.mock_calls) @@ -117,7 +122,7 @@ async def test_hmip_heating_group(hass, default_mock_hap): assert hmip_device.mock_calls[-1][1] == (False,) await async_manipulate_test_data(hass, hmip_device, "boostMode", False) ha_state = hass.states.get(entity_id) - assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" # Not required for hmip, but a posiblity to send no temperature. await hass.services.async_call( @@ -153,6 +158,18 @@ async def test_hmip_heating_group(hass, default_mock_hap): ha_state = hass.states.get(entity_id) assert ha_state.attributes[ATTR_PRESET_MODE] == PRESET_ECO + # Not required for hmip, but a posiblity to send no temperature. + await hass.services.async_call( + "climate", + "set_preset_mode", + {"entity_id": entity_id, "preset_mode": "Winter"}, + blocking=True, + ) + + assert len(hmip_device.mock_calls) == service_call_counter + 16 + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + async def test_hmip_climate_services(hass, mock_hap_with_service): """Test HomematicipHeatingGroup.""" @@ -264,3 +281,35 @@ async def test_hmip_climate_services(hass, mock_hap_with_service): assert home.mock_calls[-1][1] == () # There is no further call on connection. assert len(home._connection.mock_calls) == 10 # pylint: disable=W0212 + + +async def test_hmip_heating_group_services(hass, mock_hap_with_service): + """Test HomematicipHeatingGroup services.""" + entity_id = "climate.badezimmer" + entity_name = "Badezimmer" + device_model = None + + ha_state, hmip_device = get_and_check_entity_basics( + hass, mock_hap_with_service, entity_id, entity_name, device_model + ) + assert ha_state + + await hass.services.async_call( + "homematicip_cloud", + "set_active_climate_profile", + {"climate_profile_index": 2, "entity_id": "climate.badezimmer"}, + blocking=True, + ) + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + assert len(hmip_device._connection.mock_calls) == 2 # pylint: disable=W0212 + + await hass.services.async_call( + "homematicip_cloud", + "set_active_climate_profile", + {"climate_profile_index": 2, "entity_id": "all"}, + blocking=True, + ) + assert hmip_device.mock_calls[-1][0] == "set_active_profile" + assert hmip_device.mock_calls[-1][1] == (1,) + assert len(hmip_device._connection.mock_calls) == 12 # pylint: disable=W0212 diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index 1d3d5bfd8f4..e17df9c2039 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -4638,7 +4638,7 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000021", "index": "PROFILE_1", - "name": "", + "name": "STD", "profileId": "00000000-0000-0000-0000-000000000038", "visible": true }, @@ -4646,9 +4646,9 @@ "enabled": true, "groupId": "00000000-0000-0000-0000-000000000021", "index": "PROFILE_2", - "name": "", + "name": "Winter", "profileId": "00000000-0000-0000-0000-000000000039", - "visible": false + "visible": true }, "PROFILE_3": { "enabled": true, From 8c0deeb176911e0b9c8088c67836d83965852152 Mon Sep 17 00:00:00 2001 From: Quentame Date: Sat, 19 Oct 2019 18:22:32 +0200 Subject: [PATCH 444/639] Move imports in luftdaten component (#27929) --- homeassistant/components/luftdaten/__init__.py | 6 ++---- homeassistant/components/luftdaten/config_flow.py | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index 86129eafc02..ac524502f8d 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -1,6 +1,8 @@ """Support for Luftdaten stations.""" import logging +from luftdaten import Luftdaten +from luftdaten.exceptions import LuftdatenError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -114,8 +116,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up Luftdaten as config entry.""" - from luftdaten import Luftdaten - from luftdaten.exceptions import LuftdatenError if not isinstance(config_entry.data[CONF_SENSOR_ID], int): _async_fixup_sensor_id(hass, config_entry, config_entry.data[CONF_SENSOR_ID]) @@ -191,8 +191,6 @@ class LuftDatenData: async def async_update(self): """Update sensor/binary sensor data.""" - from luftdaten.exceptions import LuftdatenError - try: await self.client.get_data() diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py index 7a8ef0df8ba..1f382b86c0f 100644 --- a/homeassistant/components/luftdaten/config_flow.py +++ b/homeassistant/components/luftdaten/config_flow.py @@ -1,6 +1,8 @@ """Config flow to configure the Luftdaten component.""" from collections import OrderedDict +from luftdaten import Luftdaten +from luftdaten.exceptions import LuftdatenConnectionError import voluptuous as vol from homeassistant import config_entries @@ -60,7 +62,6 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle the start of the config flow.""" - from luftdaten import Luftdaten, exceptions if not user_input: return self._show_form() @@ -75,7 +76,7 @@ class LuftDatenFlowHandler(config_entries.ConfigFlow): try: await luftdaten.get_data() valid = await luftdaten.validate_sensor() - except exceptions.LuftdatenConnectionError: + except LuftdatenConnectionError: return self._show_form({CONF_SENSOR_ID: "communication_error"}) if not valid: From de1477f00b5cc9af165760ece900902a42129cbd Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 19 Oct 2019 18:24:28 +0200 Subject: [PATCH 445/639] Bump version of homematicip to 0.10.13 (#27928) The Home websocket can now automatically reopen a lost connection (default) --- homeassistant/components/homematicip_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 40c8c7c3598..4feef19c8da 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "requirements": [ - "homematicip==0.10.12" + "homematicip==0.10.13" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 80be965c415..8e935142ff0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -655,7 +655,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.12 +homematicip==0.10.13 # homeassistant.components.horizon horimote==0.4.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2013c75a06..93d639dadd9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -248,7 +248,7 @@ homeassistant-pyozw==0.1.4 homekit[IP]==0.15.0 # homeassistant.components.homematicip_cloud -homematicip==0.10.12 +homematicip==0.10.13 # homeassistant.components.google # homeassistant.components.remember_the_milk From 840001e168ede7a123eae7a3c93bd954145c6ccf Mon Sep 17 00:00:00 2001 From: Greg Rapp Date: Sat, 19 Oct 2019 14:26:07 -0400 Subject: [PATCH 446/639] Added night arm mode support to Envisalink component (#27087) --- .../components/envisalink/alarm_control_panel.py | 9 +++++++++ homeassistant/components/envisalink/manifest.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/envisalink/alarm_control_panel.py b/homeassistant/components/envisalink/alarm_control_panel.py index 81e656708c5..663f19c8ed5 100644 --- a/homeassistant/components/envisalink/alarm_control_panel.py +++ b/homeassistant/components/envisalink/alarm_control_panel.py @@ -8,6 +8,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, @@ -126,6 +127,8 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): if self._info["status"]["alarm"]: state = STATE_ALARM_TRIGGERED + elif self._info["status"]["armed_zero_entry_delay"]: + state = STATE_ALARM_ARMED_NIGHT elif self._info["status"]["armed_away"]: state = STATE_ALARM_ARMED_AWAY elif self._info["status"]["armed_stay"]: @@ -173,6 +176,12 @@ class EnvisalinkAlarm(EnvisalinkDevice, alarm.AlarmControlPanel): """Alarm trigger command. Will be used to trigger a panic alarm.""" self.hass.data[DATA_EVL].panic_alarm(self._panic_type) + async def async_alarm_arm_night(self, code=None): + """Send arm night command.""" + self.hass.data[DATA_EVL].arm_night_partition( + str(code) if code else str(self._code), self._partition_number + ) + @callback def async_alarm_keypress(self, keypress=None): """Send custom keypress.""" diff --git a/homeassistant/components/envisalink/manifest.json b/homeassistant/components/envisalink/manifest.json index 3cee270f099..52303c18413 100644 --- a/homeassistant/components/envisalink/manifest.json +++ b/homeassistant/components/envisalink/manifest.json @@ -7,4 +7,4 @@ ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file From 9ec06029862c41370bd5f770745bbb2d8431e44b Mon Sep 17 00:00:00 2001 From: bouni Date: Sat, 19 Oct 2019 20:33:05 +0200 Subject: [PATCH 447/639] Move imports in cpuspeed component (#27890) --- homeassistant/components/cpuspeed/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/cpuspeed/sensor.py b/homeassistant/components/cpuspeed/sensor.py index 9484e770998..53598e24c70 100644 --- a/homeassistant/components/cpuspeed/sensor.py +++ b/homeassistant/components/cpuspeed/sensor.py @@ -1,11 +1,12 @@ """Support for displaying the current CPU speed.""" import logging +from cpuinfo import cpuinfo import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -75,7 +76,6 @@ class CpuSpeedSensor(Entity): def update(self): """Get the latest data and updates the state.""" - from cpuinfo import cpuinfo self.info = cpuinfo.get_cpu_info() if HZ_ACTUAL_RAW in self.info: From 758fcc9b001e971d08f0092285f9ab285f4bcd3d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 Oct 2019 11:33:21 -0700 Subject: [PATCH 448/639] Remove helper imports relying on installed requirements (#27898) --- homeassistant/helpers/state.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/homeassistant/helpers/state.py b/homeassistant/helpers/state.py index 2f49a566a32..4cb7fb85bff 100644 --- a/homeassistant/helpers/state.py +++ b/homeassistant/helpers/state.py @@ -11,11 +11,9 @@ from homeassistant.loader import bind_hass, async_get_integration, IntegrationNo import homeassistant.util.dt as dt_util from homeassistant.components.notify import ATTR_MESSAGE, SERVICE_NOTIFY from homeassistant.components.sun import STATE_ABOVE_HORIZON, STATE_BELOW_HORIZON -from homeassistant.components.mysensors.switch import ATTR_IR_CODE, SERVICE_SEND_IR_CODE from homeassistant.components.cover import ATTR_POSITION, ATTR_TILT_POSITION from homeassistant.const import ( ATTR_ENTITY_ID, - ATTR_OPTION, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, SERVICE_ALARM_DISARM, @@ -41,7 +39,6 @@ from homeassistant.const import ( STATE_OPEN, STATE_UNKNOWN, STATE_UNLOCKED, - SERVICE_SELECT_OPTION, ) from homeassistant.core import Context, State, DOMAIN as HASS_DOMAIN from .typing import HomeAssistantType @@ -54,8 +51,6 @@ GROUP_DOMAIN = "group" # Each item is a service with a list of required attributes. SERVICE_ATTRIBUTES = { SERVICE_NOTIFY: [ATTR_MESSAGE], - SERVICE_SEND_IR_CODE: [ATTR_IR_CODE], - SERVICE_SELECT_OPTION: [ATTR_OPTION], SERVICE_SET_COVER_POSITION: [ATTR_POSITION], SERVICE_SET_COVER_TILT_POSITION: [ATTR_TILT_POSITION], } From 381d423fecd95f2b901f3ca1fa277746e7756c76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 19 Oct 2019 21:35:57 +0300 Subject: [PATCH 449/639] Upgrade mypy to 0.740 (#27913) * Upgrade mypy to 0.740 http://mypy-lang.blogspot.com/2019/10/mypy-0740-released.html * Type hint additions * Type fixes * Remove no longer needed type ignores and casts * Disable untyped definition checks in bunch of files --- homeassistant/auth/__init__.py | 4 ++-- homeassistant/components/cover/__init__.py | 2 +- homeassistant/components/group/__init__.py | 2 +- homeassistant/components/group/cover.py | 5 +++-- homeassistant/components/group/light.py | 1 + homeassistant/components/group/notify.py | 2 +- homeassistant/components/sun/__init__.py | 2 +- homeassistant/components/switch/light.py | 2 +- homeassistant/components/websocket_api/http.py | 7 ++++--- homeassistant/components/websocket_api/sensor.py | 2 +- homeassistant/components/zone/config_flow.py | 2 +- homeassistant/config_entries.py | 4 ++-- homeassistant/helpers/config_entry_flow.py | 2 +- homeassistant/helpers/storage.py | 1 + homeassistant/util/location.py | 4 ++-- requirements_test.txt | 2 +- requirements_test_all.txt | 2 +- 17 files changed, 25 insertions(+), 21 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 64391debc10..921bec71e78 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -45,7 +45,7 @@ async def auth_manager_from_config( ) ) else: - providers = () + providers = [] # So returned auth providers are in same order as config provider_hash: _ProviderDict = OrderedDict() for provider in providers: @@ -57,7 +57,7 @@ async def auth_manager_from_config( *(auth_mfa_module_from_config(hass, config) for config in module_configs) ) else: - modules = () + modules = [] # So returned auth modules are in same order as config module_hash: _MfaModuleDict = OrderedDict() for module in modules: diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 8d2b4430fe1..cfac143a5d8 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -34,7 +34,7 @@ from homeassistant.const import ( ) -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 39574a2b03b..29126c82d44 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -36,7 +36,7 @@ from homeassistant.helpers.config_validation import ENTITY_SERVICE_SCHEMA from homeassistant.helpers.typing import HomeAssistantType -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs DOMAIN = "group" diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index c5200082f2f..f7a9643e5c8 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -44,6 +44,7 @@ from homeassistant.components.cover import ( # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -74,7 +75,7 @@ class CoverGroup(CoverDevice): """Initialize a CoverGroup entity.""" self._name = name self._is_closed = False - self._cover_position = 100 + self._cover_position: Optional[int] = 100 self._tilt_position = None self._supported_features = 0 self._assumed_state = True @@ -178,7 +179,7 @@ class CoverGroup(CoverDevice): return self._is_closed @property - def current_cover_position(self): + def current_cover_position(self) -> Optional[int]: """Return current position for all covers.""" return self._cover_position diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index e77c858fc02..85804552494 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -45,6 +45,7 @@ from homeassistant.components.light import ( # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs +# mypy: no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/group/notify.py b/homeassistant/components/group/notify.py index 2ffb7fea049..e17990690fa 100644 --- a/homeassistant/components/group/notify.py +++ b/homeassistant/components/group/notify.py @@ -18,7 +18,7 @@ from homeassistant.components.notify import ( ) -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/sun/__init__.py b/homeassistant/components/sun/__init__.py index 7d883e273e5..e848449e61e 100644 --- a/homeassistant/components/sun/__init__.py +++ b/homeassistant/components/sun/__init__.py @@ -18,7 +18,7 @@ from homeassistant.helpers.sun import ( from homeassistant.util import dt as dt_util -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 8f3b5d87f8c..b0abf957991 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.components.light import PLATFORM_SCHEMA, Light -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index 08a0430ee2a..be1830aa07b 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -2,6 +2,7 @@ import asyncio from contextlib import suppress import logging +from typing import Optional from aiohttp import web, WSMsgType import async_timeout @@ -25,7 +26,7 @@ from .error import Disconnect from .messages import error_message -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs class WebsocketAPIView(HomeAssistantView): @@ -47,7 +48,7 @@ class WebSocketHandler: """Initialize an active connection.""" self.hass = hass self.request = request - self.wsock = None + self.wsock: Optional[web.WebSocketResponse] = None self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) self._handle_task = None self._writer_task = None @@ -115,7 +116,7 @@ class WebSocketHandler: # Py3.7+ if hasattr(asyncio, "current_task"): # pylint: disable=no-member - self._handle_task = asyncio.current_task() # type: ignore + self._handle_task = asyncio.current_task() else: self._handle_task = asyncio.Task.current_task() diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index 20a6a90860b..f8f1257aefc 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -10,7 +10,7 @@ from .const import ( ) -# mypy: allow-untyped-calls, allow-untyped-defs +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): diff --git a/homeassistant/components/zone/config_flow.py b/homeassistant/components/zone/config_flow.py index d23fb5a4757..39633754772 100644 --- a/homeassistant/components/zone/config_flow.py +++ b/homeassistant/components/zone/config_flow.py @@ -20,7 +20,7 @@ from homeassistant.util import slugify from .const import CONF_PASSIVE, DOMAIN, HOME_ZONE -# mypy: allow-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs @callback diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f8c7c7a9da1..aee15d6c0ce 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -15,7 +15,7 @@ from homeassistant.setup import async_setup_component, async_process_deps_reqs from homeassistant.util.decorator import Registry from homeassistant.helpers import entity_registry -# mypy: allow-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) _UNDEF = object() @@ -676,7 +676,7 @@ async def _old_conf_migrator(old_config): class ConfigFlow(data_entry_flow.FlowHandler): """Base class for config flows with some helpers.""" - def __init_subclass__(cls, domain=None, **kwargs): + def __init_subclass__(cls, domain: Optional[str] = None, **kwargs: Any) -> None: """Initialize a subclass, register if possible.""" super().__init_subclass__(**kwargs) # type: ignore if domain is not None: diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 374ef795846..7a1512957a2 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -3,7 +3,7 @@ from typing import Callable, Awaitable, Union from homeassistant import config_entries from .typing import HomeAssistantType -# mypy: allow-untyped-defs +# mypy: allow-untyped-defs, no-check-untyped-defs DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index cd99a47cf57..72458d24c82 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -13,6 +13,7 @@ from homeassistant.helpers.event import async_call_later # mypy: allow-untyped-calls, allow-untyped-defs, no-warn-return-any +# mypy: no-check-untyped-defs STORAGE_DIR = ".storage" _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index f81c40a52bb..7c61a8ab1e9 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -6,7 +6,7 @@ detect_location_info and elevation are mocked by default during tests. import asyncio import collections import math -from typing import Any, Optional, Tuple, Dict, cast +from typing import Any, Optional, Tuple, Dict import aiohttp @@ -159,7 +159,7 @@ def vincenty( if miles: s *= MILES_PER_KILOMETER # kilometers to miles - return round(cast(float, s), 6) + return round(s, 6) async def _get_ipapi(session: aiohttp.ClientSession) -> Optional[Dict[str, Any]]: diff --git a/requirements_test.txt b/requirements_test.txt index e491d5ea42a..7af2ec0dde3 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,7 +9,7 @@ codecov==2.0.15 flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 -mypy==0.730 +mypy==0.740 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93d639dadd9..dc6267be6a9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -10,7 +10,7 @@ codecov==2.0.15 flake8-docstrings==1.5.0 flake8==3.7.8 mock-open==1.3.1 -mypy==0.730 +mypy==0.740 pre-commit==1.18.3 pydocstyle==4.0.1 pylint==2.4.3 From 5fa4632c125af71f19ae04a898789d02153c0936 Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 19 Oct 2019 20:39:31 +0200 Subject: [PATCH 450/639] Add improved scene support to the cover integration (#27914) --- .../components/cover/reproduce_state.py | 117 +++++++++++ .../components/cover/test_reproduce_state.py | 198 ++++++++++++++++++ 2 files changed, 315 insertions(+) create mode 100644 homeassistant/components/cover/reproduce_state.py create mode 100644 tests/components/cover/test_reproduce_state.py diff --git a/homeassistant/components/cover/reproduce_state.py b/homeassistant/components/cover/reproduce_state.py new file mode 100644 index 00000000000..64ea410ce93 --- /dev/null +++ b/homeassistant/components/cover/reproduce_state.py @@ -0,0 +1,117 @@ +"""Reproduce an Cover state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if ( + cur_state.state == state.state + and cur_state.attributes.get(ATTR_CURRENT_POSITION) + == state.attributes.get(ATTR_CURRENT_POSITION) + and cur_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + == state.attributes.get(ATTR_CURRENT_TILT_POSITION) + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + service_data_tilting = {ATTR_ENTITY_ID: state.entity_id} + + if cur_state.state != state.state or cur_state.attributes.get( + ATTR_CURRENT_POSITION + ) != state.attributes.get(ATTR_CURRENT_POSITION): + # Open/Close + if state.state == STATE_CLOSED or state.state == STATE_CLOSING: + service = SERVICE_CLOSE_COVER + elif state.state == STATE_OPEN or state.state == STATE_OPENING: + if ( + ATTR_CURRENT_POSITION in cur_state.attributes + and ATTR_CURRENT_POSITION in state.attributes + ): + service = SERVICE_SET_COVER_POSITION + service_data[ATTR_POSITION] = state.attributes[ATTR_CURRENT_POSITION] + else: + service = SERVICE_OPEN_COVER + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + if ( + ATTR_CURRENT_TILT_POSITION in state.attributes + and ATTR_CURRENT_TILT_POSITION in cur_state.attributes + and cur_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + != state.attributes.get(ATTR_CURRENT_TILT_POSITION) + ): + # Tilt position + if state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 100: + service_tilting = SERVICE_OPEN_COVER_TILT + elif state.attributes.get(ATTR_CURRENT_TILT_POSITION) == 0: + service_tilting = SERVICE_CLOSE_COVER_TILT + else: + service_tilting = SERVICE_SET_COVER_TILT_POSITION + service_data_tilting[ATTR_TILT_POSITION] = state.attributes[ + ATTR_CURRENT_TILT_POSITION + ] + + await hass.services.async_call( + DOMAIN, + service_tilting, + service_data_tilting, + context=context, + blocking=True, + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Cover states.""" + # Reproduce states in parallel. + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/cover/test_reproduce_state.py b/tests/components/cover/test_reproduce_state.py new file mode 100644 index 00000000000..39fdf3d3992 --- /dev/null +++ b/tests/components/cover/test_reproduce_state.py @@ -0,0 +1,198 @@ +"""Test reproduce state for Cover.""" +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, + ATTR_POSITION, + ATTR_TILT_POSITION, +) +from homeassistant.const import ( + SERVICE_CLOSE_COVER, + SERVICE_CLOSE_COVER_TILT, + SERVICE_OPEN_COVER, + SERVICE_OPEN_COVER_TILT, + SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, + STATE_CLOSED, + STATE_OPEN, +) +from homeassistant.core import State +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Cover states.""" + hass.states.async_set("cover.entity_close", STATE_CLOSED, {}) + hass.states.async_set( + "cover.entity_close_attr", + STATE_CLOSED, + {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0}, + ) + hass.states.async_set( + "cover.entity_close_tilt", STATE_CLOSED, {ATTR_CURRENT_TILT_POSITION: 50} + ) + hass.states.async_set("cover.entity_open", STATE_OPEN, {}) + hass.states.async_set( + "cover.entity_slightly_open", STATE_OPEN, {ATTR_CURRENT_POSITION: 50} + ) + hass.states.async_set( + "cover.entity_open_attr", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 0}, + ) + hass.states.async_set( + "cover.entity_open_tilt", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50}, + ) + hass.states.async_set( + "cover.entity_entirely_open", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 100}, + ) + + close_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER) + open_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) + close_tilt_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER_TILT) + open_tilt_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER_TILT) + position_calls = async_mock_service(hass, "cover", SERVICE_SET_COVER_POSITION) + position_tilt_calls = async_mock_service( + hass, "cover", SERVICE_SET_COVER_TILT_POSITION + ) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("cover.entity_close", STATE_CLOSED), + State( + "cover.entity_close_attr", + STATE_CLOSED, + {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0}, + ), + State( + "cover.entity_close_tilt", + STATE_CLOSED, + {ATTR_CURRENT_TILT_POSITION: 50}, + ), + State("cover.entity_open", STATE_OPEN), + State( + "cover.entity_slightly_open", STATE_OPEN, {ATTR_CURRENT_POSITION: 50} + ), + State( + "cover.entity_open_attr", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 0}, + ), + State( + "cover.entity_open_tilt", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50}, + ), + State( + "cover.entity_entirely_open", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 100, ATTR_CURRENT_TILT_POSITION: 100}, + ), + ], + blocking=True, + ) + + assert len(close_calls) == 0 + assert len(open_calls) == 0 + assert len(close_tilt_calls) == 0 + assert len(open_tilt_calls) == 0 + assert len(position_calls) == 0 + assert len(position_tilt_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("cover.entity_close", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(close_calls) == 0 + assert len(open_calls) == 0 + assert len(close_tilt_calls) == 0 + assert len(open_tilt_calls) == 0 + assert len(position_calls) == 0 + assert len(position_tilt_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("cover.entity_close", STATE_OPEN), + State( + "cover.entity_close_attr", + STATE_OPEN, + {ATTR_CURRENT_POSITION: 50, ATTR_CURRENT_TILT_POSITION: 50}, + ), + State( + "cover.entity_close_tilt", + STATE_CLOSED, + {ATTR_CURRENT_TILT_POSITION: 100}, + ), + State("cover.entity_open", STATE_CLOSED), + State("cover.entity_slightly_open", STATE_OPEN, {}), + State("cover.entity_open_attr", STATE_CLOSED, {}), + State( + "cover.entity_open_tilt", STATE_OPEN, {ATTR_CURRENT_TILT_POSITION: 0} + ), + State( + "cover.entity_entirely_open", + STATE_CLOSED, + {ATTR_CURRENT_POSITION: 0, ATTR_CURRENT_TILT_POSITION: 0}, + ), + # Should not raise + State("cover.non_existing", "on"), + ], + blocking=True, + ) + + valid_close_calls = [ + {"entity_id": "cover.entity_open"}, + {"entity_id": "cover.entity_open_attr"}, + {"entity_id": "cover.entity_entirely_open"}, + ] + assert len(close_calls) == 3 + for call in close_calls: + assert call.domain == "cover" + assert call.data in valid_close_calls + valid_close_calls.remove(call.data) + + valid_open_calls = [ + {"entity_id": "cover.entity_close"}, + {"entity_id": "cover.entity_slightly_open"}, + {"entity_id": "cover.entity_open_tilt"}, + ] + assert len(open_calls) == 3 + for call in open_calls: + assert call.domain == "cover" + assert call.data in valid_open_calls + valid_open_calls.remove(call.data) + + valid_close_tilt_calls = [ + {"entity_id": "cover.entity_open_tilt"}, + {"entity_id": "cover.entity_entirely_open"}, + ] + assert len(close_tilt_calls) == 2 + for call in close_tilt_calls: + assert call.domain == "cover" + assert call.data in valid_close_tilt_calls + valid_close_tilt_calls.remove(call.data) + + assert len(open_tilt_calls) == 1 + assert open_tilt_calls[0].domain == "cover" + assert open_tilt_calls[0].data == {"entity_id": "cover.entity_close_tilt"} + + assert len(position_calls) == 1 + assert position_calls[0].domain == "cover" + assert position_calls[0].data == { + "entity_id": "cover.entity_close_attr", + ATTR_POSITION: 50, + } + + assert len(position_tilt_calls) == 1 + assert position_tilt_calls[0].domain == "cover" + assert position_tilt_calls[0].data == { + "entity_id": "cover.entity_close_attr", + ATTR_TILT_POSITION: 50, + } From 48e5655379ad434790d81c989f18c5ba0cfe47e1 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Sat, 19 Oct 2019 14:10:35 -0500 Subject: [PATCH 451/639] Bump abodepy version (#27931) --- homeassistant/components/abode/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/abode/manifest.json b/homeassistant/components/abode/manifest.json index 8316691f701..b54120c7cbd 100644 --- a/homeassistant/components/abode/manifest.json +++ b/homeassistant/components/abode/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/abode", "requirements": [ - "abodepy==0.16.5" + "abodepy==0.16.6" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8e935142ff0..d829377d8a2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -103,7 +103,7 @@ WazeRouteCalculator==0.10 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.16.5 +abodepy==0.16.6 # homeassistant.components.mcp23017 adafruit-blinka==1.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc6267be6a9..cb0b56cb5ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -46,7 +46,7 @@ RtmAPI==0.7.2 YesssSMS==0.4.1 # homeassistant.components.abode -abodepy==0.16.5 +abodepy==0.16.6 # homeassistant.components.androidtv adb-shell==0.0.7 From cb061e57d25c331daea6a2ae98eb80701dd2090c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 19 Oct 2019 21:11:09 +0200 Subject: [PATCH 452/639] Add support for AdGuard Home v0.99.0 (#27926) * Bump adguardhome to 0.3.0 * Add a more user friendly version handling and added logs * :pencil2: Fixes spelling error in abort messages * :pencil2: Error messages improvements, suggested by cgtobi --- .../components/adguard/.translations/en.json | 2 + homeassistant/components/adguard/__init__.py | 16 ++++- .../components/adguard/config_flow.py | 25 ++++++- homeassistant/components/adguard/const.py | 2 + .../components/adguard/manifest.json | 2 +- homeassistant/components/adguard/strings.json | 8 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/adguard/test_config_flow.py | 72 +++++++++++++++++-- 9 files changed, 116 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/adguard/.translations/en.json b/homeassistant/components/adguard/.translations/en.json index 6e3b5b58503..8bfb8516fd8 100644 --- a/homeassistant/components/adguard/.translations/en.json +++ b/homeassistant/components/adguard/.translations/en.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", + "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.", "existing_instance_updated": "Updated existing configuration.", "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." }, diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index ba716ae0f9c..bb53d00aab8 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -1,8 +1,9 @@ """Support for AdGuard Home.""" +from distutils.version import LooseVersion import logging from typing import Any, Dict -from adguardhome import AdGuardHome, AdGuardHomeError +from adguardhome import AdGuardHome, AdGuardHomeConnectionError, AdGuardHomeError import voluptuous as vol from homeassistant.components.adguard.const import ( @@ -10,6 +11,7 @@ from homeassistant.components.adguard.const import ( DATA_ADGUARD_CLIENT, DATA_ADGUARD_VERION, DOMAIN, + MIN_ADGUARD_HOME_VERSION, SERVICE_ADD_URL, SERVICE_DISABLE_URL, SERVICE_ENABLE_URL, @@ -27,6 +29,7 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity @@ -64,6 +67,17 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.data.setdefault(DOMAIN, {})[DATA_ADGUARD_CLIENT] = adguard + try: + version = await adguard.version() + except AdGuardHomeConnectionError as exception: + raise ConfigEntryNotReady from exception + + if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): + _LOGGER.error( + "This integration requires AdGuard Home v0.99.0 or higher to work correctly" + ) + raise ConfigEntryNotReady + for component in "sensor", "switch": hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) diff --git a/homeassistant/components/adguard/config_flow.py b/homeassistant/components/adguard/config_flow.py index 5a096aeceed..9f5645edb8d 100644 --- a/homeassistant/components/adguard/config_flow.py +++ b/homeassistant/components/adguard/config_flow.py @@ -1,11 +1,12 @@ """Config flow to configure the AdGuard Home integration.""" +from distutils.version import LooseVersion import logging from adguardhome import AdGuardHome, AdGuardHomeConnectionError import voluptuous as vol from homeassistant import config_entries -from homeassistant.components.adguard.const import DOMAIN +from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_HOST, @@ -83,11 +84,20 @@ class AdGuardHomeFlowHandler(ConfigFlow): ) try: - await adguard.version() + version = await adguard.version() except AdGuardHomeConnectionError: errors["base"] = "connection_error" return await self._show_setup_form(errors) + if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): + return self.async_abort( + reason="adguard_home_outdated", + description_placeholders={ + "current_version": version, + "minimal_version": MIN_ADGUARD_HOME_VERSION, + }, + ) + return self.async_create_entry( title=user_input[CONF_HOST], data={ @@ -156,11 +166,20 @@ class AdGuardHomeFlowHandler(ConfigFlow): ) try: - await adguard.version() + version = await adguard.version() except AdGuardHomeConnectionError: errors["base"] = "connection_error" return await self._show_hassio_form(errors) + if LooseVersion(MIN_ADGUARD_HOME_VERSION) > LooseVersion(version): + return self.async_abort( + reason="adguard_home_addon_outdated", + description_placeholders={ + "current_version": version, + "minimal_version": MIN_ADGUARD_HOME_VERSION, + }, + ) + return self.async_create_entry( title=self._hassio_discovery["addon"], data={ diff --git a/homeassistant/components/adguard/const.py b/homeassistant/components/adguard/const.py index c77d76a70cf..eb12a9c163f 100644 --- a/homeassistant/components/adguard/const.py +++ b/homeassistant/components/adguard/const.py @@ -7,6 +7,8 @@ DATA_ADGUARD_VERION = "adguard_version" CONF_FORCE = "force" +MIN_ADGUARD_HOME_VERSION = "v0.99.0" + SERVICE_ADD_URL = "add_url" SERVICE_DISABLE_URL = "disable_url" SERVICE_ENABLE_URL = "enable_url" diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index f207e6dff09..45fd21f4fc8 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adguard", "requirements": [ - "adguardhome==0.2.1" + "adguardhome==0.3.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index b3966bca820..d33ba2b397a 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -23,8 +23,10 @@ "connection_error": "Failed to connect." }, "abort": { - "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed.", - "existing_instance_updated": "Updated existing configuration." + "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", + "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.", + "existing_instance_updated": "Updated existing configuration.", + "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." } } -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index d829377d8a2..d30d01f02c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -115,7 +115,7 @@ adafruit-circuitpython-mcp230xx==1.1.2 adb-shell==0.0.7 # homeassistant.components.adguard -adguardhome==0.2.1 +adguardhome==0.3.0 # homeassistant.components.frontier_silicon afsapi==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cb0b56cb5ae..2a4fdf4eac7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -52,7 +52,7 @@ abodepy==0.16.6 adb-shell==0.0.7 # homeassistant.components.adguard -adguardhome==0.2.1 +adguardhome==0.3.0 # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.10 diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index ea5e5ad2276..dbda1e99a48 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -3,9 +3,9 @@ from unittest.mock import patch import aiohttp -from homeassistant import data_entry_flow, config_entries +from homeassistant import config_entries, data_entry_flow from homeassistant.components.adguard import config_flow -from homeassistant.components.adguard.const import DOMAIN +from homeassistant.components.adguard.const import DOMAIN, MIN_ADGUARD_HOME_VERSION from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -65,7 +65,7 @@ async def test_full_flow_implementation(hass, aioclient_mock): FIXTURE_USER_INPUT[CONF_HOST], FIXTURE_USER_INPUT[CONF_PORT], ), - json={"version": "1.0"}, + json={"version": "v0.99.0"}, headers={"Content-Type": "application/json"}, ) @@ -133,8 +133,19 @@ async def test_hassio_update_instance_not_running(hass): assert result["reason"] == "existing_instance_updated" -async def test_hassio_update_instance_running(hass): +async def test_hassio_update_instance_running(hass, aioclient_mock): """Test we only allow a single config flow.""" + aioclient_mock.get( + "http://mock-adguard-updated:3000/control/status", + json={"version": "v0.99.0"}, + headers={"Content-Type": "application/json"}, + ) + aioclient_mock.get( + "http://mock-adguard:3000/control/status", + json={"version": "v0.99.0"}, + headers={"Content-Type": "application/json"}, + ) + entry = MockConfigEntry( domain="adguard", data={ @@ -187,7 +198,7 @@ async def test_hassio_confirm(hass, aioclient_mock): """Test we can finish a config flow.""" aioclient_mock.get( "http://mock-adguard:3000/control/status", - json={"version": "1.0"}, + json={"version": "v0.99.0"}, headers={"Content-Type": "application/json"}, ) @@ -228,3 +239,54 @@ async def test_hassio_connection_error(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "hassio_confirm" assert result["errors"] == {"base": "connection_error"} + + +async def test_outdated_adguard_version(hass, aioclient_mock): + """Test we show abort when connecting with unsupported AdGuard version.""" + aioclient_mock.get( + "{}://{}:{}/control/status".format( + "https" if FIXTURE_USER_INPUT[CONF_SSL] else "http", + FIXTURE_USER_INPUT[CONF_HOST], + FIXTURE_USER_INPUT[CONF_PORT], + ), + json={"version": "v0.98.0"}, + headers={"Content-Type": "application/json"}, + ) + + flow = config_flow.AdGuardHomeFlowHandler() + flow.hass = hass + result = await flow.async_step_user(user_input=None) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "adguard_home_outdated" + assert result["description_placeholders"] == { + "current_version": "v0.98.0", + "minimal_version": MIN_ADGUARD_HOME_VERSION, + } + + +async def test_outdated_adguard_addon_version(hass, aioclient_mock): + """Test we show abort when connecting with unsupported AdGuard add-on version.""" + aioclient_mock.get( + "http://mock-adguard:3000/control/status", + json={"version": "v0.98.0"}, + headers={"Content-Type": "application/json"}, + ) + + result = await hass.config_entries.flow.async_init( + "adguard", + data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, + context={"source": "hassio"}, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "adguard_home_addon_outdated" + assert result["description_placeholders"] == { + "current_version": "v0.98.0", + "minimal_version": MIN_ADGUARD_HOME_VERSION, + } From efae9a24d5a51e1f2312306b8ea5bf2745cf6e82 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sat, 19 Oct 2019 20:27:15 +0100 Subject: [PATCH 453/639] remove duplicate unique_id, add unique_id for issues (#27916) --- homeassistant/components/geniushub/__init__.py | 2 +- homeassistant/components/geniushub/sensor.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index d9f6c877cbc..692c72e5776 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -214,7 +214,7 @@ class GeniusZone(GeniusEntity): super().__init__() self._zone = zone - self._unique_id = f"{broker.hub_uid}_device_{zone.id}" + self._unique_id = f"{broker.hub_uid}_zone_{zone.id}" self._max_temp = self._min_temp = self._supported_features = None diff --git a/homeassistant/components/geniushub/sensor.py b/homeassistant/components/geniushub/sensor.py index 2f5d9bceb8b..bd73c700e65 100644 --- a/homeassistant/components/geniushub/sensor.py +++ b/homeassistant/components/geniushub/sensor.py @@ -94,6 +94,8 @@ class GeniusIssue(GeniusEntity): super().__init__() self._hub = broker.client + self._unique_id = f"{broker.hub_uid}_{GH_LEVEL_MAPPING[level]}" + self._name = f"GeniusHub {GH_LEVEL_MAPPING[level]}" self._level = level self._issues = [] From febc48c84b3f36e9080e2565b8c7f9595d130137 Mon Sep 17 00:00:00 2001 From: Hmmbob <33529490+hmmbob@users.noreply.github.com> Date: Sat, 19 Oct 2019 21:40:45 +0200 Subject: [PATCH 454/639] Remove stride (#27934) * Remove stride * Remove Stride * Remove stride * Remove stride * Remove stride --- .coveragerc | 1 - homeassistant/components/stride/__init__.py | 1 - homeassistant/components/stride/manifest.json | 10 --- homeassistant/components/stride/notify.py | 84 ------------------- requirements_all.txt | 3 - 5 files changed, 99 deletions(-) delete mode 100644 homeassistant/components/stride/__init__.py delete mode 100644 homeassistant/components/stride/manifest.json delete mode 100644 homeassistant/components/stride/notify.py diff --git a/.coveragerc b/.coveragerc index 389d289ea20..7071312a99c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -637,7 +637,6 @@ omit = homeassistant/components/steam_online/sensor.py homeassistant/components/stiebel_eltron/* homeassistant/components/streamlabswater/* - homeassistant/components/stride/notify.py homeassistant/components/suez_water/* homeassistant/components/supervisord/sensor.py homeassistant/components/swiss_hydrological_data/sensor.py diff --git a/homeassistant/components/stride/__init__.py b/homeassistant/components/stride/__init__.py deleted file mode 100644 index 461a3ee744f..00000000000 --- a/homeassistant/components/stride/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The stride component.""" diff --git a/homeassistant/components/stride/manifest.json b/homeassistant/components/stride/manifest.json deleted file mode 100644 index 840984ad073..00000000000 --- a/homeassistant/components/stride/manifest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "domain": "stride", - "name": "Stride", - "documentation": "https://www.home-assistant.io/integrations/stride", - "requirements": [ - "pystride==0.1.7" - ], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/stride/notify.py b/homeassistant/components/stride/notify.py deleted file mode 100644 index 082d986491a..00000000000 --- a/homeassistant/components/stride/notify.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Stride platform for notify component.""" -import logging - -import voluptuous as vol - -from homeassistant.const import CONF_ROOM, CONF_TOKEN -import homeassistant.helpers.config_validation as cv - -from homeassistant.components.notify import ( - ATTR_DATA, - ATTR_TARGET, - PLATFORM_SCHEMA, - BaseNotificationService, -) - -_LOGGER = logging.getLogger(__name__) - -CONF_PANEL = "panel" -CONF_CLOUDID = "cloudid" - -DEFAULT_PANEL = None - -VALID_PANELS = {"info", "note", "tip", "warning", None} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_CLOUDID): cv.string, - vol.Required(CONF_ROOM): cv.string, - vol.Required(CONF_TOKEN): cv.string, - vol.Optional(CONF_PANEL, default=DEFAULT_PANEL): vol.In(VALID_PANELS), - } -) - - -def get_service(hass, config, discovery_info=None): - """Get the Stride notification service.""" - return StrideNotificationService( - config[CONF_TOKEN], config[CONF_ROOM], config[CONF_PANEL], config[CONF_CLOUDID] - ) - - -class StrideNotificationService(BaseNotificationService): - """Implement the notification service for Stride.""" - - def __init__(self, token, default_room, default_panel, cloudid): - """Initialize the service.""" - self._token = token - self._default_room = default_room - self._default_panel = default_panel - self._cloudid = cloudid - - from stride import Stride - - self._stride = Stride(self._cloudid, access_token=self._token) - - def send_message(self, message="", **kwargs): - """Send a message.""" - panel = self._default_panel - - if kwargs.get(ATTR_DATA) is not None: - data = kwargs.get(ATTR_DATA) - if (data.get(CONF_PANEL) is not None) and ( - data.get(CONF_PANEL) in VALID_PANELS - ): - panel = data.get(CONF_PANEL) - - message_text = { - "type": "paragraph", - "content": [{"type": "text", "text": message}], - } - panel_text = message_text - if panel is not None: - panel_text = { - "type": "panel", - "attrs": {"panelType": panel}, - "content": [message_text], - } - - message_doc = {"body": {"version": 1, "type": "doc", "content": [panel_text]}} - - targets = kwargs.get(ATTR_TARGET, [self._default_room]) - - for target in targets: - self._stride.message_room(target, message_doc) diff --git a/requirements_all.txt b/requirements_all.txt index d30d01f02c0..b4b60784a3d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1464,9 +1464,6 @@ pyspcwebgw==0.4.0 # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2 -# homeassistant.components.stride -pystride==0.1.7 - # homeassistant.components.suez_water pysuez==0.1.17 From bb5da77f2c6535754d6aeef4c6b21c71784ee7b9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 19 Oct 2019 12:44:51 -0700 Subject: [PATCH 455/639] Import shuffle (#27935) * Simplify persistent_notification ws command * Move cors import inside setup * Fix stream imports --- homeassistant/components/http/cors.py | 5 ++++- .../components/persistent_notification/__init__.py | 10 ++-------- homeassistant/components/stream/__init__.py | 8 ++++++-- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/http/cors.py b/homeassistant/components/http/cors.py index 0e6b9f9439a..de4547f4782 100644 --- a/homeassistant/components/http/cors.py +++ b/homeassistant/components/http/cors.py @@ -1,5 +1,4 @@ """Provide CORS support for the HTTP component.""" -import aiohttp_cors from aiohttp.web_urldispatcher import Resource, ResourceRoute, StaticResource from aiohttp.hdrs import ACCEPT, CONTENT_TYPE, ORIGIN, AUTHORIZATION @@ -22,6 +21,10 @@ VALID_CORS_TYPES = (Resource, ResourceRoute, StaticResource) @callback def setup_cors(app, origins): """Set up CORS.""" + # This import should remain here. That way the HTTP integration can always + # be imported by other integrations without it's requirements being installed. + import aiohttp_cors + cors = aiohttp_cors.setup( app, defaults={ diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 6b9c7c44ddf..33f17b18a80 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -52,11 +52,6 @@ STATE = "notifying" STATUS_UNREAD = "unread" STATUS_READ = "read" -WS_TYPE_GET_NOTIFICATIONS = "persistent_notification/get" -SCHEMA_WS_GET = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_GET_NOTIFICATIONS} -) - @bind_hass def create(hass, message, title=None, notification_id=None): @@ -198,14 +193,13 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: DOMAIN, SERVICE_MARK_READ, mark_read_service, SCHEMA_SERVICE_MARK_READ ) - hass.components.websocket_api.async_register_command( - WS_TYPE_GET_NOTIFICATIONS, websocket_get_notifications, SCHEMA_WS_GET - ) + hass.components.websocket_api.async_register_command(websocket_get_notifications) return True @callback +@websocket_api.websocket_command({vol.Required("type"): "persistent_notification/get"}) def websocket_get_notifications( hass: HomeAssistant, connection: websocket_api.ActiveConnection, diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 4c93ce46135..a83f05820e2 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -22,8 +22,6 @@ from .const import ( ) from .core import PROVIDERS from .hls import async_setup_hls -from .recorder import async_setup_recorder -from .worker import stream_worker try: import uvloop @@ -105,6 +103,9 @@ def request_stream(hass, stream_source, *, fmt="hls", keepalive=False, options=N async def async_setup(hass, config): """Set up stream.""" + # Keep import here so that we can import stream integration without installing reqs + from .recorder import async_setup_recorder + hass.data[DOMAIN] = {} hass.data[DOMAIN][ATTR_ENDPOINTS] = {} hass.data[DOMAIN][ATTR_STREAMS] = {} @@ -182,6 +183,9 @@ class Stream: def start(self): """Start a stream.""" + # Keep import here so that we can import stream integration without installing reqs + from .worker import stream_worker + if self._thread is None or not self._thread.isAlive(): self._thread_quit = threading.Event() self._thread = threading.Thread( From 2a269fb9eb78f4c845d5778de09bdead9e9f0345 Mon Sep 17 00:00:00 2001 From: Tim McCormick Date: Sat, 19 Oct 2019 21:54:36 +0100 Subject: [PATCH 456/639] Update pysonos to 0.0.24 (#27937) --- homeassistant/components/sonos/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 6d636f36b3f..7b0c041b2a9 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", "requirements": [ - "pysonos==0.0.23" + "pysonos==0.0.24" ], "dependencies": [], "ssdp": { diff --git a/requirements_all.txt b/requirements_all.txt index b4b60784a3d..a929e90ac3a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1456,7 +1456,7 @@ pysnmp==4.4.11 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.23 +pysonos==0.0.24 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a4fdf4eac7..0c6c0ea0a44 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -497,7 +497,7 @@ pysmartthings==0.6.9 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.23 +pysonos==0.0.24 # homeassistant.components.spc pyspcwebgw==0.4.0 From 5c50fa34056ba2cd0c91c06cbb6a9c894b4be92e Mon Sep 17 00:00:00 2001 From: Santobert Date: Sat, 19 Oct 2019 22:56:57 +0200 Subject: [PATCH 457/639] Bump pybotvac (#27933) --- homeassistant/components/neato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index a4d05e8849a..03f8089159e 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/neato", "requirements": [ - "pybotvac==0.0.16" + "pybotvac==0.0.17" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index a929e90ac3a..ad17b59b455 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1105,7 +1105,7 @@ pyblackbird==0.5 # pybluez==0.22 # homeassistant.components.neato -pybotvac==0.0.16 +pybotvac==0.0.17 # homeassistant.components.nissan_leaf pycarwings2==2.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c6c0ea0a44..9b59a1d806b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -390,7 +390,7 @@ pyarlo==0.2.3 pyblackbird==0.5 # homeassistant.components.neato -pybotvac==0.0.16 +pybotvac==0.0.17 # homeassistant.components.cast pychromecast==4.0.1 From eeb1bfc6f55bd5877c3bd46f59d82f7e93d693be Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 19 Oct 2019 16:31:15 -0500 Subject: [PATCH 458/639] Central update for Plex platforms (#27764) * Update Plex platforms together * Remove unnecessary methods * Overhaul of Plex update logic * Apply suggestions from code review Use set instead of list Co-Authored-By: Martin Hjelmare * Review suggestions and cleanup * Fixes, remove sensor throttle * Guarantee entity name, use common scheme * Keep name stable once set --- homeassistant/components/plex/__init__.py | 23 +- homeassistant/components/plex/config_flow.py | 2 +- homeassistant/components/plex/const.py | 7 +- homeassistant/components/plex/media_player.py | 342 ++++++------------ homeassistant/components/plex/sensor.py | 63 ++-- homeassistant/components/plex/server.py | 81 ++++- 6 files changed, 252 insertions(+), 266 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index ed94b6913bc..b6ed3245115 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,5 +1,6 @@ """Support to embed Plex.""" import asyncio +from datetime import timedelta import logging import plexapi.exceptions @@ -17,6 +18,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_USE_EPISODE_ART, @@ -26,6 +28,7 @@ from .const import ( DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL, + DISPATCHERS, DOMAIN as PLEX_DOMAIN, PLATFORMS, PLEX_MEDIA_PLAYER_OPTIONS, @@ -64,7 +67,9 @@ _LOGGER = logging.getLogger(__package__) def setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}}) + hass.data.setdefault( + PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}, DISPATCHERS: {}} + ) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: @@ -104,7 +109,7 @@ async def async_setup_entry(hass, entry): ) hass.config_entries.async_update_entry(entry, options=options) - plex_server = PlexServer(server_config, entry.options) + plex_server = PlexServer(hass, server_config, entry.options) try: await hass.async_add_executor_job(plex_server.connect) except requests.exceptions.ConnectionError as error: @@ -129,7 +134,9 @@ async def async_setup_entry(hass, entry): _LOGGER.debug( "Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use ) - hass.data[PLEX_DOMAIN][SERVERS][plex_server.machine_identifier] = plex_server + server_id = plex_server.machine_identifier + hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id] = [] for platform in PLATFORMS: hass.async_create_task( @@ -138,6 +145,10 @@ async def async_setup_entry(hass, entry): entry.add_update_listener(async_options_updated) + hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = async_track_time_interval( + hass, lambda now: plex_server.update_platforms(), timedelta(seconds=10) + ) + return True @@ -146,7 +157,11 @@ async def async_unload_entry(hass, entry): server_id = entry.data[CONF_SERVER_IDENTIFIER] cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id) - await hass.async_add_executor_job(cancel) + cancel() + + dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id) + for unsub in dispatchers: + unsub() tasks = [ hass.config_entries.async_forward_entry_unload(entry, platform) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index 9e74756977d..a11fb9119a6 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -79,7 +79,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} self.current_login = server_config - plex_server = PlexServer(server_config) + plex_server = PlexServer(self.hass, server_config) try: await self.hass.async_add_executor_job(plex_server.connect) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 0b436c4e208..c576f1d6a59 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -2,12 +2,13 @@ from homeassistant.const import __version__ DOMAIN = "plex" -NAME_FORMAT = "Plex {}" +NAME_FORMAT = "Plex ({})" DEFAULT_PORT = 32400 DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True +DISPATCHERS = "dispatchers" PLATFORMS = ["media_player", "sensor"] REFRESH_LISTENERS = "refresh_listeners" SERVERS = "servers" @@ -16,6 +17,10 @@ PLEX_CONFIG_FILE = "plex.conf" PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" PLEX_SERVER_CONFIG = "server_config" +PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal" +PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" +PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal" + CONF_SERVER = "server" CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index a49e4c9c057..4a48950a67c 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -1,5 +1,4 @@ """Support to interface with the Plex API.""" -from datetime import timedelta import json import logging from xml.etree.ElementTree import ParseError @@ -29,14 +28,17 @@ from homeassistant.const import ( STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.helpers.event import track_time_interval +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util import dt as dt_util from .const import ( CONF_SERVER_IDENTIFIER, + DISPATCHERS, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, - REFRESH_LISTENERS, + PLEX_NEW_MP_SIGNAL, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, SERVERS, ) @@ -53,142 +55,53 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex media_player from a config entry.""" - - def add_entities(entities, update_before_add=False): - """Sync version of async add entities.""" - hass.add_job(async_add_entities, entities, update_before_add) - - hass.async_add_executor_job(_setup_platform, hass, config_entry, add_entities) - - -def _setup_platform(hass, config_entry, add_entities_callback): - """Set up the Plex media_player platform.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] + + def async_new_media_players(new_entities): + _async_add_entities( + hass, config_entry, async_add_entities, server_id, new_entities + ) + + unsub = async_dispatcher_connect(hass, PLEX_NEW_MP_SIGNAL, async_new_media_players) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + +@callback +def _async_add_entities( + hass, config_entry, async_add_entities, server_id, new_entities +): + """Set up Plex media_player entities.""" + entities = [] plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] - plex_clients = {} - plex_sessions = {} - hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = track_time_interval( - hass, lambda now: update_devices(), timedelta(seconds=10) - ) + for entity_params in new_entities: + plex_mp = PlexMediaPlayer(plexserver, **entity_params) + entities.append(plex_mp) - def update_devices(): - """Update the devices objects.""" - try: - devices = plexserver.clients() - except plexapi.exceptions.BadRequest: - _LOGGER.exception("Error listing plex devices") - return - except requests.exceptions.RequestException as ex: - _LOGGER.warning( - "Could not connect to Plex server: %s (%s)", - plexserver.friendly_name, - ex, - ) - return - - new_plex_clients = [] - available_client_ids = [] - for device in devices: - # For now, let's allow all deviceClass types - if device.deviceClass in ["badClient"]: - continue - - available_client_ids.append(device.machineIdentifier) - - if device.machineIdentifier not in plex_clients: - new_client = PlexClient( - plexserver, device, None, plex_sessions, update_devices - ) - plex_clients[device.machineIdentifier] = new_client - _LOGGER.debug("New device: %s", device.machineIdentifier) - new_plex_clients.append(new_client) - else: - _LOGGER.debug("Refreshing device: %s", device.machineIdentifier) - plex_clients[device.machineIdentifier].refresh(device, None) - - # add devices with a session and no client (ex. PlexConnect Apple TV's) - try: - sessions = plexserver.sessions() - except plexapi.exceptions.BadRequest: - _LOGGER.exception("Error listing plex sessions") - return - except requests.exceptions.RequestException as ex: - _LOGGER.warning( - "Could not connect to Plex server: %s (%s)", - plexserver.friendly_name, - ex, - ) - return - - plex_sessions.clear() - for session in sessions: - for player in session.players: - plex_sessions[player.machineIdentifier] = session, player - - for machine_identifier, (session, player) in plex_sessions.items(): - if machine_identifier in available_client_ids: - # Avoid using session if already added as a device. - _LOGGER.debug("Skipping session, device exists: %s", machine_identifier) - continue - - if ( - machine_identifier not in plex_clients - and machine_identifier is not None - ): - new_client = PlexClient( - plexserver, player, session, plex_sessions, update_devices - ) - plex_clients[machine_identifier] = new_client - _LOGGER.debug("New session: %s", machine_identifier) - new_plex_clients.append(new_client) - else: - _LOGGER.debug("Refreshing session: %s", machine_identifier) - plex_clients[machine_identifier].refresh(None, session) - - for client in plex_clients.values(): - # force devices to idle that do not have a valid session - if client.session is None: - client.force_idle() - - client.set_availability( - client.machine_identifier in available_client_ids - or client.machine_identifier in plex_sessions - ) - - if client not in new_plex_clients: - client.schedule_update_ha_state() - - if new_plex_clients: - add_entities_callback(new_plex_clients) + async_add_entities(entities, True) -class PlexClient(MediaPlayerDevice): +class PlexMediaPlayer(MediaPlayerDevice): """Representation of a Plex device.""" - def __init__(self, plex_server, device, session, plex_sessions, update_devices): + def __init__(self, plex_server, device, session=None): """Initialize the Plex device.""" + self.plex_server = plex_server + self.device = device + self.session = session self._app_name = "" - self._device = None self._available = False - self._marked_unavailable = None self._device_protocol_capabilities = None self._is_player_active = False - self._is_player_available = False - self._player = None - self._machine_identifier = None + self._machine_identifier = device.machineIdentifier self._make = "" self._name = None self._player_state = "idle" self._previous_volume_level = 1 # Used in fake muting - self._session = None self._session_type = None self._session_username = None self._state = STATE_IDLE self._volume_level = 1 # since we can't retrieve remotely self._volume_muted = False # since we can't retrieve remotely - self.plex_server = plex_server - self.plex_sessions = plex_sessions - self.update_devices = update_devices # General self._media_content_id = None self._media_content_rating = None @@ -208,7 +121,22 @@ class PlexClient(MediaPlayerDevice): self._media_season = None self._media_series_title = None - self.refresh(device, session) + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + server_id = self.plex_server.machine_identifier + unsub = async_dispatcher_connect( + self.hass, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(self.unique_id), + self.async_refresh_media_player, + ) + self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + @callback + def async_refresh_media_player(self, device, session): + """Set instance objects and trigger an entity state update.""" + self.device = device + self.session = session + self.async_schedule_update_ha_state(True) def _clear_media_details(self): """Set all Media Items to None.""" @@ -232,52 +160,46 @@ class PlexClient(MediaPlayerDevice): # Clear library Name self._app_name = "" - def refresh(self, device, session): + def update(self): """Refresh key device data.""" self._clear_media_details() - if session: # Not being triggered by Chrome or FireTablet Plex App - self._session = session - if device: - self._device = device + self._available = self.device or self.session + name_base = None + + if self.device: try: - device_url = self._device.url("/") + device_url = self.device.url("/") except plexapi.exceptions.BadRequest: device_url = "127.0.0.1" if "127.0.0.1" in device_url: - self._device.proxyThroughServer() - self._session = None - self._machine_identifier = self._device.machineIdentifier - self._name = NAME_FORMAT.format(self._device.title or DEVICE_DEFAULT_NAME) - self._device_protocol_capabilities = self._device.protocolCapabilities + self.device.proxyThroughServer() + name_base = self.device.title or self.device.product + self._device_protocol_capabilities = self.device.protocolCapabilities + self._player_state = self.device.state - # set valid session, preferring device session - if self._device.machineIdentifier in self.plex_sessions: - self._session = self.plex_sessions.get( - self._device.machineIdentifier, [None, None] - )[0] - - if self._session: - if ( - self._device is not None - and self._device.machineIdentifier is not None - and self._session.players - ): - self._is_player_available = True - self._player = [ + if not self.session: + self.force_idle() + else: + session_device = next( + ( p - for p in self._session.players - if p.machineIdentifier == self._device.machineIdentifier - ][0] - self._name = NAME_FORMAT.format(self._player.title) - self._player_state = self._player.state - self._session_username = self._session.usernames[0] - self._make = self._player.device + for p in self.session.players + if p.machineIdentifier == self.device.machineIdentifier + ), + None, + ) + if session_device: + self._make = session_device.device or "" + self._player_state = session_device.state + name_base = name_base or session_device.title or session_device.product else: - self._is_player_available = False + _LOGGER.warning("No player associated with active session") + + self._session_username = self.session.usernames[0] # Calculate throttled position for proper progress display. - position = int(self._session.viewOffset / 1000) + position = int(self.session.viewOffset / 1000) now = dt_util.utcnow() if self._media_position is not None: pos_diff = position - self._media_position @@ -289,21 +211,22 @@ class PlexClient(MediaPlayerDevice): self._media_position_updated_at = now self._media_position = position - self._media_content_id = self._session.ratingKey - self._media_content_rating = getattr(self._session, "contentRating", None) + self._media_content_id = self.session.ratingKey + self._media_content_rating = getattr(self.session, "contentRating", None) + self._name = self._name or NAME_FORMAT.format(name_base or DEVICE_DEFAULT_NAME) self._set_player_state() - if self._is_player_active and self._session is not None: - self._session_type = self._session.type - self._media_duration = int(self._session.duration / 1000) + if self._is_player_active and self.session is not None: + self._session_type = self.session.type + self._media_duration = int(self.session.duration / 1000) # title (movie name, tv episode name, music song name) - self._media_title = self._session.title + self._media_title = self.session.title # media type self._set_media_type() self._app_name = ( - self._session.section().title - if self._session.section() is not None + self.session.section().title + if self.session.section() is not None else "" ) self._set_media_image() @@ -311,33 +234,21 @@ class PlexClient(MediaPlayerDevice): self._session_type = None def _set_media_image(self): - thumb_url = self._session.thumbUrl + thumb_url = self.session.thumbUrl if ( self.media_content_type is MEDIA_TYPE_TVSHOW and not self.plex_server.use_episode_art ): - thumb_url = self._session.url(self._session.grandparentThumb) + thumb_url = self.session.url(self.session.grandparentThumb) if thumb_url is None: _LOGGER.debug( - "Using media art because media thumb " "was not found: %s", - self.entity_id, + "Using media art because media thumb was not found: %s", self.name ) - thumb_url = self.session.url(self._session.art) + thumb_url = self.session.url(self.session.art) self._media_image_url = thumb_url - def set_availability(self, available): - """Set the device as available/unavailable noting time.""" - if not available: - self._clear_media_details() - if self._marked_unavailable is None: - self._marked_unavailable = dt_util.utcnow() - else: - self._marked_unavailable = None - - self._available = available - def _set_player_state(self): if self._player_state == "playing": self._is_player_active = True @@ -357,41 +268,41 @@ class PlexClient(MediaPlayerDevice): self._media_content_type = MEDIA_TYPE_TVSHOW # season number (00) - if callable(self._session.season): - self._media_season = str((self._session.season()).index).zfill(2) - elif self._session.parentIndex is not None: - self._media_season = self._session.parentIndex.zfill(2) + if callable(self.session.season): + self._media_season = str((self.session.season()).index).zfill(2) + elif self.session.parentIndex is not None: + self._media_season = self.session.parentIndex.zfill(2) else: self._media_season = None # show name - self._media_series_title = self._session.grandparentTitle + self._media_series_title = self.session.grandparentTitle # episode number (00) - if self._session.index is not None: - self._media_episode = str(self._session.index).zfill(2) + if self.session.index is not None: + self._media_episode = str(self.session.index).zfill(2) elif self._session_type == "movie": self._media_content_type = MEDIA_TYPE_MOVIE - if self._session.year is not None and self._media_title is not None: - self._media_title += " (" + str(self._session.year) + ")" + if self.session.year is not None and self._media_title is not None: + self._media_title += " (" + str(self.session.year) + ")" elif self._session_type == "track": self._media_content_type = MEDIA_TYPE_MUSIC - self._media_album_name = self._session.parentTitle - self._media_album_artist = self._session.grandparentTitle - self._media_track = self._session.index - self._media_artist = self._session.originalTitle + self._media_album_name = self.session.parentTitle + self._media_album_artist = self.session.grandparentTitle + self._media_track = self.session.index + self._media_artist = self.session.originalTitle # use album artist if track artist is missing if self._media_artist is None: _LOGGER.debug( - "Using album artist because track artist " "was not found: %s", - self.entity_id, + "Using album artist because track artist was not found: %s", + self.name, ) self._media_artist = self._media_album_artist def force_idle(self): """Force client to idle.""" self._state = STATE_IDLE - self._session = None + self.session = None self._clear_media_details() @property @@ -402,7 +313,7 @@ class PlexClient(MediaPlayerDevice): @property def unique_id(self): """Return the id of this plex client.""" - return self.machine_identifier + return self._machine_identifier @property def available(self): @@ -414,31 +325,11 @@ class PlexClient(MediaPlayerDevice): """Return the name of the device.""" return self._name - @property - def machine_identifier(self): - """Return the machine identifier of the device.""" - return self._machine_identifier - @property def app_name(self): """Return the library name of playing media.""" return self._app_name - @property - def device(self): - """Return the device, if any.""" - return self._device - - @property - def marked_unavailable(self): - """Return time device was marked unavailable.""" - return self._marked_unavailable - - @property - def session(self): - """Return the session, if any.""" - return self._session - @property def state(self): """Return the state of the device.""" @@ -462,8 +353,7 @@ class PlexClient(MediaPlayerDevice): """Return the content type of current playing media.""" if self._session_type == "clip": _LOGGER.debug( - "Clip content type detected, " "compatibility may vary: %s", - self.entity_id, + "Clip content type detected, compatibility may vary: %s", self.name ) return MEDIA_TYPE_TVSHOW if self._session_type == "episode": @@ -560,8 +450,8 @@ class PlexClient(MediaPlayerDevice): # no mute support if self.make.lower() == "shield android tv": _LOGGER.debug( - "Shield Android TV client detected, disabling mute " "controls: %s", - self.entity_id, + "Shield Android TV client detected, disabling mute controls: %s", + self.name, ) return ( SUPPORT_PAUSE @@ -579,7 +469,7 @@ class PlexClient(MediaPlayerDevice): _LOGGER.debug( "Tivo client detected, only enabling pause, play, " "stop, and off controls: %s", - self.entity_id, + self.name, ) return SUPPORT_PAUSE | SUPPORT_PLAY | SUPPORT_STOP | SUPPORT_TURN_OFF @@ -603,7 +493,7 @@ class PlexClient(MediaPlayerDevice): if self.device and "playback" in self._device_protocol_capabilities: self.device.setVolume(int(volume * 100), self._active_media_plexapi_type) self._volume_level = volume # store since we can't retrieve - self.update_devices() + self.plex_server.update_platforms() @property def volume_level(self): @@ -642,19 +532,19 @@ class PlexClient(MediaPlayerDevice): """Send play command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.play(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def media_pause(self): """Send pause command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.pause(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def media_stop(self): """Send stop command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.stop(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def turn_off(self): """Turn the client off.""" @@ -665,13 +555,13 @@ class PlexClient(MediaPlayerDevice): """Send next track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipNext(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def media_previous_track(self): """Send previous track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipPrevious(self._active_media_plexapi_type) - self.update_devices() + self.plex_server.update_platforms() def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" @@ -706,7 +596,7 @@ class PlexClient(MediaPlayerDevice): except requests.exceptions.ConnectTimeout: _LOGGER.error("Timed out playing on %s", self.name) - self.update_devices() + self.plex_server.update_platforms() def _get_music_media(self, library_name, src): """Find music media and return a Plex media object.""" diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 7d5b54356a0..3cde2adb8f4 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,19 +1,21 @@ """Support for Plex media server monitoring.""" -from datetime import timedelta import logging -import plexapi.exceptions -import requests.exceptions - +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from .const import CONF_SERVER_IDENTIFIER, DOMAIN as PLEX_DOMAIN, SERVERS +from .const import ( + CONF_SERVER_IDENTIFIER, + DISPATCHERS, + DOMAIN as PLEX_DOMAIN, + NAME_FORMAT, + PLEX_UPDATE_SENSOR_SIGNAL, + SERVERS, +) _LOGGER = logging.getLogger(__name__) -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Plex sensor platform. @@ -26,8 +28,9 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Plex sensor from a config entry.""" server_id = config_entry.data[CONF_SERVER_IDENTIFIER] - sensor = PlexSensor(hass.data[PLEX_DOMAIN][SERVERS][server_id]) - async_add_entities([sensor], True) + plexserver = hass.data[PLEX_DOMAIN][SERVERS][server_id] + sensor = PlexSensor(plexserver) + async_add_entities([sensor]) class PlexSensor(Entity): @@ -35,12 +38,27 @@ class PlexSensor(Entity): def __init__(self, plex_server): """Initialize the sensor.""" + self.sessions = [] self._state = None self._now_playing = [] self._server = plex_server - self._name = f"Plex ({plex_server.friendly_name})" + self._name = NAME_FORMAT.format(plex_server.friendly_name) self._unique_id = f"sensor-{plex_server.machine_identifier}" + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + server_id = self._server.machine_identifier + unsub = async_dispatcher_connect( + self.hass, PLEX_UPDATE_SENSOR_SIGNAL, self.async_refresh_sensor + ) + self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + @callback + def async_refresh_sensor(self, sessions): + """Set instance object and trigger an entity state update.""" + self.sessions = sessions + self.async_schedule_update_ha_state(True) + @property def name(self): """Return the name of the sensor.""" @@ -51,6 +69,11 @@ class PlexSensor(Entity): """Return the id of this plex client.""" return self._unique_id + @property + def should_poll(self): + """Return True if entity has to be polled for state.""" + return False + @property def state(self): """Return the state of the sensor.""" @@ -66,24 +89,10 @@ class PlexSensor(Entity): """Return the state attributes.""" return {content[0]: content[1] for content in self._now_playing} - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update method for Plex sensor.""" - try: - sessions = self._server.sessions() - except plexapi.exceptions.BadRequest: - _LOGGER.error( - "Error listing current Plex sessions on %s", self._server.friendly_name - ) - return - except requests.exceptions.RequestException as ex: - _LOGGER.warning( - "Temporary error connecting to %s (%s)", self._server.friendly_name, ex - ) - return - now_playing = [] - for sess in sessions: + for sess in self.sessions: user = sess.usernames[0] device = sess.players[0].title now_playing_user = f"{user} - {device}" @@ -120,5 +129,5 @@ class PlexSensor(Entity): now_playing_title += f" ({sess.year})" now_playing.append((now_playing_user, now_playing_title)) - self._state = len(sessions) + self._state = len(self.sessions) self._now_playing = now_playing diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d9ddc28c89a..128bcdd45c6 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,17 +1,24 @@ """Shared class to maintain Plex server instances.""" +import logging + import plexapi.myplex import plexapi.playqueue import plexapi.server from requests import Session +import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL +from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( CONF_SERVER, CONF_SHOW_ALL_CONTROLS, CONF_USE_EPISODE_ART, DEFAULT_VERIFY_SSL, + PLEX_NEW_MP_SIGNAL, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, + PLEX_UPDATE_SENSOR_SIGNAL, X_PLEX_DEVICE_NAME, X_PLEX_PLATFORM, X_PLEX_PRODUCT, @@ -19,6 +26,8 @@ from .const import ( ) from .errors import NoServersFound, ServerNotSpecified +_LOGGER = logging.getLogger(__name__) + # Set default headers sent by plexapi plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM @@ -31,9 +40,11 @@ plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() class PlexServer: """Manages a single Plex server connection.""" - def __init__(self, server_config, options=None): + def __init__(self, hass, server_config, options=None): """Initialize a Plex server instance.""" + self._hass = hass self._plex_server = None + self._known_clients = set() self._url = server_config.get(CONF_URL) self._token = server_config.get(CONF_TOKEN) self._server_name = server_config.get(CONF_SERVER) @@ -76,13 +87,69 @@ class PlexServer: else: _connect_with_token() - def clients(self): - """Pass through clients call to plexapi.""" - return self._plex_server.clients() + def refresh_entity(self, machine_identifier, device, session): + """Forward refresh dispatch to media_player.""" + dispatcher_send( + self._hass, + PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(machine_identifier), + device, + session, + ) - def sessions(self): - """Pass through sessions call to plexapi.""" - return self._plex_server.sessions() + def update_platforms(self): + """Update the platform entities.""" + available_clients = {} + new_clients = set() + + try: + devices = self._plex_server.clients() + sessions = self._plex_server.sessions() + except plexapi.exceptions.BadRequest: + _LOGGER.exception("Error requesting Plex client data from server") + return + except requests.exceptions.RequestException as ex: + _LOGGER.warning( + "Could not connect to Plex server: %s (%s)", self.friendly_name, ex + ) + return + + for device in devices: + available_clients[device.machineIdentifier] = {"device": device} + + if device.machineIdentifier not in self._known_clients: + new_clients.add(device.machineIdentifier) + _LOGGER.debug("New device: %s", device.machineIdentifier) + + for session in sessions: + for player in session.players: + available_clients.setdefault( + player.machineIdentifier, {"device": player} + ) + available_clients[player.machineIdentifier]["session"] = session + + if player.machineIdentifier not in self._known_clients: + new_clients.add(player.machineIdentifier) + _LOGGER.debug("New session: %s", player.machineIdentifier) + + new_entity_configs = [] + for client_id, client_data in available_clients.items(): + if client_id in new_clients: + new_entity_configs.append(client_data) + else: + self.refresh_entity( + client_id, client_data["device"], client_data.get("session") + ) + + self._known_clients.update(new_clients) + + idle_clients = self._known_clients.difference(available_clients) + for client_id in idle_clients: + self.refresh_entity(client_id, None, None) + + if new_entity_configs: + dispatcher_send(self._hass, PLEX_NEW_MP_SIGNAL, new_entity_configs) + + dispatcher_send(self._hass, PLEX_UPDATE_SENSOR_SIGNAL, sessions) @property def friendly_name(self): From bfba46d64a6778d6f98bd8b0c44e92f556088b7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sat, 19 Oct 2019 23:52:42 +0200 Subject: [PATCH 459/639] move imports in sonos component (#27938) --- homeassistant/components/sonos/__init__.py | 4 ++-- homeassistant/components/sonos/config_flow.py | 7 ++++--- homeassistant/components/sonos/media_player.py | 15 +++++++-------- tests/components/sonos/conftest.py | 2 +- tests/components/sonos/test_init.py | 2 +- tests/components/sonos/test_media_player.py | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index bd16cfe353a..d2c6210f01c 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -1,16 +1,16 @@ """Support to embed Sonos.""" import asyncio + import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_HOSTS, ATTR_ENTITY_ID, ATTR_TIME +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TIME, CONF_HOSTS from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN - CONF_ADVERTISE_ADDR = "advertise_addr" CONF_INTERFACE_ADDR = "interface_addr" diff --git a/homeassistant/components/sonos/config_flow.py b/homeassistant/components/sonos/config_flow.py index 3ce62f54a2f..42ac32163a4 100644 --- a/homeassistant/components/sonos/config_flow.py +++ b/homeassistant/components/sonos/config_flow.py @@ -1,13 +1,14 @@ """Config flow for SONOS.""" -from homeassistant.helpers import config_entry_flow +import pysonos + from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + from .const import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - import pysonos - return await hass.async_add_executor_job(pysonos.discover) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 41472413a07..94d252e9fee 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -8,8 +8,8 @@ import urllib import async_timeout import pysonos +from pysonos.exceptions import SoCoException, SoCoUPnPException import pysonos.snapshot -from pysonos.exceptions import SoCoUPnPException, SoCoException from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -40,11 +40,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import utcnow from . import ( - CONF_ADVERTISE_ADDR, - CONF_HOSTS, - CONF_INTERFACE_ADDR, - DATA_SERVICE_EVENT, - DOMAIN as SONOS_DOMAIN, ATTR_ALARM_ID, ATTR_ENABLED, ATTR_INCLUDE_LINKED_ZONES, @@ -56,6 +51,11 @@ from . import ( ATTR_TIME, ATTR_VOLUME, ATTR_WITH_GROUP, + CONF_ADVERTISE_ADDR, + CONF_HOSTS, + CONF_INTERFACE_ADDR, + DATA_SERVICE_EVENT, + DOMAIN as SONOS_DOMAIN, SERVICE_CLEAR_TIMER, SERVICE_JOIN, SERVICE_PLAY_QUEUE, @@ -1161,10 +1161,9 @@ class SonosEntity(MediaPlayerDevice): @soco_coordinator def set_alarm(self, data): """Set the alarm clock on the player.""" - from pysonos import alarms alarm = None - for one_alarm in alarms.get_alarms(self.soco): + for one_alarm in pysonos.alarms.get_alarms(self.soco): # pylint: disable=protected-access if one_alarm._alarm_id == str(data[ATTR_ALARM_ID]): alarm = one_alarm diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 135b7279244..e0257585ad5 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -2,8 +2,8 @@ from asynctest.mock import Mock, patch as patch import pytest -from homeassistant.components.sonos import DOMAIN from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS from tests.common import MockConfigEntry diff --git a/tests/components/sonos/test_init.py b/tests/components/sonos/test_init.py index b9ceaa49639..86ec90f32b8 100644 --- a/tests/components/sonos/test_init.py +++ b/tests/components/sonos/test_init.py @@ -2,8 +2,8 @@ from unittest.mock import patch from homeassistant import config_entries, data_entry_flow -from homeassistant.setup import async_setup_component from homeassistant.components import sonos +from homeassistant.setup import async_setup_component from tests.common import mock_coro diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index ec5861b536a..d21d3f01792 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,5 +1,5 @@ """Tests for the Sonos Media Player platform.""" -from homeassistant.components.sonos import media_player, DOMAIN +from homeassistant.components.sonos import DOMAIN, media_player from homeassistant.setup import async_setup_component From 2c00ff7e522ccb16a023816c313f4747d718810d Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 20 Oct 2019 00:32:18 +0000 Subject: [PATCH 460/639] [ci skip] Translation update --- .../components/abode/.translations/pl.json | 14 ++++++++++++-- .../components/adguard/.translations/en.json | 2 +- .../alarm_control_panel/.translations/ca.json | 11 +++++++++++ .../alarm_control_panel/.translations/pl.json | 11 +++++++++++ .../components/axis/.translations/ca.json | 1 + .../components/axis/.translations/pl.json | 1 + .../components/cover/.translations/pl.json | 10 ++++++++++ .../components/deconz/.translations/ca.json | 1 + .../components/deconz/.translations/pl.json | 1 + .../components/ifttt/.translations/ca.json | 2 +- .../components/linky/.translations/pl.json | 2 +- .../components/lock/.translations/ca.json | 5 +++++ .../components/lock/.translations/pl.json | 13 +++++++++++++ .../components/mailgun/.translations/ca.json | 2 +- .../components/opentherm_gw/.translations/ca.json | 11 +++++++++++ .../components/opentherm_gw/.translations/pl.json | 3 ++- .../components/soma/.translations/ca.json | 10 ++++++++++ .../components/soma/.translations/pl.json | 5 ++++- .../components/twilio/.translations/ca.json | 2 +- 19 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 homeassistant/components/alarm_control_panel/.translations/ca.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/pl.json create mode 100644 homeassistant/components/cover/.translations/pl.json create mode 100644 homeassistant/components/lock/.translations/pl.json diff --git a/homeassistant/components/abode/.translations/pl.json b/homeassistant/components/abode/.translations/pl.json index 09fbdc93241..c3f3b8f2c88 100644 --- a/homeassistant/components/abode/.translations/pl.json +++ b/homeassistant/components/abode/.translations/pl.json @@ -1,12 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja Abode." + }, + "error": { + "connection_error": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z Abode.", + "identifier_exists": "Konto zosta\u0142o ju\u017c zarejestrowane", + "invalid_credentials": "Nieprawid\u0142owe dane uwierzytelniaj\u0105ce" + }, "step": { "user": { "data": { "password": "Has\u0142o", "username": "Adres e-mail" - } + }, + "title": "Wprowad\u017a informacje logowania Abode" } - } + }, + "title": "Abode" } } \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/en.json b/homeassistant/components/adguard/.translations/en.json index 8bfb8516fd8..00d048c3343 100644 --- a/homeassistant/components/adguard/.translations/en.json +++ b/homeassistant/components/adguard/.translations/en.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", "adguard_home_addon_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}. Please update your Hass.io AdGuard Home add-on.", + "adguard_home_outdated": "This integration requires AdGuard Home {minimal_version} or higher, you have {current_version}.", "existing_instance_updated": "Updated existing configuration.", "single_instance_allowed": "Only a single configuration of AdGuard Home is allowed." }, diff --git a/homeassistant/components/alarm_control_panel/.translations/ca.json b/homeassistant/components/alarm_control_panel/.translations/ca.json new file mode 100644 index 00000000000..8d95d5f6485 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/ca.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Activa {entity_name} fora", + "arm_home": "Activa {entity_name} a casa", + "arm_night": "Activa {entity_name} nocturn", + "disarm": "Desactiva {entity_name}", + "trigger": "Dispara {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/pl.json b/homeassistant/components/alarm_control_panel/.translations/pl.json new file mode 100644 index 00000000000..a5dc326c267 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/pl.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "uzbr\u00f3j (poza domem) {entity_name}", + "arm_home": "uzbr\u00f3j (w domu) {entity_name}", + "arm_night": "uzbr\u00f3j (noc) {entity_name}", + "disarm": "rozbr\u00f3j {entity_name}", + "trigger": "wyzw\u00f3l {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/ca.json b/homeassistant/components/axis/.translations/ca.json index 75dd89ef9c1..3458dcc4529 100644 --- a/homeassistant/components/axis/.translations/ca.json +++ b/homeassistant/components/axis/.translations/ca.json @@ -12,6 +12,7 @@ "device_unavailable": "El dispositiu no est\u00e0 disponible", "faulty_credentials": "Credencials d'usuari incorrectes" }, + "flow_title": "Dispositiu d'eix: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/pl.json b/homeassistant/components/axis/.translations/pl.json index 88e80360536..4ca87310f48 100644 --- a/homeassistant/components/axis/.translations/pl.json +++ b/homeassistant/components/axis/.translations/pl.json @@ -12,6 +12,7 @@ "device_unavailable": "Urz\u0105dzenie jest niedost\u0119pne", "faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce" }, + "flow_title": "Urz\u0105dzenie Axis: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/cover/.translations/pl.json b/homeassistant/components/cover/.translations/pl.json new file mode 100644 index 00000000000..4adc0c17b54 --- /dev/null +++ b/homeassistant/components/cover/.translations/pl.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "pokrywa {entity_name} jest zamkni\u0119ta", + "is_closing": "{entity_name} si\u0119 zamyka", + "is_open": "pokrywa {entity_name} jest otwarta", + "is_opening": "{entity_name} si\u0119 otwiera" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index d36de4acc1e..a2facf0d7c2 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -11,6 +11,7 @@ "error": { "no_key": "No s'ha pogut obtenir una clau API" }, + "flow_title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index 6d36d6ab39d..ac9f06f1f17 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -11,6 +11,7 @@ "error": { "no_key": "Nie mo\u017cna uzyska\u0107 klucza API" }, + "flow_title": "Bramka deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/ifttt/.translations/ca.json b/homeassistant/components/ifttt/.translations/ca.json index 597328a2ee4..979ed3cd71f 100644 --- a/homeassistant/components/ifttt/.translations/ca.json +++ b/homeassistant/components/ifttt/.translations/ca.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, necessitar\u00e0s utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- Method: POST \n- Content Type: application/json \n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, necessitar\u00e0s utilitzar l'acci\u00f3 \"Make a web resquest\" de [IFTTT Webhook applet]({applet_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- Method: POST \n- Content Type: application/json \n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants." }, "step": { "user": { diff --git a/homeassistant/components/linky/.translations/pl.json b/homeassistant/components/linky/.translations/pl.json index a4f68fa8687..d4fa7ee4d11 100644 --- a/homeassistant/components/linky/.translations/pl.json +++ b/homeassistant/components/linky/.translations/pl.json @@ -14,7 +14,7 @@ "user": { "data": { "password": "Has\u0142o", - "username": "E-mail" + "username": "Adres e-mail" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", "title": "Linky" diff --git a/homeassistant/components/lock/.translations/ca.json b/homeassistant/components/lock/.translations/ca.json index 0e05d512bf4..53198a21573 100644 --- a/homeassistant/components/lock/.translations/ca.json +++ b/homeassistant/components/lock/.translations/ca.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Bloqueja {entity_name}", + "open": "Obre {entity_name}", + "unlock": "Desbloqueja {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} est\u00e0 bloquejat/ada", "is_unlocked": "{entity_name} est\u00e0 desbloquejat/ada" diff --git a/homeassistant/components/lock/.translations/pl.json b/homeassistant/components/lock/.translations/pl.json new file mode 100644 index 00000000000..a3fe7358398 --- /dev/null +++ b/homeassistant/components/lock/.translations/pl.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "zablokuj {entity_name}", + "open": "otw\u00f3rz {entity_name}", + "unlock": "odblokuj {entity_name}" + }, + "condition_type": { + "is_locked": "zamek {entity_name} jest zamkni\u0119ty", + "is_unlocked": "zamek {entity_name} jest otwarty" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/.translations/ca.json b/homeassistant/components/mailgun/.translations/ca.json index f43467de7d9..6bcb737588a 100644 --- a/homeassistant/components/mailgun/.translations/ca.json +++ b/homeassistant/components/mailgun/.translations/ca.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Mailgun]({mailgun_url}). \n\nCompleta la seg\u00fcent informaci\u00f3: \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants." }, "step": { "user": { diff --git a/homeassistant/components/opentherm_gw/.translations/ca.json b/homeassistant/components/opentherm_gw/.translations/ca.json index 0224d663a83..07567149063 100644 --- a/homeassistant/components/opentherm_gw/.translations/ca.json +++ b/homeassistant/components/opentherm_gw/.translations/ca.json @@ -19,5 +19,16 @@ } }, "title": "Passarel\u00b7la d'OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura de la planta", + "precision": "Precisi\u00f3" + }, + "description": "Opcions del la passarel\u00b7la d'enlla\u00e7 d\u2019OpenTherm" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index 3d4c643b848..e4403420b11 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -26,7 +26,8 @@ "data": { "floor_temperature": "Temperatura pod\u0142ogi", "precision": "Precyzja" - } + }, + "description": "Opcje dla bramki OpenTherm" } } } diff --git a/homeassistant/components/soma/.translations/ca.json b/homeassistant/components/soma/.translations/ca.json index 6bd4737d6fc..18b33d1bc9b 100644 --- a/homeassistant/components/soma/.translations/ca.json +++ b/homeassistant/components/soma/.translations/ca.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Soma." }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "description": "Introdueix la informaci\u00f3 de connexi\u00f3 de SOMA Connect.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/pl.json b/homeassistant/components/soma/.translations/pl.json index dc843f29fd5..4d783f3f0a0 100644 --- a/homeassistant/components/soma/.translations/pl.json +++ b/homeassistant/components/soma/.translations/pl.json @@ -11,8 +11,11 @@ "step": { "user": { "data": { + "host": "Host", "port": "Port" - } + }, + "description": "Wprowad\u017a ustawienia po\u0142\u0105czenia SOMA Connect.", + "title": "SOMA Connect" } }, "title": "Soma" diff --git a/homeassistant/components/twilio/.translations/ca.json b/homeassistant/components/twilio/.translations/ca.json index 324ab0dd69a..bad78e51a36 100644 --- a/homeassistant/components/twilio/.translations/ca.json +++ b/homeassistant/components/twilio/.translations/ca.json @@ -5,7 +5,7 @@ "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Twilio]({twilio_url}).\n\nCompleta la seg\u00fcent informaci\u00f3 : \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar els automatismes per gestionar dades entrants." + "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar [Webhooks amb Twilio]({twilio_url}).\n\nCompleta la seg\u00fcent informaci\u00f3 : \n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n- Tipus de contingut: application/x-www-form-urlencoded\n\nConsulta la [documentaci\u00f3]({docs_url}) sobre com configurar les automatitzacions per gestionar dades entrants." }, "step": { "user": { From 2706e3289dfaf6b0f1c56f35a51e430b20f90bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 10:05:11 +0200 Subject: [PATCH 461/639] Move imports in smappee component (#27943) * move imports in smappee component * fix: unneeded object inheritance --- homeassistant/components/smappee/__init__.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/smappee/__init__.py b/homeassistant/components/smappee/__init__.py index 0da0b29fbc2..ecab09f6ff9 100644 --- a/homeassistant/components/smappee/__init__.py +++ b/homeassistant/components/smappee/__init__.py @@ -1,13 +1,16 @@ """Support for Smappee energy monitor.""" -import logging from datetime import datetime, timedelta +import logging import re -import voluptuous as vol + from requests.exceptions import RequestException -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_HOST -from homeassistant.util import Throttle -from homeassistant.helpers.discovery import load_platform +import smappy +import voluptuous as vol + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -72,7 +75,6 @@ class Smappee: self, client_id, client_secret, username, password, host, host_password ): """Initialize the data.""" - import smappy self._remote_active = False self._local_active = False From a5ec5b567e45a3ad6c204a98277398022089b275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 10:05:37 +0200 Subject: [PATCH 462/639] move imports in snapcast component (#27940) --- homeassistant/components/snapcast/__init__.py | 1 + .../components/snapcast/media_player.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/snapcast/__init__.py b/homeassistant/components/snapcast/__init__.py index 9e41bd8ff38..e6c574b7b2b 100644 --- a/homeassistant/components/snapcast/__init__.py +++ b/homeassistant/components/snapcast/__init__.py @@ -1,6 +1,7 @@ """The snapcast component.""" import asyncio + import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID diff --git a/homeassistant/components/snapcast/media_player.py b/homeassistant/components/snapcast/media_player.py index 81cd6538578..c3c9138eb89 100644 --- a/homeassistant/components/snapcast/media_player.py +++ b/homeassistant/components/snapcast/media_player.py @@ -2,9 +2,11 @@ import logging import socket +import snapcast.control +from snapcast.control.server import CONTROL_PORT import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, @@ -24,12 +26,12 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( - DOMAIN, - SERVICE_SNAPSHOT, - SERVICE_RESTORE, - SERVICE_JOIN, - SERVICE_UNJOIN, ATTR_MASTER, + DOMAIN, + SERVICE_JOIN, + SERVICE_RESTORE, + SERVICE_SNAPSHOT, + SERVICE_UNJOIN, ) _LOGGER = logging.getLogger(__name__) @@ -55,8 +57,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Snapcast platform.""" - import snapcast.control - from snapcast.control.server import CONTROL_PORT host = config.get(CONF_HOST) port = config.get(CONF_PORT, CONTROL_PORT) From e01562ceea357b7fbf568a57adca9e099d9e8fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 10:06:32 +0200 Subject: [PATCH 463/639] Move imports in snmp component (#27939) * move imports in snmp component * fix: move hlapi import top level --- .../components/snmp/device_tracker.py | 4 ++-- homeassistant/components/snmp/sensor.py | 23 +++++++++---------- homeassistant/components/snmp/switch.py | 22 ++++++++++-------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index a628c426e0f..eafae9537e5 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -2,6 +2,8 @@ import binascii import logging +from pysnmp.entity import config as cfg +from pysnmp.entity.rfc3413.oneliner import cmdgen import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -45,8 +47,6 @@ class SnmpScanner(DeviceScanner): def __init__(self, config): """Initialize the scanner.""" - from pysnmp.entity.rfc3413.oneliner import cmdgen - from pysnmp.entity import config as cfg self.snmp = cmdgen.CommandGenerator() diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 5e6b5ed1f28..b369ec83c58 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -2,6 +2,17 @@ from datetime import timedelta import logging +import pysnmp.hlapi.asyncio as hlapi +from pysnmp.hlapi.asyncio import ( + CommunityData, + ContextData, + ObjectIdentity, + ObjectType, + SnmpEngine, + UdpTransportTarget, + UsmUserData, + getCmd, +) import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -70,16 +81,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the SNMP sensor.""" - from pysnmp.hlapi.asyncio import ( - getCmd, - CommunityData, - SnmpEngine, - UdpTransportTarget, - ContextData, - ObjectType, - ObjectIdentity, - UsmUserData, - ) name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -101,7 +102,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= value_template.hass = hass if version == "3": - import pysnmp.hlapi.asyncio as hlapi if not authkey: authproto = "none" @@ -194,7 +194,6 @@ class SnmpData: async def async_update(self): """Get the latest data from the remote SNMP capable host.""" - from pysnmp.hlapi.asyncio import getCmd, ObjectType, ObjectIdentity errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 204e98cca83..aac43208a1f 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -1,6 +1,18 @@ """Support for SNMP enabled switch.""" import logging +import pysnmp.hlapi.asyncio as hlapi +from pysnmp.hlapi.asyncio import ( + CommunityData, + ContextData, + ObjectIdentity, + ObjectType, + SnmpEngine, + UdpTransportTarget, + UsmUserData, + getCmd, + setCmd, +) import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice @@ -136,13 +148,6 @@ class SnmpSwitch(SwitchDevice): command_payload_off, ): """Initialize the switch.""" - from pysnmp.hlapi.asyncio import ( - CommunityData, - ContextData, - SnmpEngine, - UdpTransportTarget, - UsmUserData, - ) self._name = name self._baseoid = baseoid @@ -157,7 +162,6 @@ class SnmpSwitch(SwitchDevice): self._payload_off = payload_off if version == "3": - import pysnmp.hlapi.asyncio as hlapi if not authkey: authproto = "none" @@ -194,7 +198,6 @@ class SnmpSwitch(SwitchDevice): async def async_update(self): """Update the state.""" - from pysnmp.hlapi.asyncio import getCmd, ObjectType, ObjectIdentity errindication, errstatus, errindex, restable = await getCmd( *self._request_args, ObjectType(ObjectIdentity(self._baseoid)) @@ -228,7 +231,6 @@ class SnmpSwitch(SwitchDevice): return self._state async def _set(self, value): - from pysnmp.hlapi.asyncio import setCmd, ObjectType, ObjectIdentity await setCmd( *self._request_args, ObjectType(ObjectIdentity(self._commandoid), value) From 9571f869d138926d2272b5cb968f138e61d92d42 Mon Sep 17 00:00:00 2001 From: Jacob Mansfield Date: Sun, 20 Oct 2019 09:07:34 +0100 Subject: [PATCH 464/639] Fix whois error, check expiration_date for list and pick first (#27930) --- homeassistant/components/whois/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/whois/sensor.py b/homeassistant/components/whois/sensor.py index 09cf40f193f..3c78d80ba92 100644 --- a/homeassistant/components/whois/sensor.py +++ b/homeassistant/components/whois/sensor.py @@ -119,7 +119,10 @@ class WhoisSensor(Entity): attrs = {} expiration_date = response["expiration_date"] - attrs[ATTR_EXPIRES] = expiration_date.isoformat() + if isinstance(expiration_date, list): + attrs[ATTR_EXPIRES] = expiration_date[0].isoformat() + else: + attrs[ATTR_EXPIRES] = expiration_date.isoformat() if "nameservers" in response: attrs[ATTR_NAME_SERVERS] = " ".join(response["nameservers"]) From ed46834a30fcff447ad674ad5cd584e8e76d7308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 10:10:27 +0200 Subject: [PATCH 465/639] Move imports in sql component (#27713) * move imports in sql component * fix: variable redeclaration * fix: close test db session on platform setup --- homeassistant/components/sql/sensor.py | 18 +++++++++--------- tests/components/sql/test_sensor.py | 3 ++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sql/sensor.py b/homeassistant/components/sql/sensor.py index 3b32f2747f1..52899c7da80 100644 --- a/homeassistant/components/sql/sensor.py +++ b/homeassistant/components/sql/sensor.py @@ -1,13 +1,15 @@ """Sensor from an SQL Query.""" -import decimal import datetime +import decimal import logging +import sqlalchemy +from sqlalchemy.orm import scoped_session, sessionmaker import voluptuous as vol +from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_DB_FILE, DEFAULT_URL from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE -from homeassistant.components.recorder import CONF_DB_URL, DEFAULT_URL, DEFAULT_DB_FILE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -46,20 +48,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if not db_url: db_url = DEFAULT_URL.format(hass_config_path=hass.config.path(DEFAULT_DB_FILE)) - import sqlalchemy - from sqlalchemy.orm import sessionmaker, scoped_session - try: engine = sqlalchemy.create_engine(db_url) - sessionmaker = scoped_session(sessionmaker(bind=engine)) + sessmaker = scoped_session(sessionmaker(bind=engine)) # Run a dummy query just to test the db_url - sess = sessionmaker() + sess = sessmaker() sess.execute("SELECT 1;") except sqlalchemy.exc.SQLAlchemyError as err: _LOGGER.error("Couldn't connect using %s DB_URL: %s", db_url, err) return + finally: + sess.close() queries = [] @@ -74,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): value_template.hass = hass sensor = SQLSensor( - name, sessionmaker, query_str, column_name, unit, value_template + name, sessmaker, query_str, column_name, unit, value_template ) queries.append(sensor) @@ -120,7 +121,6 @@ class SQLSensor(Entity): def update(self): """Retrieve sensor data from the query.""" - import sqlalchemy try: sess = self.sessionmaker() diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index 28357ab34b5..afc7bebea09 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -1,11 +1,12 @@ """The test for the sql sensor platform.""" import unittest + import pytest import voluptuous as vol from homeassistant.components.sql.sensor import validate_sql_select -from homeassistant.setup import setup_component from homeassistant.const import STATE_UNKNOWN +from homeassistant.setup import setup_component from tests.common import get_test_home_assistant From c42ca94a8656c68eebeed6c108a90afa5a2b5758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 12:14:07 +0200 Subject: [PATCH 466/639] move imports in smarthab component (#27942) --- homeassistant/components/smarthab/__init__.py | 4 ++-- homeassistant/components/smarthab/cover.py | 16 +++++++++------- homeassistant/components/smarthab/light.py | 8 +++++--- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/smarthab/__init__.py b/homeassistant/components/smarthab/__init__.py index 7206bea110b..ef2da4e9a1d 100644 --- a/homeassistant/components/smarthab/__init__.py +++ b/homeassistant/components/smarthab/__init__.py @@ -6,11 +6,12 @@ https://home-assistant.io/integrations/smarthab/ """ import logging +import pysmarthab import voluptuous as vol -from homeassistant.helpers.discovery import load_platform from homeassistant.const import CONF_EMAIL, CONF_PASSWORD import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform DOMAIN = "smarthab" DATA_HUB = "hub" @@ -32,7 +33,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config) -> bool: """Set up the SmartHab platform.""" - import pysmarthab sh_conf = config.get(DOMAIN) diff --git a/homeassistant/components/smarthab/cover.py b/homeassistant/components/smarthab/cover.py index 3d5b4259aa9..9bcb89b7ab4 100644 --- a/homeassistant/components/smarthab/cover.py +++ b/homeassistant/components/smarthab/cover.py @@ -4,18 +4,21 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/smarthab/ """ -import logging from datetime import timedelta +import logging + +import pysmarthab from requests.exceptions import Timeout from homeassistant.components.cover import ( - CoverDevice, - SUPPORT_OPEN, - SUPPORT_CLOSE, - SUPPORT_SET_POSITION, ATTR_POSITION, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverDevice, ) -from . import DOMAIN, DATA_HUB + +from . import DATA_HUB, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -24,7 +27,6 @@ SCAN_INTERVAL = timedelta(seconds=60) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the SmartHab roller shutters platform.""" - import pysmarthab hub = hass.data[DOMAIN][DATA_HUB] devices = hub.get_device_list() diff --git a/homeassistant/components/smarthab/light.py b/homeassistant/components/smarthab/light.py index a8a55dea48a..bc6eb31fd04 100644 --- a/homeassistant/components/smarthab/light.py +++ b/homeassistant/components/smarthab/light.py @@ -4,12 +4,15 @@ Support for SmartHab device integration. For more details about this component, please refer to the documentation at https://home-assistant.io/integrations/smarthab/ """ -import logging from datetime import timedelta +import logging + +import pysmarthab from requests.exceptions import Timeout from homeassistant.components.light import Light -from . import DOMAIN, DATA_HUB + +from . import DATA_HUB, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -18,7 +21,6 @@ SCAN_INTERVAL = timedelta(seconds=60) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the SmartHab lights platform.""" - import pysmarthab hub = hass.data[DOMAIN][DATA_HUB] devices = hub.get_device_list() From 5ce437dc304520c08c7792c8cc3fafa41b3959de Mon Sep 17 00:00:00 2001 From: Quentame Date: Sun, 20 Oct 2019 12:15:46 +0200 Subject: [PATCH 467/639] Fixing config_entries.async_forward_entry_unload calls (step 1) (#27857) --- homeassistant/components/cert_expiry/__init__.py | 5 +---- homeassistant/components/linky/__init__.py | 5 +---- homeassistant/components/locative/__init__.py | 3 +-- homeassistant/components/luftdaten/__init__.py | 5 +---- homeassistant/components/withings/__init__.py | 6 +----- 5 files changed, 5 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/cert_expiry/__init__.py b/homeassistant/components/cert_expiry/__init__.py index f5078219809..28a79a3e505 100644 --- a/homeassistant/components/cert_expiry/__init__.py +++ b/homeassistant/components/cert_expiry/__init__.py @@ -19,7 +19,4 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass, entry): """Unload a config entry.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_unload(entry, "sensor") - ) - return True + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/linky/__init__.py b/homeassistant/components/linky/__init__.py index ad5b6743d37..1d382b43525 100644 --- a/homeassistant/components/linky/__init__.py +++ b/homeassistant/components/linky/__init__.py @@ -55,7 +55,4 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload Linky sensors.""" - hass.async_create_task( - hass.config_entries.async_forward_entry_unload(entry, "sensor") - ) - return True + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/locative/__init__.py b/homeassistant/components/locative/__init__.py index 61e0b1f7474..ed8bcb6e7e5 100644 --- a/homeassistant/components/locative/__init__.py +++ b/homeassistant/components/locative/__init__.py @@ -127,8 +127,7 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" hass.components.webhook.async_unregister(entry.data[CONF_WEBHOOK_ID]) hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() - await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) - return True + return await hass.config_entries.async_forward_entry_unload(entry, DEVICE_TRACKER) # pylint: disable=invalid-name diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py index ac524502f8d..3dca82404c0 100644 --- a/homeassistant/components/luftdaten/__init__.py +++ b/homeassistant/components/luftdaten/__init__.py @@ -172,12 +172,9 @@ async def async_unload_entry(hass, config_entry): ) remove_listener() - for component in ("sensor",): - await hass.config_entries.async_forward_entry_unload(config_entry, component) - hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT].pop(config_entry.entry_id) - return True + return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") class LuftDatenData: diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index ecefa681b87..baed9300d46 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -92,8 +92,4 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload Withings config entry.""" - await hass.async_create_task( - hass.config_entries.async_forward_entry_unload(entry, "sensor") - ) - - return True + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") From 5a592f1291d37dab5b5cd514e2841d7343085700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mrozek?= Date: Sun, 20 Oct 2019 14:33:58 +0200 Subject: [PATCH 468/639] move imports in sma component (#27945) --- homeassistant/components/sma/sensor.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sma/sensor.py b/homeassistant/components/sma/sensor.py index 56e10b03d2a..ff1c48a141d 100644 --- a/homeassistant/components/sma/sensor.py +++ b/homeassistant/components/sma/sensor.py @@ -3,17 +3,18 @@ import asyncio from datetime import timedelta import logging +import pysma import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, + CONF_PATH, CONF_SCAN_INTERVAL, CONF_SSL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, - CONF_PATH, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -35,8 +36,6 @@ GROUPS = ["user", "installer"] def _check_sensor_schema(conf): """Check sensors and attributes are valid.""" try: - import pysma - valid = [s.name for s in pysma.Sensors()] except (ImportError, AttributeError): return conf @@ -87,7 +86,6 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up SMA WebConnect sensor.""" - import pysma # Check config again during load - dependency available config = _check_sensor_schema(config) From ac5ce4136edb836e439acf9f5449cf1039087277 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sun, 20 Oct 2019 18:11:48 +0200 Subject: [PATCH 469/639] Remove tplink device tracker (#27936) * Remove tplink device tracker Version 0.94 added a distress signal and since then nothing has happened. This commit removes the device tracker which should have never been a part of tplink integration in the first place as it does not share anything with this pyhs100-based integration / kasa smarthome. * add updated requirements_test_all that was forgotten * remove unit tests --- .../components/tplink/device_tracker.py | 508 ------------------ homeassistant/components/tplink/manifest.json | 3 +- requirements_all.txt | 3 - requirements_test_all.txt | 3 - .../components/tplink/test_device_tracker.py | 71 --- 5 files changed, 1 insertion(+), 587 deletions(-) delete mode 100644 homeassistant/components/tplink/device_tracker.py delete mode 100644 tests/components/tplink/test_device_tracker.py diff --git a/homeassistant/components/tplink/device_tracker.py b/homeassistant/components/tplink/device_tracker.py deleted file mode 100644 index ce17f6e465f..00000000000 --- a/homeassistant/components/tplink/device_tracker.py +++ /dev/null @@ -1,508 +0,0 @@ -"""Support for TP-Link routers.""" -import base64 -from datetime import datetime -import hashlib -import logging -import re - -from aiohttp.hdrs import ( - ACCEPT, - ACCEPT_ENCODING, - ACCEPT_LANGUAGE, - CACHE_CONTROL, - CONNECTION, - CONTENT_TYPE, - COOKIE, - KEEP_ALIVE, - PRAGMA, - REFERER, - USER_AGENT, -) -import requests -from tplink.tplink import TpLinkClient -import voluptuous as vol - -from homeassistant.components.device_tracker import ( - DOMAIN, - PLATFORM_SCHEMA, - DeviceScanner, -) -from homeassistant.const import ( - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - HTTP_HEADER_X_REQUESTED_WITH, -) -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -HTTP_HEADER_NO_CACHE = "no-cache" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - } -) - - -def get_scanner(hass, config): - """ - Validate the configuration and return a TP-Link scanner. - - The default way of integrating devices is to use a pypi - - package, The TplinkDeviceScanner has been refactored - - to depend on a pypi package, the other implementations - - should be gradually migrated in the pypi package - - """ - _LOGGER.warning( - "TP-Link device tracker is unmaintained and will be " - "removed in the future releases if no maintainer is " - "found. If you have interest in this integration, " - "feel free to create a pull request to move this code " - "to a new 'tplink_router' integration and refactoring " - "the device-specific parts to the tplink library" - ) - for cls in [ - TplinkDeviceScanner, - Tplink5DeviceScanner, - Tplink4DeviceScanner, - Tplink3DeviceScanner, - Tplink2DeviceScanner, - Tplink1DeviceScanner, - ]: - scanner = cls(config[DOMAIN]) - if scanner.success_init: - return scanner - - return None - - -class TplinkDeviceScanner(DeviceScanner): - """Queries the router for connected devices.""" - - def __init__(self, config): - """Initialize the scanner.""" - - host = config[CONF_HOST] - password = config[CONF_PASSWORD] - username = config[CONF_USERNAME] - - self.success_init = False - try: - self.tplink_client = TpLinkClient(password, host=host, username=username) - - self.last_results = {} - - self.success_init = self._update_info() - except requests.exceptions.RequestException: - _LOGGER.debug("RequestException in %s", self.__class__.__name__) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results.keys() - - def get_device_name(self, device): - """Get the name of the device.""" - return self.last_results.get(device) - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - result = self.tplink_client.get_connected_devices() - - if result: - self.last_results = result - return True - - return False - - -class Tplink1DeviceScanner(DeviceScanner): - """This class queries a wireless router running TP-Link firmware.""" - - def __init__(self, config): - """Initialize the scanner.""" - host = config[CONF_HOST] - username, password = config[CONF_USERNAME], config[CONF_PASSWORD] - - self.parse_macs = re.compile( - "[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}-" - + "[0-9A-F]{2}-[0-9A-F]{2}-[0-9A-F]{2}" - ) - - self.host = host - self.username = username - self.password = password - - self.last_results = {} - self.success_init = False - try: - self.success_init = self._update_info() - except requests.exceptions.RequestException: - _LOGGER.debug("RequestException in %s", self.__class__.__name__) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results - - def get_device_name(self, device): - """Get firmware doesn't save the name of the wireless device.""" - return None - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - - url = f"http://{self.host}/userRpm/WlanStationRpm.htm" - referer = f"http://{self.host}" - page = requests.get( - url, - auth=(self.username, self.password), - headers={REFERER: referer}, - timeout=4, - ) - - result = self.parse_macs.findall(page.text) - - if result: - self.last_results = [mac.replace("-", ":") for mac in result] - return True - - return False - - -class Tplink2DeviceScanner(Tplink1DeviceScanner): - """This class queries a router with newer version of TP-Link firmware.""" - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results.keys() - - def get_device_name(self, device): - """Get firmware doesn't save the name of the wireless device.""" - return self.last_results.get(device) - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - - url = f"http://{self.host}/data/map_access_wireless_client_grid.json" - referer = f"http://{self.host}" - - # Router uses Authorization cookie instead of header - # Let's create the cookie - username_password = f"{self.username}:{self.password}" - b64_encoded_username_password = base64.b64encode( - username_password.encode("ascii") - ).decode("ascii") - cookie = f"Authorization=Basic {b64_encoded_username_password}" - - response = requests.post( - url, headers={REFERER: referer, COOKIE: cookie}, timeout=4 - ) - - try: - result = response.json().get("data") - except ValueError: - _LOGGER.error( - "Router didn't respond with JSON. " "Check if credentials are correct." - ) - return False - - if result: - self.last_results = { - device["mac_addr"].replace("-", ":"): device["name"] - for device in result - } - return True - - return False - - -class Tplink3DeviceScanner(Tplink1DeviceScanner): - """This class queries the Archer C9 router with version 150811 or high.""" - - def __init__(self, config): - """Initialize the scanner.""" - self.stok = "" - self.sysauth = "" - super().__init__(config) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - self._log_out() - return self.last_results.keys() - - def get_device_name(self, device): - """Get the firmware doesn't save the name of the wireless device. - - We are forced to use the MAC address as name here. - """ - return self.last_results.get(device) - - def _get_auth_tokens(self): - """Retrieve auth tokens from the router.""" - _LOGGER.info("Retrieving auth tokens...") - - url = f"http://{self.host}/cgi-bin/luci/;stok=/login?form=login" - referer = f"http://{self.host}/webpages/login.html" - - # If possible implement RSA encryption of password here. - response = requests.post( - url, - params={ - "operation": "login", - "username": self.username, - "password": self.password, - }, - headers={REFERER: referer}, - timeout=4, - ) - - try: - self.stok = response.json().get("data").get("stok") - _LOGGER.info(self.stok) - regex_result = re.search("sysauth=(.*);", response.headers["set-cookie"]) - self.sysauth = regex_result.group(1) - _LOGGER.info(self.sysauth) - return True - except (ValueError, KeyError): - _LOGGER.error("Couldn't fetch auth tokens! Response was: %s", response.text) - return False - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - if (self.stok == "") or (self.sysauth == ""): - self._get_auth_tokens() - - _LOGGER.info("Loading wireless clients...") - - url = ( - "http://{}/cgi-bin/luci/;stok={}/admin/wireless?" "form=statistics" - ).format(self.host, self.stok) - referer = f"http://{self.host}/webpages/index.html" - - response = requests.post( - url, - params={"operation": "load"}, - headers={REFERER: referer}, - cookies={"sysauth": self.sysauth}, - timeout=5, - ) - - try: - json_response = response.json() - - if json_response.get("success"): - result = response.json().get("data") - else: - if json_response.get("errorcode") == "timeout": - _LOGGER.info("Token timed out. Relogging on next scan") - self.stok = "" - self.sysauth = "" - return False - _LOGGER.error("An unknown error happened while fetching data") - return False - except ValueError: - _LOGGER.error( - "Router didn't respond with JSON. " "Check if credentials are correct" - ) - return False - - if result: - self.last_results = { - device["mac"].replace("-", ":"): device["mac"] for device in result - } - return True - - return False - - def _log_out(self): - _LOGGER.info("Logging out of router admin interface...") - - url = ("http://{}/cgi-bin/luci/;stok={}/admin/system?" "form=logout").format( - self.host, self.stok - ) - referer = f"http://{self.host}/webpages/index.html" - - requests.post( - url, - params={"operation": "write"}, - headers={REFERER: referer}, - cookies={"sysauth": self.sysauth}, - ) - self.stok = "" - self.sysauth = "" - - -class Tplink4DeviceScanner(Tplink1DeviceScanner): - """This class queries an Archer C7 router with TP-Link firmware 150427.""" - - def __init__(self, config): - """Initialize the scanner.""" - self.credentials = "" - self.token = "" - super().__init__(config) - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return self.last_results - - def get_device_name(self, device): - """Get the name of the wireless device.""" - return None - - def _get_auth_tokens(self): - """Retrieve auth tokens from the router.""" - _LOGGER.info("Retrieving auth tokens...") - url = f"http://{self.host}/userRpm/LoginRpm.htm?Save=Save" - - # Generate md5 hash of password. The C7 appears to use the first 15 - # characters of the password only, so we truncate to remove additional - # characters from being hashed. - password = hashlib.md5(self.password.encode("utf")[:15]).hexdigest() - credentials = f"{self.username}:{password}".encode("utf") - - # Encode the credentials to be sent as a cookie. - self.credentials = base64.b64encode(credentials).decode("utf") - - # Create the authorization cookie. - cookie = f"Authorization=Basic {self.credentials}" - - response = requests.get(url, headers={COOKIE: cookie}) - - try: - result = re.search( - r"window.parent.location.href = " - r'"https?:\/\/.*\/(.*)\/userRpm\/Index.htm";', - response.text, - ) - if not result: - return False - self.token = result.group(1) - return True - except ValueError: - _LOGGER.error("Couldn't fetch auth tokens") - return False - - def _update_info(self): - """Ensure the information from the TP-Link router is up to date. - - Return boolean if scanning successful. - """ - if (self.credentials == "") or (self.token == ""): - self._get_auth_tokens() - - _LOGGER.info("Loading wireless clients...") - - mac_results = [] - - # Check both the 2.4GHz and 5GHz client list URLs - for clients_url in ("WlanStationRpm.htm", "WlanStationRpm_5g.htm"): - url = f"http://{self.host}/{self.token}/userRpm/{clients_url}" - referer = f"http://{self.host}" - cookie = f"Authorization=Basic {self.credentials}" - - page = requests.get(url, headers={COOKIE: cookie, REFERER: referer}) - mac_results.extend(self.parse_macs.findall(page.text)) - - if not mac_results: - return False - - self.last_results = [mac.replace("-", ":") for mac in mac_results] - return True - - -class Tplink5DeviceScanner(Tplink1DeviceScanner): - """This class queries a TP-Link EAP-225 AP with newer TP-Link FW.""" - - def scan_devices(self): - """Scan for new devices and return a list with found MAC IDs.""" - self._update_info() - return self.last_results.keys() - - def get_device_name(self, device): - """Get firmware doesn't save the name of the wireless device.""" - return None - - def _update_info(self): - """Ensure the information from the TP-Link AP is up to date. - - Return boolean if scanning successful. - """ - _LOGGER.info("Loading wireless clients...") - - base_url = f"http://{self.host}" - - header = { - USER_AGENT: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12;" - " rv:53.0) Gecko/20100101 Firefox/53.0", - ACCEPT: "application/json, text/javascript, */*; q=0.01", - ACCEPT_LANGUAGE: "Accept-Language: en-US,en;q=0.5", - ACCEPT_ENCODING: "gzip, deflate", - CONTENT_TYPE: "application/x-www-form-urlencoded; charset=UTF-8", - HTTP_HEADER_X_REQUESTED_WITH: "XMLHttpRequest", - REFERER: f"http://{self.host}/", - CONNECTION: KEEP_ALIVE, - PRAGMA: HTTP_HEADER_NO_CACHE, - CACHE_CONTROL: HTTP_HEADER_NO_CACHE, - } - - password_md5 = hashlib.md5(self.password.encode("utf")).hexdigest().upper() - - # Create a session to handle cookie easier - session = requests.session() - session.get(base_url, headers=header) - - login_data = {"username": self.username, "password": password_md5} - session.post(base_url, login_data, headers=header) - - # A timestamp is required to be sent as get parameter - timestamp = int(datetime.now().timestamp() * 1e3) - - client_list_url = f"{base_url}/data/monitor.client.client.json" - - get_params = {"operation": "load", "_": timestamp} - - response = session.get(client_list_url, headers=header, params=get_params) - session.close() - try: - list_of_devices = response.json() - except ValueError: - _LOGGER.error( - "AP didn't respond with JSON. " "Check if credentials are correct" - ) - return False - - if list_of_devices: - self.last_results = { - device["MAC"].replace("-", ":"): device["DeviceName"] - for device in list_of_devices["data"] - } - return True - - return False diff --git a/homeassistant/components/tplink/manifest.json b/homeassistant/components/tplink/manifest.json index f299e02e2d3..c2a2197c844 100644 --- a/homeassistant/components/tplink/manifest.json +++ b/homeassistant/components/tplink/manifest.json @@ -4,8 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tplink", "requirements": [ - "pyHS100==0.3.5", - "tplink==0.2.1" + "pyHS100==0.3.5" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index ad17b59b455..c79e36413a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1899,9 +1899,6 @@ total_connect_client==0.28 # homeassistant.components.tplink_lte tp-connected==0.0.4 -# homeassistant.components.tplink -tplink==0.2.1 - # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9b59a1d806b..7912de963ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -586,9 +586,6 @@ tellduslive==0.10.10 # homeassistant.components.toon toonapilib==3.2.4 -# homeassistant.components.tplink -tplink==0.2.1 - # homeassistant.components.transmission transmissionrpc==0.11 diff --git a/tests/components/tplink/test_device_tracker.py b/tests/components/tplink/test_device_tracker.py deleted file mode 100644 index bbe73dc121a..00000000000 --- a/tests/components/tplink/test_device_tracker.py +++ /dev/null @@ -1,71 +0,0 @@ -"""The tests for the tplink device tracker platform.""" - -import os -import pytest - -from homeassistant.components.device_tracker.legacy import YAML_DEVICES -from homeassistant.components.tplink.device_tracker import Tplink4DeviceScanner -from homeassistant.const import CONF_PLATFORM, CONF_PASSWORD, CONF_USERNAME, CONF_HOST -import requests_mock - - -@pytest.fixture(autouse=True) -def setup_comp(hass): - """Initialize components.""" - yaml_devices = hass.config.path(YAML_DEVICES) - yield - if os.path.isfile(yaml_devices): - os.remove(yaml_devices) - - -async def test_get_mac_addresses_from_both_bands(hass): - """Test grabbing the mac addresses from 2.4 and 5 GHz clients pages.""" - with requests_mock.Mocker() as m: - conf_dict = { - CONF_PLATFORM: "tplink", - CONF_HOST: "fake-host", - CONF_USERNAME: "fake_user", - CONF_PASSWORD: "fake_pass", - } - - # Mock the token retrieval process - FAKE_TOKEN = "fake_token" - fake_auth_token_response = ( - "window.parent.location.href = " - '"https://a/{}/userRpm/Index.htm";'.format(FAKE_TOKEN) - ) - - m.get( - "http://{}/userRpm/LoginRpm.htm?Save=Save".format(conf_dict[CONF_HOST]), - text=fake_auth_token_response, - ) - - FAKE_MAC_1 = "CA-FC-8A-C8-BB-53" - FAKE_MAC_2 = "6C-48-83-21-46-8D" - FAKE_MAC_3 = "77-98-75-65-B1-2B" - mac_response_2_4 = "{} {}".format(FAKE_MAC_1, FAKE_MAC_2) - mac_response_5 = "{}".format(FAKE_MAC_3) - - # Mock the 2.4 GHz clients page - m.get( - "http://{}/{}/userRpm/WlanStationRpm.htm".format( - conf_dict[CONF_HOST], FAKE_TOKEN - ), - text=mac_response_2_4, - ) - - # Mock the 5 GHz clients page - m.get( - "http://{}/{}/userRpm/WlanStationRpm_5g.htm".format( - conf_dict[CONF_HOST], FAKE_TOKEN - ), - text=mac_response_5, - ) - - tplink = Tplink4DeviceScanner(conf_dict) - - expected_mac_results = [ - mac.replace("-", ":") for mac in [FAKE_MAC_1, FAKE_MAC_2, FAKE_MAC_3] - ] - - assert tplink.last_results == expected_mac_results From 22b29a800501bc3959c1035b7664acff82908a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=BDdrale?= Date: Sun, 20 Oct 2019 18:43:44 +0200 Subject: [PATCH 470/639] Add option to disable HTTPS verification in Luci component (#27946) * Add option to disable HTTPS verification in Luci component * Update code owners * Update code owners --- CODEOWNERS | 2 +- homeassistant/components/luci/device_tracker.py | 11 ++++++++++- homeassistant/components/luci/manifest.json | 7 +++++-- requirements_all.txt | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 2f228105cbb..30946fb14f2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -171,7 +171,7 @@ homeassistant/components/liveboxplaytv/* @pschmitt homeassistant/components/logger/* @home-assistant/core homeassistant/components/logi_circle/* @evanjd homeassistant/components/lovelace/* @home-assistant/frontend -homeassistant/components/luci/* @fbradyirl +homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luftdaten/* @fabaff homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf diff --git a/homeassistant/components/luci/device_tracker.py b/homeassistant/components/luci/device_tracker.py index 87a32767cc2..59c3251a437 100644 --- a/homeassistant/components/luci/device_tracker.py +++ b/homeassistant/components/luci/device_tracker.py @@ -8,12 +8,19 @@ from homeassistant.components.device_tracker import ( PLATFORM_SCHEMA, DeviceScanner, ) -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_SSL, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) DEFAULT_SSL = False +DEFAULT_VERIFY_SSL = True PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -21,6 +28,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, } ) @@ -44,6 +52,7 @@ class LuciDeviceScanner(DeviceScanner): config[CONF_USERNAME], config[CONF_PASSWORD], config[CONF_SSL], + config[CONF_VERIFY_SSL], ) self.last_results = {} diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 646fc1a3cbf..d7cf72ebaf5 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -3,8 +3,11 @@ "name": "Luci", "documentation": "https://www.home-assistant.io/integrations/luci", "requirements": [ - "openwrt-luci-rpc==1.1.1" + "openwrt-luci-rpc==1.1.2" ], "dependencies": [], - "codeowners": ["@fbradyirl"] + "codeowners": [ + "@fbradyirl", + "@mzdrale" + ] } diff --git a/requirements_all.txt b/requirements_all.txt index c79e36413a9..0b86ae2d7da 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -917,7 +917,7 @@ opensensemap-api==0.1.5 openwebifpy==3.1.1 # homeassistant.components.luci -openwrt-luci-rpc==1.1.1 +openwrt-luci-rpc==1.1.2 # homeassistant.components.oru oru==0.1.9 From 425e7fd1a7aa52995b056408324ce574d19765b5 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 20 Oct 2019 17:51:08 +0100 Subject: [PATCH 471/639] bugfix evohome and bump client (#27968) * bump client to 0.3.4b1 * handle bad schedules that cause issue #27768 --- homeassistant/components/evohome/__init__.py | 59 +++++++++++-------- .../components/evohome/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 35 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index a52780c8a0f..f86bf974205 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -460,37 +460,44 @@ class EvoChild(EvoDevice): day_of_week = int(day_time.strftime("%w")) # 0 is Sunday time_of_day = day_time.strftime("%H:%M:%S") - # Iterate today's switchpoints until past the current time of day... - day = self._schedule["DailySchedules"][day_of_week] - sp_idx = -1 # last switchpoint of the day before - for i, tmp in enumerate(day["Switchpoints"]): - if time_of_day > tmp["TimeOfDay"]: - sp_idx = i # current setpoint - else: - break + try: + # Iterate today's switchpoints until past the current time of day... + day = self._schedule["DailySchedules"][day_of_week] + sp_idx = -1 # last switchpoint of the day before + for i, tmp in enumerate(day["Switchpoints"]): + if time_of_day > tmp["TimeOfDay"]: + sp_idx = i # current setpoint + else: + break - # Did the current SP start yesterday? Does the next start SP tomorrow? - this_sp_day = -1 if sp_idx == -1 else 0 - next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 + # Did the current SP start yesterday? Does the next start SP tomorrow? + this_sp_day = -1 if sp_idx == -1 else 0 + next_sp_day = 1 if sp_idx + 1 == len(day["Switchpoints"]) else 0 - for key, offset, idx in [ - ("this", this_sp_day, sp_idx), - ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), - ]: - sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") - day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] - switchpoint = day["Switchpoints"][idx] + for key, offset, idx in [ + ("this", this_sp_day, sp_idx), + ("next", next_sp_day, (sp_idx + 1) * (1 - next_sp_day)), + ]: + sp_date = (day_time + timedelta(days=offset)).strftime("%Y-%m-%d") + day = self._schedule["DailySchedules"][(day_of_week + offset) % 7] + switchpoint = day["Switchpoints"][idx] - dt_local_aware = _local_dt_to_aware( - dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}") + dt_local_aware = _local_dt_to_aware( + dt_util.parse_datetime(f"{sp_date}T{switchpoint['TimeOfDay']}") + ) + + self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat() + try: + self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"] + except KeyError: + self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] + + except IndexError: + self._setpoints = {} + _LOGGER.warning( + "Failed to get setpoints - please report as an issue", exc_info=True ) - self._setpoints[f"{key}_sp_from"] = dt_local_aware.isoformat() - try: - self._setpoints[f"{key}_sp_temp"] = switchpoint["heatSetpoint"] - except KeyError: - self._setpoints[f"{key}_sp_state"] = switchpoint["DhwState"] - return self._setpoints async def _update_schedule(self) -> None: diff --git a/homeassistant/components/evohome/manifest.json b/homeassistant/components/evohome/manifest.json index da942db7920..0b112df42bb 100644 --- a/homeassistant/components/evohome/manifest.json +++ b/homeassistant/components/evohome/manifest.json @@ -3,7 +3,7 @@ "name": "Evohome", "documentation": "https://www.home-assistant.io/integrations/evohome", "requirements": [ - "evohome-async==0.3.3b5" + "evohome-async==0.3.4b1" ], "dependencies": [], "codeowners": ["@zxdavb"] diff --git a/requirements_all.txt b/requirements_all.txt index 0b86ae2d7da..3c649e91756 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -480,7 +480,7 @@ eternalegypt==0.0.10 # evdev==0.6.1 # homeassistant.components.evohome -evohome-async==0.3.3b5 +evohome-async==0.3.4b1 # homeassistant.components.dlib_face_detect # homeassistant.components.dlib_face_identify From c44163548dd6a137597d699e15eb60d667cf7ea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 15:40:13 -0300 Subject: [PATCH 472/639] Move imports in dte_energy_bridge component (#27975) --- homeassistant/components/dte_energy_bridge/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dte_energy_bridge/sensor.py b/homeassistant/components/dte_energy_bridge/sensor.py index b904d004c61..aa822da0d6a 100644 --- a/homeassistant/components/dte_energy_bridge/sensor.py +++ b/homeassistant/components/dte_energy_bridge/sensor.py @@ -1,12 +1,13 @@ """Support for monitoring energy usage using the DTE energy bridge.""" import logging +import requests import voluptuous as vol -from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -78,8 +79,6 @@ class DteEnergyBridgeSensor(Entity): def update(self): """Get the energy usage data from the DTE energy bridge.""" - import requests - try: response = requests.get(self._url, timeout=5) except (requests.exceptions.RequestException, ValueError): From 57b6c2c6b086b73f168a037740bd58eb065b12a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 15:41:11 -0300 Subject: [PATCH 473/639] Move imports in crimereports component (#27973) --- homeassistant/components/crimereports/sensor.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/crimereports/sensor.py b/homeassistant/components/crimereports/sensor.py index 6295125b7ca..cf5b2e374e2 100644 --- a/homeassistant/components/crimereports/sensor.py +++ b/homeassistant/components/crimereports/sensor.py @@ -3,27 +3,28 @@ from collections import defaultdict from datetime import timedelta import logging +import crimereports import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_INCLUDE, - CONF_EXCLUDE, - CONF_NAME, - CONF_LATITUDE, - CONF_LONGITUDE, ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, + CONF_EXCLUDE, + CONF_INCLUDE, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, CONF_RADIUS, LENGTH_KILOMETERS, LENGTH_METERS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import slugify from homeassistant.util.distance import convert from homeassistant.util.dt import now -import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -65,8 +66,6 @@ class CrimeReportsSensor(Entity): def __init__(self, hass, name, latitude, longitude, radius, include, exclude): """Initialize the Crime Reports sensor.""" - import crimereports - self._hass = hass self._name = name self._include = include @@ -113,8 +112,6 @@ class CrimeReportsSensor(Entity): def update(self): """Update device state.""" - import crimereports - incident_counts = defaultdict(int) incidents = self._crimereports.get_incidents( now().date(), include=self._include, exclude=self._exclude From 6951d788745ad1750685a48855bac0cf18bd1a1d Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 20 Oct 2019 19:46:24 +0100 Subject: [PATCH 474/639] move imports in serial component (#27971) --- homeassistant/components/serial/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/serial/sensor.py b/homeassistant/components/serial/sensor.py index 27775b8c702..a08f9522c4b 100644 --- a/homeassistant/components/serial/sensor.py +++ b/homeassistant/components/serial/sensor.py @@ -1,12 +1,13 @@ """Support for reading data from a serial port.""" -import logging import json +import logging +import serial_asyncio 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_VALUE_TEMPLATE, EVENT_HOMEASSISTANT_STOP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ class SerialSensor(Entity): async def serial_read(self, device, rate, **kwargs): """Read the data from the port.""" - import serial_asyncio - reader, _ = await serial_asyncio.open_serial_connection( url=device, baudrate=rate, **kwargs ) From 9db07b2a4162ae69c4f1da1977b58e49793a185a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 15:46:51 -0300 Subject: [PATCH 475/639] Move imports in onvif component (#27969) --- homeassistant/components/onvif/camera.py | 42 ++++++++++-------------- 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 29af1049fae..c73886c13c0 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -8,21 +8,29 @@ import asyncio import datetime as dt import logging import os -import voluptuous as vol +from aiohttp.client_exceptions import ClientConnectionError, ServerDisconnectedError +from haffmpeg.camera import CameraMjpeg +from haffmpeg.tools import IMAGE_JPEG, ImageFrame +import onvif +from onvif import ONVIFCamera, exceptions +import voluptuous as vol +from zeep.exceptions import Fault + +from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.components.camera.const import DOMAIN +from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, DATA_FFMPEG from homeassistant.const import ( - CONF_NAME, + ATTR_ENTITY_ID, CONF_HOST, - CONF_USERNAME, + CONF_NAME, CONF_PASSWORD, CONF_PORT, - ATTR_ENTITY_ID, + CONF_USERNAME, ) -from homeassistant.components.camera import Camera, PLATFORM_SCHEMA, SUPPORT_STREAM -from homeassistant.components.camera.const import DOMAIN -from homeassistant.components.ffmpeg import DATA_FFMPEG, CONF_EXTRA_ARGUMENTS -import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.service import async_extract_entity_ids import homeassistant.util.dt as dt_util @@ -122,9 +130,6 @@ class ONVIFHassCamera(Camera): _LOGGER.debug("Importing dependencies") - import onvif - from onvif import ONVIFCamera - _LOGGER.debug("Setting up the ONVIF camera component") self._username = config.get(CONF_USERNAME) @@ -156,10 +161,6 @@ class ONVIFHassCamera(Camera): Initializes the camera by obtaining the input uri and connecting to the camera. Also retrieves the ONVIF profiles. """ - from aiohttp.client_exceptions import ClientConnectionError - from homeassistant.exceptions import PlatformNotReady - from zeep.exceptions import Fault - try: _LOGGER.debug("Updating service addresses") await self._camera.update_xaddrs() @@ -169,7 +170,7 @@ class ONVIFHassCamera(Camera): self.setup_ptz() except ClientConnectionError as err: _LOGGER.warning( - "Couldn't connect to camera '%s', but will " "retry later. Error: %s", + "Couldn't connect to camera '%s', but will retry later. Error: %s", self._name, err, ) @@ -184,8 +185,6 @@ class ONVIFHassCamera(Camera): async def async_check_date_and_time(self): """Warns if camera and system date not synced.""" - from aiohttp.client_exceptions import ServerDisconnectedError - _LOGGER.debug("Setting up the ONVIF device management service") devicemgmt = self._camera.create_devicemgmt_service() @@ -228,8 +227,6 @@ class ONVIFHassCamera(Camera): async def async_obtain_input_uri(self): """Set the input uri for the camera.""" - from onvif import exceptions - _LOGGER.debug( "Connecting with ONVIF Camera: %s on port %s", self._host, self._port ) @@ -289,8 +286,6 @@ class ONVIFHassCamera(Camera): async def async_perform_ptz(self, pan, tilt, zoom): """Perform a PTZ action on the camera.""" - from onvif import exceptions - if self._ptz_service is None: _LOGGER.warning("PTZ actions are not supported on camera '%s'", self._name) return @@ -332,7 +327,6 @@ class ONVIFHassCamera(Camera): async def async_camera_image(self): """Return a still image response from the camera.""" - from haffmpeg.tools import ImageFrame, IMAGE_JPEG _LOGGER.debug("Retrieving image from camera '%s'", self._name) @@ -347,8 +341,6 @@ class ONVIFHassCamera(Camera): async def handle_async_mjpeg_stream(self, request): """Generate an HTTP MJPEG stream from the camera.""" - from haffmpeg.camera import CameraMjpeg - _LOGGER.debug("Handling mjpeg stream from camera '%s'", self._name) ffmpeg_manager = self.hass.data[DATA_FFMPEG] From 54a711ca6af2ee6fb977441db2a25bea1198e65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 15:47:40 -0300 Subject: [PATCH 476/639] Move imports in dweet component (#27976) --- homeassistant/components/dweet/__init__.py | 7 +++---- homeassistant/components/dweet/sensor.py | 15 ++++++--------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/dweet/__init__.py b/homeassistant/components/dweet/__init__.py index bf1298479c3..db985e57a41 100644 --- a/homeassistant/components/dweet/__init__.py +++ b/homeassistant/components/dweet/__init__.py @@ -1,7 +1,8 @@ """Support for sending data to Dweet.io.""" -import logging from datetime import timedelta +import logging +import dweepy import voluptuous as vol from homeassistant.const import ( @@ -10,8 +11,8 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, STATE_UNKNOWN, ) -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -69,8 +70,6 @@ def setup(hass, config): @Throttle(MIN_TIME_BETWEEN_UPDATES) def send_data(name, msg): """Send the collected data to Dweet.io.""" - import dweepy - try: dweepy.dweet_for(name, msg) except dweepy.DweepyError: diff --git a/homeassistant/components/dweet/sensor.py b/homeassistant/components/dweet/sensor.py index 937de9b030a..f3f604ff369 100644 --- a/homeassistant/components/dweet/sensor.py +++ b/homeassistant/components/dweet/sensor.py @@ -1,18 +1,19 @@ """Support for showing values from Dweet.io.""" +from datetime import timedelta import json import logging -from datetime import timedelta +import dweepy 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_VALUE_TEMPLATE, - CONF_UNIT_OF_MEASUREMENT, CONF_DEVICE, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + CONF_VALUE_TEMPLATE, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -33,8 +34,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Dweet sensor.""" - import dweepy - name = config.get(CONF_NAME) device = config.get(CONF_DEVICE) value_template = config.get(CONF_VALUE_TEMPLATE) @@ -107,8 +106,6 @@ class DweetData: def update(self): """Get the latest data from Dweet.io.""" - import dweepy - try: self.data = dweepy.get_latest_dweet_for(self._device) except dweepy.DweepyError: From 8356d92f04b9c53943f7c7b4f04d01e11dd20ec9 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 20 Oct 2019 19:48:52 +0100 Subject: [PATCH 477/639] Refactor entity_ids, tweak names and consolidate classes (#27921) * refactor entity_ids, and consolidate classes * isort the code --- .../components/incomfort/__init__.py | 41 +++++++++++++++- .../components/incomfort/binary_sensor.py | 35 +++----------- homeassistant/components/incomfort/climate.py | 36 +++----------- homeassistant/components/incomfort/sensor.py | 47 +++++-------------- .../components/incomfort/water_heater.py | 23 ++++----- 5 files changed, 76 insertions(+), 106 deletions(-) diff --git a/homeassistant/components/incomfort/__init__.py b/homeassistant/components/incomfort/__init__.py index d6f72209f06..adf57e35093 100644 --- a/homeassistant/components/incomfort/__init__.py +++ b/homeassistant/components/incomfort/__init__.py @@ -1,14 +1,18 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" import logging +from typing import Optional from aiohttp import ClientResponseError -import voluptuous as vol from incomfortclient import Gateway as InComfortGateway +import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -53,3 +57,38 @@ async def async_setup(hass, hass_config): ) return True + + +class IncomfortEntity(Entity): + """Base class for all InComfort entities.""" + + def __init__(self) -> None: + """Initialize the class.""" + self._unique_id = self._name = None + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> Optional[str]: + """Return the name of the sensor.""" + return self._name + + +class IncomfortChild(IncomfortEntity): + """Base class for all InComfort entities (excluding the boiler).""" + + async def async_added_to_hass(self) -> None: + """Set up a listener when this entity is added to HA.""" + async_dispatcher_connect(self.hass, DOMAIN, self._refresh) + + @callback + def _refresh(self) -> None: + self.async_schedule_update_ha_state(force_refresh=True) + + @property + def should_poll(self) -> bool: + """Return False as this device should never be polled.""" + return False diff --git a/homeassistant/components/incomfort/binary_sensor.py b/homeassistant/components/incomfort/binary_sensor.py index 39a45429cb1..b5dbd8e223d 100644 --- a/homeassistant/components/incomfort/binary_sensor.py +++ b/homeassistant/components/incomfort/binary_sensor.py @@ -1,11 +1,9 @@ """Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" from typing import Any, Dict, Optional -from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice -from . import DOMAIN +from . import DOMAIN, IncomfortChild async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -18,34 +16,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class IncomfortFailed(BinarySensorDevice): +class IncomfortFailed(IncomfortChild, BinarySensorDevice): """Representation of an InComfort Failed sensor.""" def __init__(self, client, heater) -> None: """Initialize the binary sensor.""" + super().__init__() + self._unique_id = f"{heater.serial_no}_failed" + self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_failed") + self._name = "Boiler Fault" self._client = client self._heater = heater - async def async_added_to_hass(self) -> None: - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> Optional[str]: - """Return the name of the sensor.""" - return "Fault state" - @property def is_on(self) -> bool: """Return the status of the sensor.""" @@ -55,8 +39,3 @@ class IncomfortFailed(BinarySensorDevice): def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" return {"fault_code": self._heater.status["fault_code"]} - - @property - def should_poll(self) -> bool: - """Return False as this device should never be polled.""" - return False diff --git a/homeassistant/components/incomfort/climate.py b/homeassistant/components/incomfort/climate.py index 3918244d4e8..95ccf186372 100644 --- a/homeassistant/components/incomfort/climate.py +++ b/homeassistant/components/incomfort/climate.py @@ -1,16 +1,14 @@ """Support for an Intergas boiler via an InComfort/InTouch Lan2RF gateway.""" from typing import Any, Dict, List, Optional -from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate import ENTITY_ID_FORMAT, ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import DOMAIN +from . import DOMAIN, IncomfortChild async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -24,39 +22,19 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([InComfortClimate(client, heater, r) for r in heater.rooms]) -class InComfortClimate(ClimateDevice): +class InComfortClimate(IncomfortChild, ClimateDevice): """Representation of an InComfort/InTouch climate device.""" def __init__(self, client, heater, room) -> None: """Initialize the climate device.""" + super().__init__() + self._unique_id = f"{heater.serial_no}_{room.room_no}" + self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_{room.room_no}") + self._name = f"Thermostat {room.room_no}" self._client = client self._room = room - self._name = f"Room {room.room_no}" - - async def async_added_to_hass(self) -> None: - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def should_poll(self) -> bool: - """Return False as this device should never be polled.""" - return False - - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the climate device.""" - return self._name @property def device_state_attributes(self) -> Dict[str, Any]: diff --git a/homeassistant/components/incomfort/sensor.py b/homeassistant/components/incomfort/sensor.py index 772b5dab183..f3170b7b9bb 100644 --- a/homeassistant/components/incomfort/sensor.py +++ b/homeassistant/components/incomfort/sensor.py @@ -1,18 +1,16 @@ """Support for an Intergas heater via an InComfort/InTouch Lan2RF gateway.""" from typing import Any, Dict, Optional +from homeassistant.components.sensor import ENTITY_ID_FORMAT from homeassistant.const import ( - PRESSURE_BAR, - TEMP_CELSIUS, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + PRESSURE_BAR, + TEMP_CELSIUS, ) -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity from homeassistant.util import slugify -from . import DOMAIN +from . import DOMAIN, IncomfortChild INCOMFORT_HEATER_TEMP = "CV Temp" INCOMFORT_PRESSURE = "CV Pressure" @@ -42,42 +40,28 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= ) -class IncomfortSensor(Entity): +class IncomfortSensor(IncomfortChild): """Representation of an InComfort/InTouch sensor device.""" def __init__(self, client, heater, name) -> None: """Initialize the sensor.""" + super().__init__() + self._client = client self._heater = heater self._unique_id = f"{heater.serial_no}_{slugify(name)}" + self.entity_id = ENTITY_ID_FORMAT.format(f"{DOMAIN}_{slugify(name)}") + self._name = f"Boiler {name}" - self._name = name self._device_class = None + self._state_attr = INCOMFORT_MAP_ATTRS[name][0] self._unit_of_measurement = None - async def async_added_to_hass(self) -> None: - """Set up a listener when this entity is added to HA.""" - async_dispatcher_connect(self.hass, DOMAIN, self._refresh) - - @callback - def _refresh(self) -> None: - self.async_schedule_update_ha_state(force_refresh=True) - - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> Optional[str]: - """Return the name of the sensor.""" - return self._name - @property def state(self) -> Optional[str]: """Return the state of the sensor.""" - return self._heater.status[INCOMFORT_MAP_ATTRS[self._name][0]] + return self._heater.status[self._state_attr] @property def device_class(self) -> Optional[str]: @@ -89,11 +73,6 @@ class IncomfortSensor(Entity): """Return the unit of measurement of the sensor.""" return self._unit_of_measurement - @property - def should_poll(self) -> bool: - """Return False as this device should never be polled.""" - return False - class IncomfortPressure(IncomfortSensor): """Representation of an InTouch CV Pressure sensor.""" @@ -113,11 +92,11 @@ class IncomfortTemperature(IncomfortSensor): """Initialize the signal strength sensor.""" super().__init__(client, heater, name) + self._attr = INCOMFORT_MAP_ATTRS[name][1] self._device_class = DEVICE_CLASS_TEMPERATURE self._unit_of_measurement = TEMP_CELSIUS @property def device_state_attributes(self) -> Optional[Dict[str, Any]]: """Return the device state attributes.""" - key = INCOMFORT_MAP_ATTRS[self._name][1] - return {key: self._heater.status[key]} + return {self._attr: self._heater.status[self._attr]} diff --git a/homeassistant/components/incomfort/water_heater.py b/homeassistant/components/incomfort/water_heater.py index 70423611705..0015107b40f 100644 --- a/homeassistant/components/incomfort/water_heater.py +++ b/homeassistant/components/incomfort/water_heater.py @@ -1,14 +1,15 @@ """Support for an Intergas boiler via an InComfort/Intouch Lan2RF gateway.""" import asyncio import logging -from typing import Any, Dict, Optional +from typing import Any, Dict from aiohttp import ClientResponseError -from homeassistant.components.water_heater import WaterHeaterDevice + +from homeassistant.components.water_heater import ENTITY_ID_FORMAT, WaterHeaterDevice from homeassistant.const import TEMP_CELSIUS from homeassistant.helpers.dispatcher import async_dispatcher_send -from . import DOMAIN +from . import DOMAIN, IncomfortEntity _LOGGER = logging.getLogger(__name__) @@ -26,26 +27,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities([IncomfortWaterHeater(client, heater)]) -class IncomfortWaterHeater(WaterHeaterDevice): +class IncomfortWaterHeater(IncomfortEntity, WaterHeaterDevice): """Representation of an InComfort/Intouch water_heater device.""" def __init__(self, client, heater) -> None: """Initialize the water_heater device.""" + super().__init__() + self._unique_id = f"{heater.serial_no}" + self.entity_id = ENTITY_ID_FORMAT.format(DOMAIN) + self._name = "Boiler" self._client = client self._heater = heater - @property - def unique_id(self) -> Optional[str]: - """Return a unique ID.""" - return self._unique_id - - @property - def name(self) -> str: - """Return the name of the water_heater device.""" - return "Boiler" - @property def icon(self) -> str: """Return the icon of the water_heater device.""" From ca0a4a8750b1b1a8a6e050d4dc6e2692cea3e5a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 16:16:04 -0300 Subject: [PATCH 478/639] Move imports for ebusd component (#27979) --- homeassistant/components/ebusd/__init__.py | 10 +++------- homeassistant/components/ebusd/const.py | 2 +- homeassistant/components/ebusd/sensor.py | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py index e11de446e40..e4d0bdbcdb1 100644 --- a/homeassistant/components/ebusd/__init__.py +++ b/homeassistant/components/ebusd/__init__.py @@ -3,13 +3,14 @@ from datetime import timedelta import logging import socket +import ebusdpy import voluptuous as vol from homeassistant.const import ( - CONF_NAME, CONF_HOST, - CONF_PORT, CONF_MONITORED_CONDITIONS, + CONF_NAME, + CONF_PORT, ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -66,7 +67,6 @@ def setup(hass, config): try: _LOGGER.debug("Ebusd integration setup started") - import ebusdpy ebusdpy.init(server_address) hass.data[DOMAIN] = EbusdData(server_address, circuit) @@ -98,8 +98,6 @@ class EbusdData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, name, stype): """Call the Ebusd API to update the data.""" - import ebusdpy - try: _LOGGER.debug("Opening socket to ebusd %s", name) command_result = ebusdpy.read( @@ -116,8 +114,6 @@ class EbusdData: def write(self, call): """Call write methon on ebusd.""" - import ebusdpy - name = call.data.get("name") value = call.data.get("value") diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index db79d81736e..ec097a153c9 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -1,5 +1,5 @@ """Constants for ebus component.""" -from homeassistant.const import ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS, PRESSURE_BAR +from homeassistant.const import ENERGY_KILO_WATT_HOUR, PRESSURE_BAR, TEMP_CELSIUS DOMAIN = "ebusd" TIME_SECONDS = "seconds" diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py index 4bc79e7bd39..63f72a89ccd 100644 --- a/homeassistant/components/ebusd/sensor.py +++ b/homeassistant/components/ebusd/sensor.py @@ -1,6 +1,6 @@ """Support for Ebusd sensors.""" -import logging import datetime +import logging from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util From 5d94c821751021251b5c6e1d2d185972de985f11 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 20 Oct 2019 20:17:27 +0100 Subject: [PATCH 479/639] isort the geniushub code (#27978) --- homeassistant/components/geniushub/__init__.py | 5 ++--- homeassistant/components/geniushub/climate.py | 8 ++++---- homeassistant/components/geniushub/water_heater.py | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 692c72e5776..b34c46a9f26 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -4,9 +4,8 @@ import logging from typing import Any, Dict, Optional import aiohttp -import voluptuous as vol - from geniushubclient import GeniusHub +import voluptuous as vol from homeassistant.const import ( ATTR_TEMPERATURE, @@ -22,8 +21,8 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( - async_dispatcher_send, async_dispatcher_connect, + async_dispatcher_send, ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index f27b1cc7f1a..5acc25a36ee 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -1,14 +1,14 @@ """Support for Genius Hub climate devices.""" -from typing import Optional, List +from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_OFF, HVAC_MODE_HEAT, - PRESET_BOOST, + HVAC_MODE_OFF, PRESET_ACTIVITY, - SUPPORT_TARGET_TEMPERATURE, + PRESET_BOOST, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.helpers.typing import ConfigType, HomeAssistantType diff --git a/homeassistant/components/geniushub/water_heater.py b/homeassistant/components/geniushub/water_heater.py index cd4f536e14f..4141e9f8c04 100644 --- a/homeassistant/components/geniushub/water_heater.py +++ b/homeassistant/components/geniushub/water_heater.py @@ -2,9 +2,9 @@ from typing import List from homeassistant.components.water_heater import ( - WaterHeaterDevice, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_OPERATION_MODE, + SUPPORT_TARGET_TEMPERATURE, + WaterHeaterDevice, ) from homeassistant.const import STATE_OFF from homeassistant.helpers.typing import ConfigType, HomeAssistantType From bce9f14751167bfbffaa4cb315ceb0a456887981 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 20 Oct 2019 20:20:53 +0100 Subject: [PATCH 480/639] isort the evohome code (#27977) --- homeassistant/components/evohome/__init__.py | 6 +++--- homeassistant/components/evohome/climate.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index f86bf974205..29f89dc08d6 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -8,9 +8,9 @@ import re from typing import Any, Dict, Optional, Tuple import aiohttp.client_exceptions -import voluptuous as vol -import evohomeasync2 import evohomeasync +import evohomeasync2 +import voluptuous as vol from homeassistant.const import ( CONF_PASSWORD, @@ -28,7 +28,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType import homeassistant.util.dt as dt_util -from .const import DOMAIN, EVO_FOLLOW, STORAGE_VERSION, STORAGE_KEY, GWS, TCS +from .const import DOMAIN, EVO_FOLLOW, GWS, STORAGE_KEY, STORAGE_VERSION, TCS _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index eb7f3f7d7d8..82a7001539d 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -1,6 +1,6 @@ """Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems.""" import logging -from typing import Optional, List +from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -14,26 +14,26 @@ from homeassistant.components.climate.const import ( PRESET_ECO, PRESET_HOME, PRESET_NONE, - SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import PRECISION_TENTHS from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util.dt import parse_datetime -from . import CONF_LOCATION_IDX, EvoDevice, EvoChild +from . import CONF_LOCATION_IDX, EvoChild, EvoDevice from .const import ( DOMAIN, - EVO_RESET, EVO_AUTO, EVO_AUTOECO, EVO_AWAY, EVO_CUSTOM, EVO_DAYOFF, - EVO_HEATOFF, EVO_FOLLOW, - EVO_TEMPOVER, + EVO_HEATOFF, EVO_PERMOVER, + EVO_RESET, + EVO_TEMPOVER, ) _LOGGER = logging.getLogger(__name__) From 5fa8c02e6413ad9b20eda5b0dc8659999e246cad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:21:17 -0300 Subject: [PATCH 481/639] Move imports in futurenow component (#27991) --- homeassistant/components/futurenow/light.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/futurenow/light.py b/homeassistant/components/futurenow/light.py index eba768f82e3..7b9e79dbb3e 100644 --- a/homeassistant/components/futurenow/light.py +++ b/homeassistant/components/futurenow/light.py @@ -2,15 +2,16 @@ import logging +import pyfnip import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_HOST, CONF_PORT, CONF_DEVICES from homeassistant.components.light import ( ATTR_BRIGHTNESS, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, - PLATFORM_SCHEMA, ) +from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -68,8 +69,6 @@ class FutureNowLight(Light): def __init__(self, device): """Initialize the light.""" - import pyfnip - self._name = device["name"] self._dimmable = device["dimmable"] self._channel = device["channel"] From dd4075d4957e2c989ee9563b1164bc8ac2cafad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:22:51 -0300 Subject: [PATCH 482/639] Move imports in frontier_silicon component (#27990) --- homeassistant/components/frontier_silicon/media_player.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/frontier_silicon/media_player.py b/homeassistant/components/frontier_silicon/media_player.py index 8ab379b050b..010420d0f98 100644 --- a/homeassistant/components/frontier_silicon/media_player.py +++ b/homeassistant/components/frontier_silicon/media_player.py @@ -1,9 +1,11 @@ """Support for Frontier Silicon Devices (Medion, Hama, Auna,...).""" import logging +from afsapi import AFSAPI +import requests import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK, @@ -64,8 +66,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Frontier Silicon platform.""" - import requests - if discovery_info is not None: async_add_entities( [AFSAPIDevice(discovery_info["ssdp_description"], DEFAULT_PASSWORD)], True @@ -118,8 +118,6 @@ class AFSAPIDevice(MediaPlayerDevice): connected to the device in between the updates and invalidated the existing session (i.e UNDOK). """ - from afsapi import AFSAPI - return AFSAPI(self._device_url, self._password) @property From ab2d1ee134719d77a8e8181c122f54d8cafdacd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:23:14 -0300 Subject: [PATCH 483/639] Move imports in gc100 component (#27993) --- homeassistant/components/gc100/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gc100/__init__.py b/homeassistant/components/gc100/__init__.py index 19303fdc6d2..36779b28df2 100644 --- a/homeassistant/components/gc100/__init__.py +++ b/homeassistant/components/gc100/__init__.py @@ -1,9 +1,10 @@ """Support for controlling Global Cache gc100.""" import logging +import gc100 import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, CONF_HOST, CONF_PORT +from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -31,8 +32,6 @@ CONFIG_SCHEMA = vol.Schema( # pylint: disable=no-member def setup(hass, base_config): """Set up the gc100 component.""" - import gc100 - config = base_config[DOMAIN] host = config[CONF_HOST] port = config[CONF_PORT] From 92ed89969c768e5879953dee109250ffee67466a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:28:23 -0300 Subject: [PATCH 484/639] Move imports in gntp component (#27994) --- homeassistant/components/gntp/notify.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/gntp/notify.py b/homeassistant/components/gntp/notify.py index 48c02cf0ba8..5c05b097a1f 100644 --- a/homeassistant/components/gntp/notify.py +++ b/homeassistant/components/gntp/notify.py @@ -2,17 +2,18 @@ import logging import os +import gntp.errors +import gntp.notifier import voluptuous as vol -from homeassistant.const import CONF_PASSWORD, CONF_PORT -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TITLE, ATTR_TITLE_DEFAULT, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_PASSWORD, CONF_PORT +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -69,9 +70,6 @@ class GNTPNotificationService(BaseNotificationService): def __init__(self, app_name, app_icon, hostname, password, port): """Initialize the service.""" - import gntp.notifier - import gntp.errors - self.gntp = gntp.notifier.GrowlNotifier( applicationName=app_name, notifications=["Notification"], From bf6a30d1bbd57132f76067541b89e242535954d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:35:42 -0300 Subject: [PATCH 485/639] Move imports in goalfeed component (#27995) --- homeassistant/components/goalfeed/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/goalfeed/__init__.py b/homeassistant/components/goalfeed/__init__.py index 3a14eb2831d..cdca99e0309 100644 --- a/homeassistant/components/goalfeed/__init__.py +++ b/homeassistant/components/goalfeed/__init__.py @@ -1,11 +1,12 @@ """Component for the Goalfeed service.""" import json +import pysher import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv # Version downgraded due to regression in library # For details: https://github.com/nlsdfnbch/Pysher/issues/38 @@ -30,8 +31,6 @@ GOALFEED_APP_ID = "bfd4ed98c1ff22c04074" def setup(hass, config): """Set up the Goalfeed component.""" - import pysher - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) From ef8f88e25a95ed5104a9b6c15e843b75f11994e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:36:43 -0300 Subject: [PATCH 486/639] Move imports in everlights component (#27983) --- homeassistant/components/everlights/light.py | 23 +++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 506617e4c60..f7fa9deffa0 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -1,25 +1,26 @@ """Support for EverLights lights.""" -import logging from datetime import timedelta +import logging from typing import Tuple +import pyeverlights import voluptuous as vol -from homeassistant.const import CONF_HOSTS from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, ATTR_EFFECT, - SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, - SUPPORT_COLOR, - Light, + ATTR_HS_COLOR, PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_EFFECT, + Light, ) +from homeassistant.const import CONF_HOSTS +from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.exceptions import PlatformNotReady _LOGGER = logging.getLogger(__name__) @@ -46,8 +47,6 @@ def color_int_to_rgb(value: int) -> Tuple[int, int, int]: async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the EverLights lights from configuration.yaml.""" - import pyeverlights - lights = [] for ipaddr in config[CONF_HOSTS]: @@ -159,8 +158,6 @@ class EverLightsLight(Light): async def async_update(self): """Synchronize state with control box.""" - import pyeverlights - try: self._status = await self._api.get_status() except pyeverlights.ConnectionError: From ce00d06cbdd66364bf9a23fedb0a005d45828de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:37:52 -0300 Subject: [PATCH 487/639] Move imports in elkm1 component (#27982) --- homeassistant/components/elkm1/__init__.py | 9 ++++---- .../components/elkm1/alarm_control_panel.py | 15 +----------- homeassistant/components/elkm1/climate.py | 23 ++----------------- homeassistant/components/elkm1/sensor.py | 23 +++++++------------ 4 files changed, 15 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index d15399df67b..d257c46839c 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -2,7 +2,10 @@ import logging import re +import elkm1_lib as elkm1 +from elkm1_lib.const import Max import voluptuous as vol + from homeassistant.const import ( CONF_EXCLUDE, CONF_HOST, @@ -12,8 +15,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback # noqa -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType # noqa @@ -125,9 +127,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: """Set up the Elk M1 platform.""" - from elkm1_lib.const import Max - import elkm1_lib as elkm1 - devices = {} elk_datas = {} diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index 927ed53115e..38519ab5b3f 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -1,4 +1,5 @@ """Each ElkM1 area will be created as a separate alarm_control_panel.""" +from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState import voluptuous as vol import homeassistant.components.alarm_control_panel as alarm @@ -93,8 +94,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= def _arm_services(): - from elkm1_lib.const import ArmLevel - return { "elkm1_alarm_arm_vacation": ArmLevel.ARMED_VACATION.value, "elkm1_alarm_arm_home_instant": ArmLevel.ARMED_STAY_INSTANT.value, @@ -147,8 +146,6 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): @property def device_state_attributes(self): """Attributes of the area.""" - from elkm1_lib.const import AlarmState, ArmedStatus, ArmUpState - attrs = self.initial_attrs() elmt = self._element attrs["is_exit"] = elmt.is_exit @@ -164,8 +161,6 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): return attrs def _element_changed(self, element, changeset): - from elkm1_lib.const import ArmedStatus - elk_state_to_hass_state = { ArmedStatus.DISARMED.value: STATE_ALARM_DISARMED, ArmedStatus.ARMED_AWAY.value: STATE_ALARM_ARMED_AWAY, @@ -191,8 +186,6 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): return self._element.timer1 > 0 or self._element.timer2 > 0 def _area_is_in_alarm_state(self): - from elkm1_lib.const import AlarmState - return self._element.alarm_state >= AlarmState.FIRE_ALARM.value async def async_alarm_disarm(self, code=None): @@ -201,20 +194,14 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): async def async_alarm_arm_home(self, code=None): """Send arm home command.""" - from elkm1_lib.const import ArmLevel - self._element.arm(ArmLevel.ARMED_STAY.value, int(code)) async def async_alarm_arm_away(self, code=None): """Send arm away command.""" - from elkm1_lib.const import ArmLevel - self._element.arm(ArmLevel.ARMED_AWAY.value, int(code)) async def async_alarm_arm_night(self, code=None): """Send arm night command.""" - from elkm1_lib.const import ArmLevel - self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code)) async def _arm_service(self, arm_level, code): diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index 58273e71222..abc9dc0933c 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -1,4 +1,6 @@ """Support for control of Elk-M1 connected thermostats.""" +from elkm1_lib.const import ThermostatFan, ThermostatMode, ThermostatSetting + from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, @@ -16,7 +18,6 @@ from homeassistant.const import PRECISION_WHOLE, STATE_ON from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities - SUPPORT_HVAC = [ HVAC_MODE_OFF, HVAC_MODE_HEAT, @@ -67,8 +68,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): @property def target_temperature(self): """Return the temperature we are trying to reach.""" - from elkm1_lib.const import ThermostatMode - if (self._element.mode == ThermostatMode.HEAT.value) or ( self._element.mode == ThermostatMode.EMERGENCY_HEAT.value ): @@ -115,8 +114,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): @property def is_aux_heat(self): """Return if aux heater is on.""" - from elkm1_lib.const import ThermostatMode - return self._element.mode == ThermostatMode.EMERGENCY_HEAT.value @property @@ -132,8 +129,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): @property def fan_mode(self): """Return the fan setting.""" - from elkm1_lib.const import ThermostatFan - if self._element.fan == ThermostatFan.AUTO.value: return HVAC_MODE_AUTO if self._element.fan == ThermostatFan.ON.value: @@ -141,8 +136,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): return None def _elk_set(self, mode, fan): - from elkm1_lib.const import ThermostatSetting - if mode is not None: self._element.set(ThermostatSetting.MODE.value, mode) if fan is not None: @@ -150,8 +143,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): async def async_set_hvac_mode(self, hvac_mode): """Set thermostat operation mode.""" - from elkm1_lib.const import ThermostatFan, ThermostatMode - settings = { HVAC_MODE_OFF: (ThermostatMode.OFF.value, ThermostatFan.AUTO.value), HVAC_MODE_HEAT: (ThermostatMode.HEAT.value, None), @@ -163,14 +154,10 @@ class ElkThermostat(ElkEntity, ClimateDevice): async def async_turn_aux_heat_on(self): """Turn auxiliary heater on.""" - from elkm1_lib.const import ThermostatMode - self._elk_set(ThermostatMode.EMERGENCY_HEAT.value, None) async def async_turn_aux_heat_off(self): """Turn auxiliary heater off.""" - from elkm1_lib.const import ThermostatMode - self._elk_set(ThermostatMode.HEAT.value, None) @property @@ -180,8 +167,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): async def async_set_fan_mode(self, fan_mode): """Set new target fan mode.""" - from elkm1_lib.const import ThermostatFan - if fan_mode == HVAC_MODE_AUTO: self._elk_set(None, ThermostatFan.AUTO.value) elif fan_mode == STATE_ON: @@ -189,8 +174,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): async def async_set_temperature(self, **kwargs): """Set new target temperature.""" - from elkm1_lib.const import ThermostatSetting - low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) if low_temp is not None: @@ -199,8 +182,6 @@ class ElkThermostat(ElkEntity, ClimateDevice): self._element.set(ThermostatSetting.COOL_SETPOINT.value, round(high_temp)) def _element_changed(self, element, changeset): - from elkm1_lib.const import ThermostatFan, ThermostatMode - mode_to_state = { ThermostatMode.OFF.value: HVAC_MODE_OFF, ThermostatMode.COOL.value: HVAC_MODE_COOL, diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 3f524b778db..3ed5356f4de 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -1,4 +1,12 @@ """Support for control of ElkM1 sensors.""" +from elkm1_lib.const import ( + SettingFormat, + ZoneLogicalStatus, + ZonePhysicalStatus, + ZoneType, +) +from elkm1_lib.util import pretty_const, username + from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities @@ -79,8 +87,6 @@ class ElkKeypad(ElkSensor): @property def device_state_attributes(self): """Attributes of the sensor.""" - from elkm1_lib.util import username - attrs = self.initial_attrs() attrs["area"] = self._element.area + 1 attrs["temperature"] = self._element.temperature @@ -140,8 +146,6 @@ class ElkSetting(ElkSensor): @property def device_state_attributes(self): """Attributes of the sensor.""" - from elkm1_lib.const import SettingFormat - attrs = self.initial_attrs() attrs["value_format"] = SettingFormat(self._element.value_format).name.lower() return attrs @@ -153,8 +157,6 @@ class ElkZone(ElkSensor): @property def icon(self): """Icon to use in the frontend.""" - from elkm1_lib.const import ZoneType - zone_icons = { ZoneType.FIRE_ALARM.value: "fire", ZoneType.FIRE_VERIFIED.value: "fire", @@ -181,8 +183,6 @@ class ElkZone(ElkSensor): @property def device_state_attributes(self): """Attributes of the sensor.""" - from elkm1_lib.const import ZoneLogicalStatus, ZonePhysicalStatus, ZoneType - attrs = self.initial_attrs() attrs["physical_status"] = ZonePhysicalStatus( self._element.physical_status @@ -199,8 +199,6 @@ class ElkZone(ElkSensor): @property def temperature_unit(self): """Return the temperature unit.""" - from elkm1_lib.const import ZoneType - if self._element.definition == ZoneType.TEMPERATURE.value: return self._temperature_unit return None @@ -208,8 +206,6 @@ class ElkZone(ElkSensor): @property def unit_of_measurement(self): """Return the unit of measurement.""" - from elkm1_lib.const import ZoneType - if self._element.definition == ZoneType.TEMPERATURE.value: return self._temperature_unit if self._element.definition == ZoneType.ANALOG_ZONE.value: @@ -217,9 +213,6 @@ class ElkZone(ElkSensor): return None def _element_changed(self, element, changeset): - from elkm1_lib.const import ZoneLogicalStatus, ZoneType - from elkm1_lib.util import pretty_const - if self._element.definition == ZoneType.TEMPERATURE.value: self._state = temperature_to_state(self._element.temperature, -60) elif self._element.definition == ZoneType.ANALOG_ZONE.value: From 87c0207163538192cdb824c6bd91b35812db64d2 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Sun, 20 Oct 2019 23:38:45 +0200 Subject: [PATCH 488/639] Move imports in osramlightify component (#27985) --- homeassistant/components/osramlightify/light.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/osramlightify/light.py b/homeassistant/components/osramlightify/light.py index 9a2da2bce06..05064861844 100644 --- a/homeassistant/components/osramlightify/light.py +++ b/homeassistant/components/osramlightify/light.py @@ -3,14 +3,15 @@ import logging import random import socket +from lightify import Lightify import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - ATTR_EFFECT, EFFECT_RANDOM, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, @@ -20,7 +21,6 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, Light, ) - from homeassistant.const import CONF_HOST import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util @@ -71,11 +71,9 @@ DEFAULT_KELVIN = 2700 def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Osram Lightify lights.""" - import lightify - host = config[CONF_HOST] try: - bridge = lightify.Lightify(host, log_level=logging.NOTSET) + bridge = Lightify(host, log_level=logging.NOTSET) except socket.error as err: msg = "Error connecting to bridge: {} due to: {}".format(host, str(err)) _LOGGER.exception(msg) From bb381d6060c6a675221e34829e70f4fba7e6a160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 18:39:24 -0300 Subject: [PATCH 489/639] Move imports in eliqonline component (#27980) --- homeassistant/components/eliqonline/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/eliqonline/sensor.py b/homeassistant/components/eliqonline/sensor.py index 1f21263a4d6..b3d56e42325 100644 --- a/homeassistant/components/eliqonline/sensor.py +++ b/homeassistant/components/eliqonline/sensor.py @@ -1,15 +1,16 @@ """Monitors home energy use for the ELIQ Online service.""" +import asyncio from datetime import timedelta import logging -import asyncio +import eliqonline import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, POWER_WATT -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -34,8 +35,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the ELIQ Online sensor.""" - import eliqonline - access_token = config.get(CONF_ACCESS_TOKEN) name = config.get(CONF_NAME, DEFAULT_NAME) channel_id = config.get(CONF_CHANNEL_ID) From a13f8a1781a817fa16252fab887636f1bf7388f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Sun, 20 Oct 2019 19:04:56 -0300 Subject: [PATCH 490/639] Move imports in frontend component (#27988) --- homeassistant/components/frontend/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index e46423c8271..541d1bf473d 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -6,23 +6,23 @@ import os import pathlib from typing import Any, Dict, Optional, Set, Tuple -from aiohttp import web, web_urldispatcher, hdrs -import voluptuous as vol +from aiohttp import hdrs, web, web_urldispatcher +import hass_frontend import jinja2 +import voluptuous as vol from yarl import URL -import homeassistant.helpers.config_validation as cv -from homeassistant.components.http.view import HomeAssistantView from homeassistant.components import websocket_api +from homeassistant.components.http.view import HomeAssistantView from homeassistant.config import find_config_file, load_yaml_config_file from homeassistant.const import CONF_NAME, EVENT_THEMES_UPDATED from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.translation import async_get_translations from homeassistant.loader import bind_hass from .storage import async_setup_frontend_storage - # mypy: allow-untyped-defs, no-check-untyped-defs # Fix mimetypes for borked Windows machines @@ -242,8 +242,6 @@ def _frontend_root(dev_repo_path): if dev_repo_path is not None: return pathlib.Path(dev_repo_path) / "hass_frontend" - import hass_frontend - return hass_frontend.where() From ff3c0e56974ba37f633bd4f08a832da7c17a6e04 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Mon, 21 Oct 2019 00:32:17 +0000 Subject: [PATCH 491/639] [ci skip] Translation update --- .../components/abode/.translations/fr.json | 2 +- .../components/abode/.translations/ko.json | 22 +++++++++++++++++++ .../components/adguard/.translations/ca.json | 2 ++ .../components/adguard/.translations/ko.json | 2 ++ .../components/adguard/.translations/no.json | 2 ++ .../components/adguard/.translations/pl.json | 2 ++ .../components/adguard/.translations/ru.json | 2 ++ .../adguard/.translations/zh-Hant.json | 2 ++ .../alarm_control_panel/.translations/ko.json | 11 ++++++++++ .../components/auth/.translations/ko.json | 2 +- .../components/axis/.translations/ko.json | 1 + .../components/cast/.translations/ko.json | 10 ++++----- .../components/cover/.translations/ko.json | 10 +++++++++ .../components/deconz/.translations/ko.json | 1 + .../components/hangouts/.translations/ko.json | 6 ++--- .../components/lock/.translations/ko.json | 13 +++++++++++ .../mobile_app/.translations/ko.json | 2 +- .../opentherm_gw/.translations/ko.json | 13 ++++++++++- .../components/soma/.translations/ko.json | 10 +++++++++ 19 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 homeassistant/components/abode/.translations/ko.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/ko.json create mode 100644 homeassistant/components/cover/.translations/ko.json create mode 100644 homeassistant/components/lock/.translations/ko.json diff --git a/homeassistant/components/abode/.translations/fr.json b/homeassistant/components/abode/.translations/fr.json index c0d9b0b577b..c0c2a35081b 100644 --- a/homeassistant/components/abode/.translations/fr.json +++ b/homeassistant/components/abode/.translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Une seule configuration de Abode est autoris\u00e9e." + "single_instance_allowed": "Une seule configuration d'Abode est autoris\u00e9e." }, "error": { "connection_error": "Impossible de se connecter \u00e0 Abode.", diff --git a/homeassistant/components/abode/.translations/ko.json b/homeassistant/components/abode/.translations/ko.json new file mode 100644 index 00000000000..9560dde6b3d --- /dev/null +++ b/homeassistant/components/abode/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "\ud558\ub098\uc758 Abode \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_error": "Abode \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "identifier_exists": "\uacc4\uc815\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c \uc8fc\uc18c" + }, + "title": "Abode \uc0ac\uc6a9\uc790 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694" + } + }, + "title": "Abode" + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/ca.json b/homeassistant/components/adguard/.translations/ca.json index 30fd509cb7a..9b7b3c39b03 100644 --- a/homeassistant/components/adguard/.translations/ca.json +++ b/homeassistant/components/adguard/.translations/ca.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Aquesta integraci\u00f3 necessita la versi\u00f3 d'AdGuard Home {minimal_version} o una superior, tens la {current_version}. Actualitza el complement de Hass.io d'AdGuard Home.", + "adguard_home_outdated": "Aquesta integraci\u00f3 necessita la versi\u00f3 d'AdGuard Home {minimal_version} o una superior, tens la {current_version}.", "existing_instance_updated": "S'ha actualitzat la configuraci\u00f3 existent.", "single_instance_allowed": "Nom\u00e9s es permet una \u00fanica configuraci\u00f3 d'AdGuard Home." }, diff --git a/homeassistant/components/adguard/.translations/ko.json b/homeassistant/components/adguard/.translations/ko.json index bb93d675103..e1f39259292 100644 --- a/homeassistant/components/adguard/.translations/ko.json +++ b/homeassistant/components/adguard/.translations/ko.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 AdGuard Home {minimal_version} \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud604\uc7ac \ubc84\uc804\uc740 {current_version} \uc785\ub2c8\ub2e4. Hass.io AdGuard Home \uc560\ub4dc\uc628\uc744 \uc5c5\ub370\uc774\ud2b8 \ud574\uc8fc\uc138\uc694.", + "adguard_home_outdated": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 AdGuard Home {minimal_version} \uc774\uc0c1\uc774 \ud544\uc694\ud569\ub2c8\ub2e4. \ud604\uc7ac \ubc84\uc804\uc740 {current_version} \uc785\ub2c8\ub2e4.", "existing_instance_updated": "\uae30\uc874 \uad6c\uc131\uc744 \uc5c5\ub370\uc774\ud2b8\ud588\uc2b5\ub2c8\ub2e4.", "single_instance_allowed": "\ud558\ub098\uc758 AdGuard Home \ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/adguard/.translations/no.json b/homeassistant/components/adguard/.translations/no.json index 2cd6cd72f6d..22a8c23644f 100644 --- a/homeassistant/components/adguard/.translations/no.json +++ b/homeassistant/components/adguard/.translations/no.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Denne integrasjonen krever AdGuard Home {minimal_version} eller h\u00f8yere, du har {current_version}. Vennligst oppdater Hass.io AdGuard Home-tillegget.", + "adguard_home_outdated": "Denne integrasjonen krever AdGuard Home {minimal_version} eller h\u00f8yere, du har {current_version}.", "existing_instance_updated": "Oppdatert eksisterende konfigurasjon.", "single_instance_allowed": "Kun en konfigurasjon av AdGuard Hjemer tillatt." }, diff --git a/homeassistant/components/adguard/.translations/pl.json b/homeassistant/components/adguard/.translations/pl.json index f8f64d54260..69ba6b024e2 100644 --- a/homeassistant/components/adguard/.translations/pl.json +++ b/homeassistant/components/adguard/.translations/pl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Ta integracja wymaga AdGuard Home {minimal_version} lub nowszej wersji, masz {current_version}. Zaktualizuj sw\u00f3j dodatek Hass.io AdGuard Home.", + "adguard_home_outdated": "Ta integracja wymaga AdGuard Home {minimal_version} lub nowszej wersji, masz {current_version}.", "existing_instance_updated": "Zaktualizowano istniej\u0105c\u0105 konfiguracj\u0119.", "single_instance_allowed": "Dozwolona jest tylko jedna konfiguracja AdGuard Home." }, diff --git a/homeassistant/components/adguard/.translations/ru.json b/homeassistant/components/adguard/.translations/ru.json index c50d0197351..eca46d7db00 100644 --- a/homeassistant/components/adguard/.translations/ru.json +++ b/homeassistant/components/adguard/.translations/ru.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 AdGuard Home \u0432\u0435\u0440\u0441\u0438\u0438 {current_version}. \u0414\u043b\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e\u0439 \u0440\u0430\u0431\u043e\u0442\u044b \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0432\u0435\u0440\u0441\u0438\u044f {minimal_version}, \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0430\u044f. \u0414\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043d\u043e\u0432\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438, \u043e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 Hass.io.", + "adguard_home_outdated": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 AdGuard Home \u0432\u0435\u0440\u0441\u0438\u0438 {current_version}. \u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0432\u0435\u0440\u0441\u0438\u044e {minimal_version} \u0438\u043b\u0438 \u0431\u043e\u043b\u0435\u0435 \u043d\u043e\u0432\u0443\u044e.", "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, diff --git a/homeassistant/components/adguard/.translations/zh-Hant.json b/homeassistant/components/adguard/.translations/zh-Hant.json index a693652fedf..d08a5715a8e 100644 --- a/homeassistant/components/adguard/.translations/zh-Hant.json +++ b/homeassistant/components/adguard/.translations/zh-Hant.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "\u6574\u5408\u9700\u8981 AdGuard Home {minimal_version} \u6216\u66f4\u65b0\u7248\u672c\uff0c\u60a8\u76ee\u524d\u4f7f\u7528\u7248\u672c\u70ba {current_version}\u3002\u8acb\u66f4\u65b0 Hass.io AdGuard Home \u5143\u4ef6\u3002", + "adguard_home_outdated": "\u6574\u5408\u9700\u8981 AdGuard Home {minimal_version} \u6216\u66f4\u65b0\u7248\u672c\uff0c\u60a8\u76ee\u524d\u4f7f\u7528\u7248\u672c\u70ba {current_version}\u3002", "existing_instance_updated": "\u5df2\u66f4\u65b0\u73fe\u6709\u8a2d\u5b9a\u3002", "single_instance_allowed": "\u50c5\u5141\u8a31\u8a2d\u5b9a\u4e00\u7d44 AdGuard Home\u3002" }, diff --git a/homeassistant/components/alarm_control_panel/.translations/ko.json b/homeassistant/components/alarm_control_panel/.translations/ko.json new file mode 100644 index 00000000000..5d6caa5fe12 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/ko.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "{entity_name} \uc678\ucd9c\uacbd\ube44", + "arm_home": "{entity_name} \uc7ac\uc2e4\uacbd\ube44", + "arm_night": "{entity_name} \uc57c\uac04\uacbd\ube44", + "disarm": "{entity_name} \uacbd\ube44\ud574\uc81c", + "trigger": "{entity_name} \ud2b8\ub9ac\uac70" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/.translations/ko.json b/homeassistant/components/auth/.translations/ko.json index 1cb70519b20..6c2e8988d83 100644 --- a/homeassistant/components/auth/.translations/ko.json +++ b/homeassistant/components/auth/.translations/ko.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [\uad6c\uae00 OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", + "description": "\uc2dc\uac04 \uae30\ubc18\uc758 \uc77c\ud68c\uc6a9 \ube44\ubc00\ubc88\ud638\ub97c \uc0ac\uc6a9\ud558\ub294 2\ub2e8\uacc4 \uc778\uc99d\uc744 \ud558\ub824\uba74 \uc778\uc99d\uc6a9 \uc571\uc744 \uc774\uc6a9\ud574\uc11c QR \ucf54\ub4dc\ub97c \uc2a4\uce94\ud574\uc8fc\uc138\uc694. \uc778\uc99d\uc6a9 \uc571\uc740 [Google OTP](https://support.google.com/accounts/answer/1066447) \ub610\ub294 [Authy](https://authy.com/) \ub97c \ucd94\ucc9c\ub4dc\ub9bd\ub2c8\ub2e4.\n\n{qr_code}\n\n\uc2a4\uce94 \ud6c4\uc5d0 \uc0dd\uc131\ub41c 6\uc790\ub9ac \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc11c \uc124\uc815\uc744 \ud655\uc778\ud558\uc138\uc694. QR \ucf54\ub4dc \uc2a4\uce94\uc5d0 \ubb38\uc81c\uac00 \uc788\ub2e4\uba74, **`{code}`** \ucf54\ub4dc\ub85c \uc9c1\uc811 \uc124\uc815\ud574\ubcf4\uc138\uc694.", "title": "TOTP 2\ub2e8\uacc4 \uc778\uc99d \uad6c\uc131" } }, diff --git a/homeassistant/components/axis/.translations/ko.json b/homeassistant/components/axis/.translations/ko.json index 5ceaa082810..f02b7cdcefa 100644 --- a/homeassistant/components/axis/.translations/ko.json +++ b/homeassistant/components/axis/.translations/ko.json @@ -12,6 +12,7 @@ "device_unavailable": "\uae30\uae30\ub97c \uc0ac\uc6a9\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "faulty_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ud639\uc740 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, + "flow_title": "Axis \uae30\uae30: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/cast/.translations/ko.json b/homeassistant/components/cast/.translations/ko.json index 71dee3afec5..1374372aa24 100644 --- a/homeassistant/components/cast/.translations/ko.json +++ b/homeassistant/components/cast/.translations/ko.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "no_devices_found": "\uad6c\uae00 \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", - "single_instance_allowed": "\ud558\ub098\uc758 \uad6c\uae00 \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + "no_devices_found": "Google \uce90\uc2a4\ud2b8 \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", + "single_instance_allowed": "\ud558\ub098\uc758 Google \uce90\uc2a4\ud2b8\ub9cc \uad6c\uc131 \ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4." }, "step": { "confirm": { - "description": "\uad6c\uae00 \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8" + "description": "Google \uce90\uc2a4\ud2b8\ub97c \uc124\uc815 \ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Google \uce90\uc2a4\ud2b8" } }, - "title": "\uad6c\uae00 \uce90\uc2a4\ud2b8" + "title": "Google \uce90\uc2a4\ud2b8" } } \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json new file mode 100644 index 00000000000..02f900a8fe5 --- /dev/null +++ b/homeassistant/components/cover/.translations/ko.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud614\uc2b5\ub2c8\ub2e4", + "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud799\ub2c8\ub2e4", + "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub838\uc2b5\ub2c8\ub2e4", + "is_opening": "{entity_name} \uc774(\uac00) \uc5f4\ub9bd\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index ef8d3910ecf..61725316b13 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -11,6 +11,7 @@ "error": { "no_key": "API \ud0a4\ub97c \uac00\uc838\uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, + "flow_title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774 ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/hangouts/.translations/ko.json b/homeassistant/components/hangouts/.translations/ko.json index 3b1c755b358..385fc128b3b 100644 --- a/homeassistant/components/hangouts/.translations/ko.json +++ b/homeassistant/components/hangouts/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uad6c\uae00 \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", + "already_configured": "Google \ud589\uc544\uc6c3\uc740 \uc774\ubbf8 \uc124\uc815\ub41c \uc0c1\ud0dc\uc785\ub2c8\ub2e4", "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { @@ -24,9 +24,9 @@ "password": "\ube44\ubc00\ubc88\ud638" }, "description": "\uc8c4\uc1a1\ud569\ub2c8\ub2e4. \uad00\ub828 \ub0b4\uc6a9\uc774 \uc544\uc9c1 \uc5c5\ub370\uc774\ud2b8 \ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \ucd94\ud6c4\uc5d0 \ubc18\uc601\ub420 \uc608\uc815\uc774\ub2c8 \uc870\uae08\ub9cc \uae30\ub2e4\ub824\uc8fc\uc138\uc694.", - "title": "\uad6c\uae00 \ud589\uc544\uc6c3 \ub85c\uadf8\uc778" + "title": "Google \ud589\uc544\uc6c3 \ub85c\uadf8\uc778" } }, - "title": "\uad6c\uae00 \ud589\uc544\uc6c3" + "title": "Google \ud589\uc544\uc6c3" } } \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/ko.json b/homeassistant/components/lock/.translations/ko.json new file mode 100644 index 00000000000..6abd9cd60e6 --- /dev/null +++ b/homeassistant/components/lock/.translations/ko.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "{entity_name} \uc7a0\uae08", + "open": "{entity_name} \uc5f4\uae30", + "unlock": "{entity_name} \uc7a0\uae08 \ud574\uc81c" + }, + "condition_type": { + "is_locked": "{entity_name} \uc774(\uac00) \uc7a0\uacbc\uc2b5\ub2c8\ub2e4", + "is_unlocked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/.translations/ko.json b/homeassistant/components/mobile_app/.translations/ko.json index faf30e5f985..899845fcc2e 100644 --- a/homeassistant/components/mobile_app/.translations/ko.json +++ b/homeassistant/components/mobile_app/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "\ubaa8\ubc14\uc77c \uc571\uc744 \uc5f4\uc5b4 Home Assistant \uc640 \ud1b5\ud569\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uc548\ub0b4]({apps_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." + "install_app": "\ubaa8\ubc14\uc77c \uc571\uc744 \uc5f4\uc5b4 Home Assistant \uc640 \uc5f0\ub3d9\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694. \ud638\ud658\ub418\ub294 \uc571 \ubaa9\ub85d\uc740 [\uc548\ub0b4]({apps_url}) \ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694." }, "step": { "confirm": { diff --git a/homeassistant/components/opentherm_gw/.translations/ko.json b/homeassistant/components/opentherm_gw/.translations/ko.json index e5daf826ee5..f370427625d 100644 --- a/homeassistant/components/opentherm_gw/.translations/ko.json +++ b/homeassistant/components/opentherm_gw/.translations/ko.json @@ -10,7 +10,7 @@ "init": { "data": { "device": "\uacbd\ub85c \ub610\ub294 URL", - "floor_temperature": "\uc2e4\ub0b4\uc628\ub3c4 \uc18c\uc218\uc810 \ub0b4\ub9bc", + "floor_temperature": "\uc2e4\ub0b4\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", "id": "ID", "name": "\uc774\ub984", "precision": "\uc2e4\ub0b4\uc628\ub3c4 \uc815\ubc00\ub3c4" @@ -19,5 +19,16 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", + "precision": "\uc815\ubc00\ub3c4" + }, + "description": "OpenTherm Gateway \uc635\uc158" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ko.json b/homeassistant/components/soma/.translations/ko.json index 53146bebf83..90995ebc9f2 100644 --- a/homeassistant/components/soma/.translations/ko.json +++ b/homeassistant/components/soma/.translations/ko.json @@ -8,6 +8,16 @@ "create_entry": { "default": "Soma \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "description": "SOMA Connect \uc640\uc758 \uc5f0\uacb0 \uc124\uc815\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "SOMA Connect" + } + }, "title": "Soma" } } \ No newline at end of file From 4bb82fa8adea2fe5c7f94abe7e85ab7ed77aef5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:48:25 -0300 Subject: [PATCH 492/639] Move imports in message_bird component (#28022) --- homeassistant/components/message_bird/notify.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/message_bird/notify.py b/homeassistant/components/message_bird/notify.py index 5df02ef69c4..ce1d275a832 100644 --- a/homeassistant/components/message_bird/notify.py +++ b/homeassistant/components/message_bird/notify.py @@ -1,16 +1,17 @@ """MessageBird platform for notify component.""" import logging +import messagebird +from messagebird.client import ErrorException import voluptuous as vol -from homeassistant.const import CONF_API_KEY, CONF_SENDER -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.const import CONF_API_KEY, CONF_SENDER +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -26,8 +27,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the MessageBird notification service.""" - import messagebird - client = messagebird.Client(config[CONF_API_KEY]) try: # validates the api key @@ -49,8 +48,6 @@ class MessageBirdNotificationService(BaseNotificationService): def send_message(self, message=None, **kwargs): """Send a message to a specified target.""" - from messagebird.client import ErrorException - targets = kwargs.get(ATTR_TARGET) if not targets: _LOGGER.error("No target specified") From ad39b957d6232a8de739d5c5007abe603d28b784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:52:49 -0300 Subject: [PATCH 493/639] Move imports in mopar component (#28028) --- homeassistant/components/mopar/__init__.py | 13 +++---------- homeassistant/components/mopar/sensor.py | 4 ++-- homeassistant/components/mopar/switch.py | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mopar/__init__.py b/homeassistant/components/mopar/__init__.py index 857dbab2a3b..21a3c3d16ea 100644 --- a/homeassistant/components/mopar/__init__.py +++ b/homeassistant/components/mopar/__init__.py @@ -1,17 +1,18 @@ """Support for Mopar vehicles.""" -import logging from datetime import timedelta +import logging +import motorparts import voluptuous as vol from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD, CONF_PIN, CONF_SCAN_INTERVAL, + CONF_USERNAME, ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -53,8 +54,6 @@ SERVICE_HORN_SCHEMA = vol.Schema({vol.Required(ATTR_VEHICLE_INDEX): cv.positive_ def setup(hass, config): """Set up the Mopar component.""" - import motorparts - conf = config[DOMAIN] cookie = hass.config.path(COOKIE_FILE) try: @@ -101,8 +100,6 @@ class MoparData: def update(self, now, **kwargs): """Update data.""" - import motorparts - _LOGGER.debug("Updating vehicle data") try: self.vehicles = motorparts.get_summary(self._session)["vehicles"] @@ -123,8 +120,6 @@ class MoparData: @property def attribution(self): """Get the attribution string from Mopar.""" - import motorparts - return motorparts.ATTRIBUTION def get_vehicle_name(self, index): @@ -136,8 +131,6 @@ class MoparData: def actuate(self, command, index): """Run a command on the specified Mopar vehicle.""" - import motorparts - try: response = getattr(motorparts, command)(self._session, index) except motorparts.MoparError as error: diff --git a/homeassistant/components/mopar/sensor.py b/homeassistant/components/mopar/sensor.py index a29e9c5c739..2243fcdaa22 100644 --- a/homeassistant/components/mopar/sensor.py +++ b/homeassistant/components/mopar/sensor.py @@ -1,8 +1,8 @@ """Support for the Mopar vehicle sensor platform.""" from homeassistant.components.mopar import ( - DOMAIN as MOPAR_DOMAIN, - DATA_UPDATED, ATTR_VEHICLE_INDEX, + DATA_UPDATED, + DOMAIN as MOPAR_DOMAIN, ) from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_KILOMETERS from homeassistant.core import callback diff --git a/homeassistant/components/mopar/switch.py b/homeassistant/components/mopar/switch.py index bbada4ecee7..2dad56637ce 100644 --- a/homeassistant/components/mopar/switch.py +++ b/homeassistant/components/mopar/switch.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.mopar import DOMAIN as MOPAR_DOMAIN from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_ON, STATE_OFF +from homeassistant.const import STATE_OFF, STATE_ON _LOGGER = logging.getLogger(__name__) From 1e27e2827d7ec200e8b9fc123d61502d5b1d625e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:53:28 -0300 Subject: [PATCH 494/639] Move imports in mvglive component (#28031) --- homeassistant/components/mvglive/sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/mvglive/sensor.py b/homeassistant/components/mvglive/sensor.py index 3c753d832e0..da1db0e02aa 100644 --- a/homeassistant/components/mvglive/sensor.py +++ b/homeassistant/components/mvglive/sensor.py @@ -1,14 +1,15 @@ """Support for departure information for public transport in Munich.""" -import logging -from datetime import timedelta - from copy import deepcopy +from datetime import timedelta +import logging + +import MVGLive import voluptuous as vol +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION _LOGGER = logging.getLogger(__name__) @@ -150,8 +151,6 @@ class MVGLiveData: self, station, destinations, directions, lines, products, timeoffset, number ): """Initialize the sensor.""" - import MVGLive - self._station = station self._destinations = destinations self._directions = directions From 6a392e13dda275a4c4332739814b5f445a63699b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:54:00 -0300 Subject: [PATCH 495/639] Move imports in mpd component (#28030) --- homeassistant/components/mpd/media_player.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index c19f8f49226..2628815727c 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -3,9 +3,10 @@ from datetime import timedelta import logging import os +import mpd import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_PLAYLIST, @@ -85,8 +86,6 @@ class MpdDevice(MediaPlayerDevice): # pylint: disable=no-member def __init__(self, server, port, password, name): """Initialize the MPD device.""" - import mpd - self.server = server self.port = port self._name = name @@ -107,8 +106,6 @@ class MpdDevice(MediaPlayerDevice): def _connect(self): """Connect to MPD.""" - import mpd - try: self._client.connect(self.server, self.port) @@ -121,8 +118,6 @@ class MpdDevice(MediaPlayerDevice): def _disconnect(self): """Disconnect from MPD.""" - import mpd - try: self._client.disconnect() except mpd.ConnectionError: @@ -144,8 +139,6 @@ class MpdDevice(MediaPlayerDevice): def update(self): """Get the latest data and update the state.""" - import mpd - try: if not self._is_connected: self._connect() @@ -261,8 +254,6 @@ class MpdDevice(MediaPlayerDevice): @Throttle(PLAYLIST_UPDATE_INTERVAL) def _update_playlists(self, **kwargs): """Update available MPD playlists.""" - import mpd - try: self._playlists = [] for playlist_data in self._client.listplaylists(): From cc3173e3ce4dadbfa42090705036865e71c16ce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:54:27 -0300 Subject: [PATCH 496/639] Move imports in namecheapdns component (#28034) --- homeassistant/components/namecheapdns/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/namecheapdns/__init__.py b/homeassistant/components/namecheapdns/__init__.py index 56c50ff52f8..fbc78f622a1 100644 --- a/homeassistant/components/namecheapdns/__init__.py +++ b/homeassistant/components/namecheapdns/__init__.py @@ -1,13 +1,14 @@ """Support for namecheap DNS services.""" -import logging from datetime import timedelta +import logging +import defusedxml.ElementTree as ET import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_DOMAIN -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.const import CONF_DOMAIN, CONF_HOST, CONF_PASSWORD from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -55,8 +56,6 @@ async def async_setup(hass, config): async def _update_namecheapdns(session, host, domain, password): """Update namecheap DNS entry.""" - import defusedxml.ElementTree as ET - params = {"host": host, "domain": domain, "password": password} resp = await session.get(UPDATE_URL, params=params) From 265a1f1fb6b8726d6cc6c129fef34a0f9bf7eb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:54:51 -0300 Subject: [PATCH 497/639] Move imports in neurio_energy component (#28035) --- homeassistant/components/neurio_energy/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/neurio_energy/sensor.py b/homeassistant/components/neurio_energy/sensor.py index eac716573db..894bfae6180 100644 --- a/homeassistant/components/neurio_energy/sensor.py +++ b/homeassistant/components/neurio_energy/sensor.py @@ -1,15 +1,16 @@ """Support for monitoring a Neurio energy sensor.""" -import logging from datetime import timedelta +import logging +import neurio import requests.exceptions import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, POWER_WATT, ENERGY_KILO_WATT_HOUR +from homeassistant.const import CONF_API_KEY, ENERGY_KILO_WATT_HOUR, POWER_WATT +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -import homeassistant.helpers.config_validation as cv import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -69,8 +70,6 @@ class NeurioData: def __init__(self, api_key, api_secret, sensor_id): """Initialize the data.""" - import neurio - self.api_key = api_key self.api_secret = api_secret self.sensor_id = sensor_id From 1342fe2b3cc87232150ccb129f9024ef99bc6f44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:55:29 -0300 Subject: [PATCH 498/639] Move imports in openevse component (#28043) --- homeassistant/components/openevse/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/openevse/sensor.py b/homeassistant/components/openevse/sensor.py index d29dec224bd..0ac655cd448 100644 --- a/homeassistant/components/openevse/sensor.py +++ b/homeassistant/components/openevse/sensor.py @@ -1,17 +1,18 @@ """Support for monitoring an OpenEVSE Charger.""" import logging +import openevsewifi from requests import RequestException import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - TEMP_CELSIUS, CONF_HOST, - ENERGY_KILO_WATT_HOUR, CONF_MONITORED_VARIABLES, + ENERGY_KILO_WATT_HOUR, + TEMP_CELSIUS, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -38,8 +39,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenEVSE sensor.""" - import openevsewifi - host = config.get(CONF_HOST) monitored_variables = config.get(CONF_MONITORED_VARIABLES) From c1fccee83a805c140c8e87ec9177c89967e2b8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:55:53 -0300 Subject: [PATCH 499/639] Move imports in magicseaweed component (#28020) --- homeassistant/components/magicseaweed/sensor.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/magicseaweed/sensor.py b/homeassistant/components/magicseaweed/sensor.py index 66ab87a6569..174ecf1882e 100644 --- a/homeassistant/components/magicseaweed/sensor.py +++ b/homeassistant/components/magicseaweed/sensor.py @@ -1,19 +1,21 @@ """Support for magicseaweed data from magicseaweed.com.""" from datetime import timedelta import logging + +import magicseaweed import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_API_KEY, - CONF_NAME, - CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION, + CONF_API_KEY, + CONF_MONITORED_CONDITIONS, + CONF_NAME, ) import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -175,8 +177,6 @@ class MagicSeaweedData: def __init__(self, api_key, spot_id, units): """Initialize the data object.""" - import magicseaweed - self._msw = magicseaweed.MSW_Forecast(api_key, spot_id, None, units) self.currently = None self.hourly = {} From ff385d5e2b45299a9009992653902cb859ae754f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:56:19 -0300 Subject: [PATCH 500/639] Move imports in lw12wifi component (#28019) --- homeassistant/components/lw12wifi/light.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/lw12wifi/light.py b/homeassistant/components/lw12wifi/light.py index 3b9ccae1681..abf75a1e318 100644 --- a/homeassistant/components/lw12wifi/light.py +++ b/homeassistant/components/lw12wifi/light.py @@ -2,6 +2,7 @@ import logging +import lw12 import voluptuous as vol from homeassistant.components.light import ( @@ -9,18 +10,17 @@ from homeassistant.components.light import ( ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, - Light, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_EFFECT, SUPPORT_COLOR, + SUPPORT_EFFECT, SUPPORT_TRANSITION, + Light, ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util - _LOGGER = logging.getLogger(__name__) @@ -38,8 +38,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up LW-12 WiFi LED Controller platform.""" - import lw12 - # Assign configuration variables. name = config.get(CONF_NAME) host = config.get(CONF_HOST) @@ -107,8 +105,6 @@ class LW12WiFi(Light): Use the Enum element name for display. """ - import lw12 - return [effect.name.replace("_", " ").title() for effect in lw12.LW12_EFFECT] @property @@ -123,8 +119,6 @@ class LW12WiFi(Light): def turn_on(self, **kwargs): """Instruct the light to turn on.""" - import lw12 - self._light.light_on() if ATTR_HS_COLOR in kwargs: self._rgb_color = color_util.color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) From 6de95995aaef6728ec8fdd2255cea665d1d84e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:57:31 -0300 Subject: [PATCH 501/639] Move imports in logbook component (#28016) --- homeassistant/components/logbook/__init__.py | 39 ++++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 3c5e828765c..8675f778a26 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -2,12 +2,26 @@ from datetime import timedelta from itertools import groupby import logging +import time +from sqlalchemy.exc import SQLAlchemyError import voluptuous as vol -from homeassistant.loader import bind_hass from homeassistant.components import sun +from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME +from homeassistant.components.homekit.const import ( + ATTR_DISPLAY_NAME, + ATTR_VALUE, + DOMAIN as DOMAIN_HOMEKIT, + EVENT_HOMEKIT_CHANGED, +) from homeassistant.components.http import HomeAssistantView +from homeassistant.components.recorder.models import Events, States +from homeassistant.components.recorder.util import ( + QUERY_RETRY_WAIT, + RETRIES, + session_scope, +) from homeassistant.const import ( ATTR_DOMAIN, ATTR_ENTITY_ID, @@ -16,26 +30,21 @@ from homeassistant.const import ( ATTR_SERVICE, CONF_EXCLUDE, CONF_INCLUDE, + EVENT_AUTOMATION_TRIGGERED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, - EVENT_STATE_CHANGED, - EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED, + EVENT_STATE_CHANGED, HTTP_BAD_REQUEST, STATE_NOT_HOME, STATE_OFF, STATE_ON, ) from homeassistant.core import DOMAIN as HA_DOMAIN, State, callback, split_entity_id -from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME -from homeassistant.components.homekit.const import ( - ATTR_DISPLAY_NAME, - ATTR_VALUE, - DOMAIN as DOMAIN_HOMEKIT, - EVENT_HOMEKIT_CHANGED, -) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entityfilter import generate_filter +from homeassistant.loader import bind_hass import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -371,11 +380,6 @@ def humanify(hass, events): def _get_related_entity_ids(session, entity_filter): - from homeassistant.components.recorder.models import States - from homeassistant.components.recorder.util import RETRIES, QUERY_RETRY_WAIT - from sqlalchemy.exc import SQLAlchemyError - import time - timer_start = time.perf_counter() query = session.query(States).with_entities(States.entity_id).distinct() @@ -402,8 +406,6 @@ def _get_related_entity_ids(session, entity_filter): def _generate_filter_from_config(config): - from homeassistant.helpers.entityfilter import generate_filter - excluded_entities = [] excluded_domains = [] included_entities = [] @@ -425,9 +427,6 @@ def _generate_filter_from_config(config): def _get_events(hass, config, start_day, end_day, entity_id=None): """Get events for a period of time.""" - from homeassistant.components.recorder.models import Events, States - from homeassistant.components.recorder.util import session_scope - entities_filter = _generate_filter_from_config(config) def yield_events(query): From e19663f172d54314455b9ebf19500ddd5f7eb4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:58:22 -0300 Subject: [PATCH 502/639] Move imports in lirc component (#28015) --- homeassistant/components/lirc/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index 47814d00e9a..bfc8e455624 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -1,12 +1,13 @@ """Support for LIRC devices.""" # pylint: disable=no-member, import-error +import logging import threading import time -import logging +import lirc import voluptuous as vol -from homeassistant.const import EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_START +from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -23,8 +24,6 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) def setup(hass, config): """Set up the LIRC capability.""" - import lirc - # blocking=True gives unexpected behavior (multiple responses for 1 press) # also by not blocking, we allow hass to shut down the thread gracefully # on exit. @@ -61,8 +60,6 @@ class LircInterface(threading.Thread): def run(self): """Run the loop of the LIRC interface thread.""" - import lirc - _LOGGER.debug("LIRC interface thread started") while not self.stopped.isSet(): try: From 67f7146cab6ccaed05b3235992a44534d431fe6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:58:46 -0300 Subject: [PATCH 503/639] Move imports in linode component (#28014) --- homeassistant/components/linode/__init__.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/linode/__init__.py b/homeassistant/components/linode/__init__.py index 6f590c33e08..a18b63d7226 100644 --- a/homeassistant/components/linode/__init__.py +++ b/homeassistant/components/linode/__init__.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import linode import voluptuous as vol from homeassistant.const import CONF_ACCESS_TOKEN @@ -35,8 +36,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Linode component.""" - import linode - conf = config[DOMAIN] access_token = conf.get(CONF_ACCESS_TOKEN) @@ -58,16 +57,12 @@ class Linode: def __init__(self, access_token): """Initialize the Linode connection.""" - import linode - self._access_token = access_token self.data = None self.manager = linode.LinodeClient(token=self._access_token) def get_node_id(self, node_name): """Get the status of a Linode Instance.""" - import linode - node_id = None try: @@ -83,8 +78,6 @@ class Linode: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Use the data from Linode API.""" - import linode - try: self.data = self.manager.linode.get_instances() except linode.errors.ApiError as _ex: From 96509c0c0bc61ffa4b81a77e0840361f10f01c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:58:59 -0300 Subject: [PATCH 504/639] Move imports in oasa_telematics component (#28039) --- homeassistant/components/oasa_telematics/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/oasa_telematics/sensor.py b/homeassistant/components/oasa_telematics/sensor.py index 0c16f3769d5..4bf6b395d5f 100644 --- a/homeassistant/components/oasa_telematics/sensor.py +++ b/homeassistant/components/oasa_telematics/sensor.py @@ -1,13 +1,14 @@ """Support for OASA Telematics from telematics.oasa.gr.""" -import logging from datetime import timedelta +import logging from operator import itemgetter +import oasatelematics import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION, DEVICE_CLASS_TIMESTAMP +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, DEVICE_CLASS_TIMESTAMP +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import dt as dt_util @@ -128,8 +129,6 @@ class OASATelematicsData: def __init__(self, stop_id, route_id): """Initialize the data object.""" - import oasatelematics - self.stop_id = stop_id self.route_id = route_id self.info = self.empty_result() From cf2ee1a09f550d278163797beba6c87713064c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:59:26 -0300 Subject: [PATCH 505/639] Move imports in iss component (#28003) --- homeassistant/components/iss/binary_sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/iss/binary_sensor.py b/homeassistant/components/iss/binary_sensor.py index 5f38c3d166e..002b2e958f7 100644 --- a/homeassistant/components/iss/binary_sensor.py +++ b/homeassistant/components/iss/binary_sensor.py @@ -1,18 +1,19 @@ """Support for International Space Station data sensor.""" -import logging from datetime import timedelta +import logging +import pyiss import requests import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import ( - CONF_NAME, - ATTR_LONGITUDE, ATTR_LATITUDE, + ATTR_LONGITUDE, + CONF_NAME, CONF_SHOW_ON_MAP, ) +import homeassistant.helpers.config_validation as cv from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -113,8 +114,6 @@ class IssData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the ISS API.""" - import pyiss - try: iss = pyiss.ISS() self.is_above = iss.is_ISS_above(self.latitude, self.longitude) From fb79c45645d24902f538bf69e7dcc78c53ec914f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 04:59:50 -0300 Subject: [PATCH 506/639] Move imports in iperf3 component (#28002) --- homeassistant/components/iperf3/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/iperf3/__init__.py b/homeassistant/components/iperf3/__init__.py index eda601b09de..753ea60efa4 100644 --- a/homeassistant/components/iperf3/__init__.py +++ b/homeassistant/components/iperf3/__init__.py @@ -1,19 +1,20 @@ """Support for Iperf3 network measurement tool.""" -import logging from datetime import timedelta +import logging +import iperf3 import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( + CONF_HOST, + CONF_HOSTS, CONF_MONITORED_CONDITIONS, CONF_PORT, - CONF_HOST, CONF_PROTOCOL, - CONF_HOSTS, CONF_SCAN_INTERVAL, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval @@ -80,8 +81,6 @@ SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_HOST, default=None): cv.string}) async def async_setup(hass, config): """Set up the iperf3 component.""" - import iperf3 - hass.data[DOMAIN] = {} conf = config[DOMAIN] From bbc71441a1863fc47d6450f4b1870f30ae6c467b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:02:22 -0300 Subject: [PATCH 507/639] Move imports in pandora component (#28045) --- homeassistant/components/pandora/media_player.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/pandora/media_player.py b/homeassistant/components/pandora/media_player.py index c242670ba48..417903c46e0 100644 --- a/homeassistant/components/pandora/media_player.py +++ b/homeassistant/components/pandora/media_player.py @@ -6,6 +6,8 @@ import re import shutil import signal +import pexpect + from homeassistant import util from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -104,8 +106,6 @@ class PandoraMediaPlayer(MediaPlayerDevice): def turn_on(self): """Turn the media player on.""" - import pexpect - if self._player_state != STATE_OFF: return self._pianobar = pexpect.spawn("pianobar") @@ -136,8 +136,6 @@ class PandoraMediaPlayer(MediaPlayerDevice): def turn_off(self): """Turn the media player off.""" - import pexpect - if self._pianobar is None: _LOGGER.info("Pianobar subprocess already stopped") return @@ -226,8 +224,6 @@ class PandoraMediaPlayer(MediaPlayerDevice): def _send_station_list_command(self): """Send a station list command.""" - import pexpect - self._pianobar.send("s") try: self._pianobar.expect("Select station:", timeout=1) @@ -248,8 +244,6 @@ class PandoraMediaPlayer(MediaPlayerDevice): def _query_for_playing_status(self): """Query system for info about current track.""" - import pexpect - self._clear_buffer() self._pianobar.send("i") try: @@ -372,8 +366,6 @@ class PandoraMediaPlayer(MediaPlayerDevice): This is necessary because there are a bunch of 00:00 in the buffer """ - import pexpect - try: while not self._pianobar.expect(".+", timeout=0.1): pass From 2f966919387927394bf0ac1b52312186985e09e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:02:59 -0300 Subject: [PATCH 508/639] Move imports in otp component (#28044) --- homeassistant/components/otp/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/otp/sensor.py b/homeassistant/components/otp/sensor.py index a175155e6f2..3c4cd464d44 100644 --- a/homeassistant/components/otp/sensor.py +++ b/homeassistant/components/otp/sensor.py @@ -1,13 +1,14 @@ """Support for One-Time Password (OTP).""" -import time import logging +import time +import pyotp import voluptuous as vol -from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_TOKEN +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -41,8 +42,6 @@ class TOTPSensor(Entity): def __init__(self, name, token): """Initialize the sensor.""" - import pyotp - self._name = name self._otp = pyotp.TOTP(token) self._state = None From 56a7233e0fc006e56897b0d17c5b9776793029a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:03:24 -0300 Subject: [PATCH 509/639] Move imports in ohmconnect component (#28041) --- homeassistant/components/ohmconnect/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ohmconnect/sensor.py b/homeassistant/components/ohmconnect/sensor.py index 10b622d16c9..a9606e25bad 100644 --- a/homeassistant/components/ohmconnect/sensor.py +++ b/homeassistant/components/ohmconnect/sensor.py @@ -1,15 +1,16 @@ """Support for OhmConnect.""" -import logging from datetime import timedelta +import logging +import defusedxml.ElementTree as ET import requests import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ class OhmconnectSensor(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from OhmConnect.""" - import defusedxml.ElementTree as ET - try: url = ("https://login.ohmconnect.com" "/verify-ohm-hour/{}").format( self._ohmid From e9674374a4362c23d07cf6125e20205167b2a32a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:03:49 -0300 Subject: [PATCH 510/639] Move imports in norway_air component (#28037) --- homeassistant/components/norway_air/air_quality.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/norway_air/air_quality.py b/homeassistant/components/norway_air/air_quality.py index 9b30ad5aaa8..8e6c13260e5 100644 --- a/homeassistant/components/norway_air/air_quality.py +++ b/homeassistant/components/norway_air/air_quality.py @@ -1,14 +1,14 @@ """Sensor for checking the air quality forecast around Norway.""" +from datetime import timedelta import logging -from datetime import timedelta +import metno import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.air_quality import PLATFORM_SCHEMA, AirQualityEntity from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession - +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -71,8 +71,6 @@ class AirSensor(AirQualityEntity): def __init__(self, name, coordinates, forecast, session): """Initialize the sensor.""" - import metno - self._name = name self._api = metno.AirQualityData(coordinates, forecast, session) From 92ed8362ce08b4410a6043ae449db03406f3d041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:04:10 -0300 Subject: [PATCH 511/639] Move imports in niko_home_control component (#28036) --- homeassistant/components/niko_home_control/light.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/niko_home_control/light.py b/homeassistant/components/niko_home_control/light.py index 4cb84956002..265e51d6e67 100644 --- a/homeassistant/components/niko_home_control/light.py +++ b/homeassistant/components/niko_home_control/light.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import nikohomecontrol import voluptuous as vol # Import the device class from the component that you want to support @@ -20,8 +21,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Niko Home Control light platform.""" - import nikohomecontrol - host = config[CONF_HOST] try: From 38db4b0a23e2c2aaf858d0b2bd9d5ae4df819e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:04:33 -0300 Subject: [PATCH 512/639] Move imports in mythicbeastsdns component (#28033) --- homeassistant/components/mythicbeastsdns/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mythicbeastsdns/__init__.py b/homeassistant/components/mythicbeastsdns/__init__.py index 993b62ac48d..d961c2e6e3d 100644 --- a/homeassistant/components/mythicbeastsdns/__init__.py +++ b/homeassistant/components/mythicbeastsdns/__init__.py @@ -1,10 +1,10 @@ """Support for Mythic Beasts Dynamic DNS service.""" -import logging from datetime import timedelta +import logging +import mbddns import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_DOMAIN, CONF_HOST, @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -39,8 +40,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Initialize the Mythic Beasts component.""" - import mbddns - domain = config[DOMAIN][CONF_DOMAIN] password = config[DOMAIN][CONF_PASSWORD] host = config[DOMAIN][CONF_HOST] From 206f8cef5c47c154354c0196b256b5feedf9478f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:05:05 -0300 Subject: [PATCH 513/639] Move imports in mychevy component (#28032) --- homeassistant/components/mychevy/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mychevy/__init__.py b/homeassistant/components/mychevy/__init__.py index 8ec83ed374b..0ec4d05a623 100644 --- a/homeassistant/components/mychevy/__init__.py +++ b/homeassistant/components/mychevy/__init__.py @@ -4,11 +4,11 @@ import logging import threading import time +import mychevy.mychevy as mc import voluptuous as vol from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.util import Throttle DOMAIN = "mychevy" @@ -71,8 +71,6 @@ class EVBinarySensorConfig: def setup(hass, base_config): """Set up the mychevy component.""" - import mychevy.mychevy as mc - config = base_config.get(DOMAIN) email = config.get(CONF_USERNAME) From 3e9d28f28a92900a44a682c85d645000bcec7c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:05:41 -0300 Subject: [PATCH 514/639] Move imports in mobile_app component (#28027) --- homeassistant/components/mobile_app/__init__.py | 3 +-- homeassistant/components/mobile_app/binary_sensor.py | 3 +-- homeassistant/components/mobile_app/config_flow.py | 3 ++- homeassistant/components/mobile_app/const.py | 2 +- .../components/mobile_app/device_tracker.py | 9 +++++---- homeassistant/components/mobile_app/helpers.py | 12 +++++------- homeassistant/components/mobile_app/http_api.py | 12 +++++------- homeassistant/components/mobile_app/notify.py | 1 - homeassistant/components/mobile_app/sensor.py | 1 - homeassistant/components/mobile_app/webhook.py | 12 ++++-------- homeassistant/components/mobile_app/websocket_api.py | 1 - 11 files changed, 24 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 01877099201..ca2a58d1f96 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,6 +1,6 @@ """Integrates Native Apps to Home Assistant.""" -from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.components.webhook import async_register as webhook_register +from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import device_registry as dr, discovery from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -20,7 +20,6 @@ from .const import ( STORAGE_KEY, STORAGE_VERSION, ) - from .http_api import RegistrationsView from .webhook import handle_webhook from .websocket_api import register_websocket_handlers diff --git a/homeassistant/components/mobile_app/binary_sensor.py b/homeassistant/components/mobile_app/binary_sensor.py index 975c4c16c32..73bf925553e 100644 --- a/homeassistant/components/mobile_app/binary_sensor.py +++ b/homeassistant/components/mobile_app/binary_sensor.py @@ -1,9 +1,9 @@ """Binary sensor platform for mobile_app.""" from functools import partial +from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.core import callback -from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( @@ -13,7 +13,6 @@ from .const import ( DATA_DEVICES, DOMAIN, ) - from .entity import MobileAppEntity, sensor_id diff --git a/homeassistant/components/mobile_app/config_flow.py b/homeassistant/components/mobile_app/config_flow.py index 96b0a35aae2..bc9c6167da8 100644 --- a/homeassistant/components/mobile_app/config_flow.py +++ b/homeassistant/components/mobile_app/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Mobile App.""" from homeassistant import config_entries -from .const import DOMAIN, ATTR_DEVICE_NAME + +from .const import ATTR_DEVICE_NAME, DOMAIN @config_entries.HANDLERS.register(DOMAIN) diff --git a/homeassistant/components/mobile_app/const.py b/homeassistant/components/mobile_app/const.py index d01990b74b9..0b6a93a39ea 100644 --- a/homeassistant/components/mobile_app/const.py +++ b/homeassistant/components/mobile_app/const.py @@ -4,13 +4,13 @@ import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASSES as BINARY_SENSOR_CLASSES, ) -from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES from homeassistant.components.device_tracker import ( ATTR_BATTERY, ATTR_GPS, ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ) +from homeassistant.components.sensor import DEVICE_CLASSES as SENSOR_CLASSES from homeassistant.const import ATTR_DOMAIN, ATTR_SERVICE, ATTR_SERVICE_DATA from homeassistant.helpers import config_validation as cv diff --git a/homeassistant/components/mobile_app/device_tracker.py b/homeassistant/components/mobile_app/device_tracker.py index 0e05c424609..f58f80aa5fc 100644 --- a/homeassistant/components/mobile_app/device_tracker.py +++ b/homeassistant/components/mobile_app/device_tracker.py @@ -1,19 +1,20 @@ """Device tracker platform that adds support for OwnTracks over MQTT.""" import logging -from homeassistant.core import callback -from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, ATTR_BATTERY_LEVEL -from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS +from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.core import callback from homeassistant.helpers.restore_state import RestoreEntity + from .const import ( ATTR_ALTITUDE, ATTR_BATTERY, ATTR_COURSE, ATTR_DEVICE_ID, ATTR_DEVICE_NAME, - ATTR_GPS_ACCURACY, ATTR_GPS, + ATTR_GPS_ACCURACY, ATTR_LOCATION_NAME, ATTR_SPEED, ATTR_VERTICAL_ACCURACY, diff --git a/homeassistant/components/mobile_app/helpers.py b/homeassistant/components/mobile_app/helpers.py index 3be082951c5..2fb949720d6 100644 --- a/homeassistant/components/mobile_app/helpers.py +++ b/homeassistant/components/mobile_app/helpers.py @@ -1,9 +1,11 @@ """Helpers for mobile_app.""" -import logging import json +import logging from typing import Callable, Dict, Tuple -from aiohttp.web import json_response, Response +from aiohttp.web import Response, json_response +from nacl.encoding import Base64Encoder +from nacl.secret import SecretBox from homeassistant.core import Context from homeassistant.helpers.json import JSONEncoder @@ -13,8 +15,8 @@ from .const import ( ATTR_APP_DATA, ATTR_APP_ID, ATTR_APP_NAME, - ATTR_DEVICE_ID, ATTR_APP_VERSION, + ATTR_DEVICE_ID, ATTR_DEVICE_NAME, ATTR_MANUFACTURER, ATTR_MODEL, @@ -36,8 +38,6 @@ def setup_decrypt() -> Tuple[int, Callable]: Async friendly. """ - from nacl.secret import SecretBox - from nacl.encoding import Base64Encoder def decrypt(ciphertext, key): """Decrypt ciphertext using key.""" @@ -51,8 +51,6 @@ def setup_encrypt() -> Tuple[int, Callable]: Async friendly. """ - from nacl.secret import SecretBox - from nacl.encoding import Base64Encoder def encrypt(ciphertext, key): """Encrypt ciphertext using key.""" diff --git a/homeassistant/components/mobile_app/http_api.py b/homeassistant/components/mobile_app/http_api.py index 67914ea7076..ee69f15fb11 100644 --- a/homeassistant/components/mobile_app/http_api.py +++ b/homeassistant/components/mobile_app/http_api.py @@ -1,18 +1,19 @@ """Provides an HTTP API for mobile_app.""" -import uuid from typing import Dict +import uuid -from aiohttp.web import Response, Request +from aiohttp.web import Request, Response +from nacl.secret import SecretBox from homeassistant.auth.util import generate_secret from homeassistant.components.cloud import ( + CloudNotAvailable, async_create_cloudhook, async_remote_ui_url, - CloudNotAvailable, ) from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from homeassistant.const import HTTP_CREATED, CONF_WEBHOOK_ID +from homeassistant.const import CONF_WEBHOOK_ID, HTTP_CREATED from .const import ( ATTR_DEVICE_ID, @@ -24,7 +25,6 @@ from .const import ( DOMAIN, REGISTRATION_SCHEMA, ) - from .helpers import supports_encryption @@ -49,8 +49,6 @@ class RegistrationsView(HomeAssistantView): data[CONF_WEBHOOK_ID] = webhook_id if data[ATTR_SUPPORTS_ENCRYPTION] and supports_encryption(): - from nacl.secret import SecretBox - data[CONF_SECRET] = generate_secret(SecretBox.KEY_SIZE) data[CONF_USER_ID] = request["hass_user"].id diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index 1e6a0517026..8ac34c9af1d 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -12,7 +12,6 @@ from homeassistant.components.notify import ( ATTR_TITLE_DEFAULT, BaseNotificationService, ) - from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util diff --git a/homeassistant/components/mobile_app/sensor.py b/homeassistant/components/mobile_app/sensor.py index b96a6f1e2f0..199ba968dd2 100644 --- a/homeassistant/components/mobile_app/sensor.py +++ b/homeassistant/components/mobile_app/sensor.py @@ -13,7 +13,6 @@ from .const import ( DATA_DEVICES, DOMAIN, ) - from .entity import MobileAppEntity, sensor_id diff --git a/homeassistant/components/mobile_app/webhook.py b/homeassistant/components/mobile_app/webhook.py index f95d5b993f0..66188500fd6 100644 --- a/homeassistant/components/mobile_app/webhook.py +++ b/homeassistant/components/mobile_app/webhook.py @@ -1,13 +1,12 @@ """Webhook handlers for mobile_app.""" import logging -from aiohttp.web import HTTPBadRequest, Response, Request +from aiohttp.web import HTTPBadRequest, Request, Response import voluptuous as vol -from homeassistant.components.cloud import async_remote_ui_url, CloudNotAvailable +from homeassistant.components.cloud import CloudNotAvailable, async_remote_ui_url from homeassistant.components.frontend import MANIFEST_JSON from homeassistant.components.zone.const import DOMAIN as ZONE_DOMAIN - from homeassistant.const import ( ATTR_DOMAIN, ATTR_SERVICE, @@ -50,10 +49,10 @@ from .const import ( ERR_ENCRYPTION_REQUIRED, ERR_SENSOR_DUPLICATE_UNIQUE_ID, ERR_SENSOR_NOT_REGISTERED, + SIGNAL_LOCATION_UPDATE, SIGNAL_SENSOR_UPDATE, WEBHOOK_PAYLOAD_SCHEMA, WEBHOOK_SCHEMAS, - WEBHOOK_TYPES, WEBHOOK_TYPE_CALL_SERVICE, WEBHOOK_TYPE_FIRE_EVENT, WEBHOOK_TYPE_GET_CONFIG, @@ -63,10 +62,8 @@ from .const import ( WEBHOOK_TYPE_UPDATE_LOCATION, WEBHOOK_TYPE_UPDATE_REGISTRATION, WEBHOOK_TYPE_UPDATE_SENSOR_STATES, - SIGNAL_LOCATION_UPDATE, + WEBHOOK_TYPES, ) - - from .helpers import ( _decrypt_payload, empty_okay_response, @@ -77,7 +74,6 @@ from .helpers import ( webhook_response, ) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/mobile_app/websocket_api.py b/homeassistant/components/mobile_app/websocket_api.py index d0d13415b4d..813d0a9cf89 100644 --- a/homeassistant/components/mobile_app/websocket_api.py +++ b/homeassistant/components/mobile_app/websocket_api.py @@ -29,7 +29,6 @@ from .const import ( DATA_STORE, DOMAIN, ) - from .helpers import safe_registration, savable_state From 936dac22703aa5ae6a2644793f86f1edd35b6497 Mon Sep 17 00:00:00 2001 From: Kevin McCormack Date: Mon, 21 Oct 2019 04:06:16 -0400 Subject: [PATCH 515/639] Add Vivotek camera component code owner (#28024) --- CODEOWNERS | 1 + homeassistant/components/vivotek/manifest.json | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 30946fb14f2..6e254c5a1d4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -321,6 +321,7 @@ homeassistant/components/velux/* @Julius2342 homeassistant/components/version/* @fabaff homeassistant/components/vesync/* @markperdue @webdjoe homeassistant/components/vicare/* @oischinger +homeassistant/components/vivotek/* @HarlemSquirrel homeassistant/components/vizio/* @raman325 homeassistant/components/vlc_telnet/* @rodripf homeassistant/components/waqi/* @andrey-git diff --git a/homeassistant/components/vivotek/manifest.json b/homeassistant/components/vivotek/manifest.json index 20b2ac347f6..ff498991127 100644 --- a/homeassistant/components/vivotek/manifest.json +++ b/homeassistant/components/vivotek/manifest.json @@ -6,5 +6,7 @@ "libpyvivotek==0.2.2" ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@HarlemSquirrel" + ] } From 4db761e6f259938b41d6cf7dee9f70b42fe61f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:06:38 -0300 Subject: [PATCH 516/639] Move imports in metoffice component (#28023) --- homeassistant/components/metoffice/sensor.py | 5 +---- homeassistant/components/metoffice/weather.py | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/metoffice/sensor.py b/homeassistant/components/metoffice/sensor.py index 3ca55533ce3..98d94ebe6ca 100644 --- a/homeassistant/components/metoffice/sensor.py +++ b/homeassistant/components/metoffice/sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import datapoint as dp import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -92,8 +93,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Met Office sensor platform.""" - import datapoint as dp - api_key = config.get(CONF_API_KEY) latitude = config.get(CONF_LATITUDE, hass.config.latitude) longitude = config.get(CONF_LONGITUDE, hass.config.longitude) @@ -193,8 +192,6 @@ class MetOfficeCurrentData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from Datapoint.""" - import datapoint as dp - try: forecast = self._datapoint.get_forecast_for_site(self._site.id, "3hourly") self.data = forecast.now() diff --git a/homeassistant/components/metoffice/weather.py b/homeassistant/components/metoffice/weather.py index bb7a64005ce..09350588d46 100644 --- a/homeassistant/components/metoffice/weather.py +++ b/homeassistant/components/metoffice/weather.py @@ -1,6 +1,7 @@ """Support for UK Met Office weather service.""" import logging +import datapoint as dp import voluptuous as vol from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity @@ -35,8 +36,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Met Office weather platform.""" - import datapoint as dp - name = config.get(CONF_NAME) datapoint = dp.connection(api_key=config.get(CONF_API_KEY)) From 8922d702ae6595f2e73231f86b7485de17233685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:07:09 -0300 Subject: [PATCH 517/639] Move imports in lupusec component (#28018) --- homeassistant/components/lupusec/__init__.py | 11 ++++------- homeassistant/components/lupusec/binary_sensor.py | 4 ++-- homeassistant/components/lupusec/switch.py | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py index c64789ec4dd..60f3a192b07 100644 --- a/homeassistant/components/lupusec/__init__.py +++ b/homeassistant/components/lupusec/__init__.py @@ -1,11 +1,12 @@ """Support for Lupusec Home Security system.""" import logging +import lupupy +from lupupy.exceptions import LupusecException import voluptuous as vol -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME, CONF_IP_ADDRESS +from homeassistant.const import CONF_IP_ADDRESS, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -34,8 +35,6 @@ LUPUSEC_PLATFORMS = ["alarm_control_panel", "binary_sensor", "switch"] def setup(hass, config): """Set up Lupusec component.""" - from lupupy.exceptions import LupusecException - conf = config[DOMAIN] username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] @@ -67,8 +66,6 @@ class LupusecSystem: def __init__(self, username, password, ip_address, name): """Initialize the system.""" - import lupupy - self.lupusec = lupupy.Lupusec(username, password, ip_address) self.name = name diff --git a/homeassistant/components/lupusec/binary_sensor.py b/homeassistant/components/lupusec/binary_sensor.py index ccd45e9f874..b2a332a03e7 100644 --- a/homeassistant/components/lupusec/binary_sensor.py +++ b/homeassistant/components/lupusec/binary_sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import lupupy.constants as CONST + from homeassistant.components.binary_sensor import DEVICE_CLASSES, BinarySensorDevice from . import DOMAIN as LUPUSEC_DOMAIN, LupusecDevice @@ -16,8 +18,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return - import lupupy.constants as CONST - data = hass.data[LUPUSEC_DOMAIN] device_types = [CONST.TYPE_OPENING] diff --git a/homeassistant/components/lupusec/switch.py b/homeassistant/components/lupusec/switch.py index b6391959397..a6864f39ef7 100644 --- a/homeassistant/components/lupusec/switch.py +++ b/homeassistant/components/lupusec/switch.py @@ -2,6 +2,8 @@ from datetime import timedelta import logging +import lupupy.constants as CONST + from homeassistant.components.switch import SwitchDevice from . import DOMAIN as LUPUSEC_DOMAIN, LupusecDevice @@ -16,8 +18,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if discovery_info is None: return - import lupupy.constants as CONST - data = hass.data[LUPUSEC_DOMAIN] devices = [] From 90731555f87ad2f9f48fb247dd9b05ebcecc8611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:07:35 -0300 Subject: [PATCH 518/639] Move imports in loopenergy component (#28017) --- homeassistant/components/loopenergy/sensor.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py index 994c3e2fd89..537907d9d0a 100644 --- a/homeassistant/components/loopenergy/sensor.py +++ b/homeassistant/components/loopenergy/sensor.py @@ -1,6 +1,7 @@ """Support for Loop Energy sensors.""" import logging +import pyloopenergy import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -54,8 +55,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Loop Energy sensors.""" - import pyloopenergy - elec_config = config.get(CONF_ELEC) gas_config = config.get(CONF_GAS, {}) From 6742b36a3dff759635ff2530793ac0fed1291581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:08:04 -0300 Subject: [PATCH 519/639] Move imports in lifx_legacy component (#28013) --- homeassistant/components/lifx_legacy/light.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/lifx_legacy/light.py b/homeassistant/components/lifx_legacy/light.py index 78a333018f9..8f767a2f559 100644 --- a/homeassistant/components/lifx_legacy/light.py +++ b/homeassistant/components/lifx_legacy/light.py @@ -9,6 +9,7 @@ https://home-assistant.io/components/light.lifx/ """ import logging +import liffylights import voluptuous as vol from homeassistant.components.light import ( @@ -16,19 +17,19 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_HS_COLOR, ATTR_TRANSITION, + PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, - SUPPORT_COLOR_TEMP, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, Light, - PLATFORM_SCHEMA, -) -from homeassistant.helpers.event import track_time_change -from homeassistant.util.color import ( - color_temperature_mired_to_kelvin, - color_temperature_kelvin_to_mired, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import track_time_change +from homeassistant.util.color import ( + color_temperature_kelvin_to_mired, + color_temperature_mired_to_kelvin, +) _LOGGER = logging.getLogger(__name__) @@ -71,8 +72,6 @@ class LIFX: def __init__(self, add_entities_callback, server_addr=None, broadcast_addr=None): """Initialize the light.""" - import liffylights - self._devices = [] self._add_entities_callback = add_entities_callback From 9a9cd1d0b259c2672b400dcb96e0aff183061f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:09:14 -0300 Subject: [PATCH 520/639] Move imports in lifx component (#28012) --- homeassistant/components/lifx/__init__.py | 6 +++--- homeassistant/components/lifx/config_flow.py | 7 ++++--- homeassistant/components/lifx/light.py | 6 ++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/lifx/__init__.py b/homeassistant/components/lifx/__init__.py index f4ae2c4030a..6e921a59afe 100644 --- a/homeassistant/components/lifx/__init__.py +++ b/homeassistant/components/lifx/__init__.py @@ -1,12 +1,12 @@ """Support for LIFX.""" import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant import config_entries -from homeassistant.const import CONF_PORT from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN -from .const import DOMAIN +from homeassistant.const import CONF_PORT +import homeassistant.helpers.config_validation as cv +from .const import DOMAIN CONF_SERVER = "server" CONF_BROADCAST = "broadcast" diff --git a/homeassistant/components/lifx/config_flow.py b/homeassistant/components/lifx/config_flow.py index b324dc0cad8..71fe7247c12 100644 --- a/homeassistant/components/lifx/config_flow.py +++ b/homeassistant/components/lifx/config_flow.py @@ -1,13 +1,14 @@ """Config flow flow LIFX.""" -from homeassistant.helpers import config_entry_flow +import aiolifx + from homeassistant import config_entries +from homeassistant.helpers import config_entry_flow + from .const import DOMAIN async def _async_has_devices(hass): """Return if there are devices that can be discovered.""" - import aiolifx - lifx_ip_addresses = await aiolifx.LifxScan(hass.loop).scan() return len(lifx_ip_addresses) > 0 diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index d183dcb0fa2..50e36e8db0a 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -6,6 +6,8 @@ import logging import math import sys +import aiolifx as aiolifx_module +import aiolifx_effects as aiolifx_effects_module import voluptuous as vol from homeassistant import util @@ -151,15 +153,11 @@ LIFX_EFFECT_STOP_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_id def aiolifx(): """Return the aiolifx module.""" - import aiolifx as aiolifx_module - return aiolifx_module def aiolifx_effects(): """Return the aiolifx_effects module.""" - import aiolifx_effects as aiolifx_effects_module - return aiolifx_effects_module From 09acbc211c90a69e33db5b54a6c32099fe83a0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:09:38 -0300 Subject: [PATCH 521/639] Move imports in lg_soundbar component (#28011) --- .../components/lg_soundbar/media_player.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 5c98f86a2bc..30cfbf17074 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -1,14 +1,15 @@ """Support for LG soundbars.""" import logging +import temescal + from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( + SUPPORT_SELECT_SOUND_MODE, SUPPORT_SELECT_SOURCE, SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, - SUPPORT_SELECT_SOUND_MODE, ) - from homeassistant.const import STATE_ON _LOGGER = logging.getLogger(__name__) @@ -32,8 +33,6 @@ class LGDevice(MediaPlayerDevice): def __init__(self, discovery_info): """Initialize the LG speakers.""" - import temescal - host = discovery_info.get("host") port = discovery_info.get("port") @@ -140,7 +139,6 @@ class LGDevice(MediaPlayerDevice): @property def sound_mode(self): """Return the current sound mode.""" - import temescal if self._equaliser == -1: return "" @@ -149,8 +147,6 @@ class LGDevice(MediaPlayerDevice): @property def sound_mode_list(self): """Return the available sound modes.""" - import temescal - modes = [] for equaliser in self._equalisers: modes.append(temescal.equalisers[equaliser]) @@ -159,8 +155,6 @@ class LGDevice(MediaPlayerDevice): @property def source(self): """Return the current input source.""" - import temescal - if self._function == -1: return "" return temescal.functions[self._function] @@ -168,8 +162,6 @@ class LGDevice(MediaPlayerDevice): @property def source_list(self): """List of available input sources.""" - import temescal - sources = [] for function in self._functions: sources.append(temescal.functions[function]) @@ -191,12 +183,8 @@ class LGDevice(MediaPlayerDevice): def select_source(self, source): """Select input source.""" - import temescal - self._device.set_func(temescal.functions.index(source)) def select_sound_mode(self, sound_mode): """Set Sound Mode for Receiver..""" - import temescal - self._device.set_eq(temescal.equalisers.index(sound_mode)) From 09f9875ccf8c7c48208c7ccaf148a90000d2e51f Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Mon, 21 Oct 2019 11:17:21 +0300 Subject: [PATCH 522/639] Glances config flow (#27221) * Glances Integration with config flow * Glances Integration with config flow * fix description texts * Glances Integration with config flow * Glances Integration with config flow * fix description texts * update .coverage.py * add codeowner * add test_options * Fixed typos, Added import, fixed tests * sort imports * remove commented code --- .coveragerc | 1 + CODEOWNERS | 2 +- .../components/glances/.translations/en.json | 37 ++++ homeassistant/components/glances/__init__.py | 175 +++++++++++++++++- .../components/glances/config_flow.py | 130 +++++++++++++ homeassistant/components/glances/const.py | 36 ++++ .../components/glances/manifest.json | 6 +- homeassistant/components/glances/sensor.py | 168 +++++------------ homeassistant/components/glances/strings.json | 37 ++++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/glances/__init__.py | 1 + tests/components/glances/test_config_flow.py | 102 ++++++++++ 13 files changed, 569 insertions(+), 130 deletions(-) create mode 100644 homeassistant/components/glances/.translations/en.json create mode 100644 homeassistant/components/glances/config_flow.py create mode 100644 homeassistant/components/glances/const.py create mode 100644 homeassistant/components/glances/strings.json create mode 100644 tests/components/glances/__init__.py create mode 100644 tests/components/glances/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 7071312a99c..7c35022c355 100644 --- a/.coveragerc +++ b/.coveragerc @@ -253,6 +253,7 @@ omit = homeassistant/components/github/sensor.py homeassistant/components/gitlab_ci/sensor.py homeassistant/components/gitter/sensor.py + homeassistant/components/glances/__init__.py homeassistant/components/glances/sensor.py homeassistant/components/gntp/notify.py homeassistant/components/goalfeed/* diff --git a/CODEOWNERS b/CODEOWNERS index 6e254c5a1d4..e1ff7b36ff1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -110,7 +110,7 @@ homeassistant/components/geniushub/* @zxdavb homeassistant/components/geo_rss_events/* @exxamalte homeassistant/components/geonetnz_quakes/* @exxamalte homeassistant/components/gitter/* @fabaff -homeassistant/components/glances/* @fabaff +homeassistant/components/glances/* @fabaff @engrbm87 homeassistant/components/gntp/* @robbiet480 homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton diff --git a/homeassistant/components/glances/.translations/en.json b/homeassistant/components/glances/.translations/en.json new file mode 100644 index 00000000000..1bd7275daef --- /dev/null +++ b/homeassistant/components/glances/.translations/en.json @@ -0,0 +1,37 @@ +{ + "config": { + "title": "Glances", + "step": { + "user": { + "title": "Setup Glances", + "data": { + "name": "Name", + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port", + "version": "Glances API Version (2 or 3)", + "ssl": "Use SSL/TLS to connect to the Glances system", + "verify_ssl": "Verify the certification of the system" + } + } + }, + "error": { + "cannot_connect": "Unable to connect to host", + "wrong_version": "Version not supported (2 or 3 only)" + }, + "abort": { + "already_configured": "Host is already configured." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Glances", + "data": { + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/__init__.py b/homeassistant/components/glances/__init__.py index b458d8788fc..d09aa782534 100644 --- a/homeassistant/components/glances/__init__.py +++ b/homeassistant/components/glances/__init__.py @@ -1 +1,174 @@ -"""The glances component.""" +"""The Glances component.""" +from datetime import timedelta +import logging + +from glances_api import Glances, exceptions +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from .const import ( + CONF_VERSION, + DATA_UPDATED, + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DEFAULT_VERSION, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +GLANCES_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, + vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In([2, 3]), + } + ) +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.All(cv.ensure_list, [GLANCES_SCHEMA])}, extra=vol.ALLOW_EXTRA +) + + +async def async_setup(hass: HomeAssistant, config: Config) -> bool: + """Configure Glances using config flow only.""" + if DOMAIN in config: + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) + ) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Glances from config entry.""" + client = GlancesData(hass, config_entry) + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client + if not await client.async_setup(): + return False + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") + hass.data[DOMAIN].pop(config_entry.entry_id) + return True + + +class GlancesData: + """Get the latest data from Glances api.""" + + def __init__(self, hass, config_entry): + """Initialize the Glances data.""" + self.hass = hass + self.config_entry = config_entry + self.api = None + self.unsub_timer = None + self.available = False + + @property + def host(self): + """Return client host.""" + return self.config_entry.data[CONF_HOST] + + async def async_update(self): + """Get the latest data from the Glances REST API.""" + try: + await self.api.get_data() + self.available = True + except exceptions.GlancesApiError: + _LOGGER.error("Unable to fetch data from Glances") + self.available = False + _LOGGER.debug("Glances data updated") + async_dispatcher_send(self.hass, DATA_UPDATED) + + async def async_setup(self): + """Set up the Glances client.""" + try: + self.api = get_api(self.hass, self.config_entry.data) + await self.api.get_data() + self.available = True + _LOGGER.debug("Successfully connected to Glances") + + except exceptions.GlancesApiConnectionError: + _LOGGER.debug("Can not connect to Glances") + raise ConfigEntryNotReady + + self.add_options() + self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) + self.config_entry.add_update_listener(self.async_options_updated) + + self.hass.async_create_task( + self.hass.config_entries.async_forward_entry_setup( + self.config_entry, "sensor" + ) + ) + return True + + def add_options(self): + """Add options for Glances integration.""" + if not self.config_entry.options: + options = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} + self.hass.config_entries.async_update_entry( + self.config_entry, options=options + ) + + def set_scan_interval(self, scan_interval): + """Update scan interval.""" + + async def refresh(event_time): + """Get the latest data from Glances api.""" + await self.async_update() + + if self.unsub_timer is not None: + self.unsub_timer() + self.unsub_timer = async_track_time_interval( + self.hass, refresh, timedelta(seconds=scan_interval) + ) + + @staticmethod + async def async_options_updated(hass, entry): + """Triggered by config entry options updates.""" + hass.data[DOMAIN][entry.entry_id].set_scan_interval( + entry.options[CONF_SCAN_INTERVAL] + ) + + +def get_api(hass, entry): + """Return the api from glances_api.""" + params = entry.copy() + params.pop(CONF_NAME) + verify_ssl = params.pop(CONF_VERIFY_SSL) + session = async_get_clientsession(hass, verify_ssl) + return Glances(hass.loop, session, **params) diff --git a/homeassistant/components/glances/config_flow.py b/homeassistant/components/glances/config_flow.py new file mode 100644 index 00000000000..3c86fae0357 --- /dev/null +++ b/homeassistant/components/glances/config_flow.py @@ -0,0 +1,130 @@ +"""Config flow for Glances.""" +import glances_api +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import callback + +from . import get_api +from .const import ( + CONF_VERSION, + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DEFAULT_VERSION, + DOMAIN, + SUPPORTED_VERSIONS, +) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + vol.Required(CONF_VERSION, default=DEFAULT_VERSION): int, + vol.Optional(CONF_SSL, default=False): bool, + vol.Optional(CONF_VERIFY_SSL, default=False): bool, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == data[CONF_HOST]: + raise AlreadyConfigured + + if data[CONF_VERSION] not in SUPPORTED_VERSIONS: + raise WrongVersion + try: + api = get_api(hass, data) + await api.get_data() + except glances_api.exceptions.GlancesApiConnectionError: + raise CannotConnect + + +class GlancesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Glances config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return GlancesOptionsFlowHandler(config_entry) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + await validate_input(self.hass, user_input) + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) + except AlreadyConfigured: + return self.async_abort(reason="already_configured") + except CannotConnect: + errors["base"] = "cannot_connect" + except WrongVersion: + errors[CONF_VERSION] = "wrong_version" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_config): + """Import from Glances sensor config.""" + + return await self.async_step_user(user_input=import_config) + + +class GlancesOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Glances client options.""" + + def __init__(self, config_entry): + """Initialize Glances options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the Glances options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + options = { + vol.Optional( + CONF_SCAN_INTERVAL, + default=self.config_entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), + ): int + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class AlreadyConfigured(exceptions.HomeAssistantError): + """Error to indicate host is already configured.""" + + +class WrongVersion(exceptions.HomeAssistantError): + """Error to indicate the selected version is wrong.""" diff --git a/homeassistant/components/glances/const.py b/homeassistant/components/glances/const.py new file mode 100644 index 00000000000..e47586ea245 --- /dev/null +++ b/homeassistant/components/glances/const.py @@ -0,0 +1,36 @@ +"""Constants for Glances component.""" +from homeassistant.const import TEMP_CELSIUS + +DOMAIN = "glances" +CONF_VERSION = "version" + +DEFAULT_HOST = "localhost" +DEFAULT_NAME = "Glances" +DEFAULT_PORT = 61208 +DEFAULT_VERSION = 3 +DEFAULT_SCAN_INTERVAL = 60 + +DATA_UPDATED = "glances_data_updated" +SUPPORTED_VERSIONS = [2, 3] + +SENSOR_TYPES = { + "disk_use_percent": ["Disk used percent", "%", "mdi:harddisk"], + "disk_use": ["Disk used", "GiB", "mdi:harddisk"], + "disk_free": ["Disk free", "GiB", "mdi:harddisk"], + "memory_use_percent": ["RAM used percent", "%", "mdi:memory"], + "memory_use": ["RAM used", "MiB", "mdi:memory"], + "memory_free": ["RAM free", "MiB", "mdi:memory"], + "swap_use_percent": ["Swap used percent", "%", "mdi:memory"], + "swap_use": ["Swap used", "GiB", "mdi:memory"], + "swap_free": ["Swap free", "GiB", "mdi:memory"], + "processor_load": ["CPU load", "15 min", "mdi:memory"], + "process_running": ["Running", "Count", "mdi:memory"], + "process_total": ["Total", "Count", "mdi:memory"], + "process_thread": ["Thread", "Count", "mdi:memory"], + "process_sleeping": ["Sleeping", "Count", "mdi:memory"], + "cpu_use_percent": ["CPU used", "%", "mdi:memory"], + "cpu_temp": ["CPU Temp", TEMP_CELSIUS, "mdi:thermometer"], + "docker_active": ["Containers active", "", "mdi:docker"], + "docker_cpu_use": ["Containers CPU used", "%", "mdi:docker"], + "docker_memory_use": ["Containers RAM used", "MiB", "mdi:docker"], +} diff --git a/homeassistant/components/glances/manifest.json b/homeassistant/components/glances/manifest.json index 775d208c1c4..6067b1a9868 100644 --- a/homeassistant/components/glances/manifest.json +++ b/homeassistant/components/glances/manifest.json @@ -1,12 +1,14 @@ { "domain": "glances", "name": "Glances", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/glances", "requirements": [ "glances_api==0.2.0" ], "dependencies": [], "codeowners": [ - "@fabaff" + "@fabaff", + "@engrbm87" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/glances/sensor.py b/homeassistant/components/glances/sensor.py index 90b4b386f37..760958f0dee 100644 --- a/homeassistant/components/glances/sensor.py +++ b/homeassistant/components/glances/sensor.py @@ -1,114 +1,31 @@ """Support gathering system information of hosts which are running glances.""" -from datetime import timedelta import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_USERNAME, - CONF_PASSWORD, - CONF_SSL, - CONF_VERIFY_SSL, - CONF_RESOURCES, - STATE_UNAVAILABLE, - TEMP_CELSIUS, -) -from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_NAME, STATE_UNAVAILABLE +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle + +from .const import DATA_UPDATED, DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) -CONF_VERSION = "version" - -DEFAULT_HOST = "localhost" -DEFAULT_NAME = "Glances" -DEFAULT_PORT = "61208" -DEFAULT_VERSION = 2 - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) - -SENSOR_TYPES = { - "disk_use_percent": ["Disk used percent", "%", "mdi:harddisk"], - "disk_use": ["Disk used", "GiB", "mdi:harddisk"], - "disk_free": ["Disk free", "GiB", "mdi:harddisk"], - "memory_use_percent": ["RAM used percent", "%", "mdi:memory"], - "memory_use": ["RAM used", "MiB", "mdi:memory"], - "memory_free": ["RAM free", "MiB", "mdi:memory"], - "swap_use_percent": ["Swap used percent", "%", "mdi:memory"], - "swap_use": ["Swap used", "GiB", "mdi:memory"], - "swap_free": ["Swap free", "GiB", "mdi:memory"], - "processor_load": ["CPU load", "15 min", "mdi:memory"], - "process_running": ["Running", "Count", "mdi:memory"], - "process_total": ["Total", "Count", "mdi:memory"], - "process_thread": ["Thread", "Count", "mdi:memory"], - "process_sleeping": ["Sleeping", "Count", "mdi:memory"], - "cpu_use_percent": ["CPU used", "%", "mdi:memory"], - "cpu_temp": ["CPU Temp", TEMP_CELSIUS, "mdi:thermometer"], - "docker_active": ["Containers active", "", "mdi:docker"], - "docker_cpu_use": ["Containers CPU used", "%", "mdi:docker"], - "docker_memory_use": ["Containers RAM used", "MiB", "mdi:docker"], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_SSL, default=False): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, - vol.Optional(CONF_RESOURCES, default=["disk_use"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] - ), - vol.Optional(CONF_VERSION, default=DEFAULT_VERSION): vol.In([2, 3]), - } -) - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Glances sensors is done through async_setup_entry.""" + pass + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Glances sensors.""" - from glances_api import Glances - - name = config[CONF_NAME] - host = config[CONF_HOST] - port = config[CONF_PORT] - version = config[CONF_VERSION] - var_conf = config[CONF_RESOURCES] - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - ssl = config[CONF_SSL] - verify_ssl = config[CONF_VERIFY_SSL] - - session = async_get_clientsession(hass, verify_ssl) - glances = GlancesData( - Glances( - hass.loop, - session, - host=host, - port=port, - version=version, - username=username, - password=password, - ssl=ssl, - ) - ) - - await glances.async_update() - - if glances.api.data is None: - raise PlatformNotReady + glances_data = hass.data[DOMAIN][config_entry.entry_id] + name = config_entry.data[CONF_NAME] dev = [] - for resource in var_conf: - dev.append(GlancesSensor(glances, name, resource)) + for sensor_type in SENSOR_TYPES: + dev.append( + GlancesSensor(glances_data, name, SENSOR_TYPES[sensor_type][0], sensor_type) + ) async_add_entities(dev, True) @@ -116,9 +33,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class GlancesSensor(Entity): """Implementation of a Glances sensor.""" - def __init__(self, glances, name, sensor_type): + def __init__(self, glances_data, name, sensor_name, sensor_type): """Initialize the sensor.""" - self.glances = glances + self.glances_data = glances_data + self._sensor_name = sensor_name self._name = name self.type = sensor_type self._state = None @@ -127,7 +45,12 @@ class GlancesSensor(Entity): @property def name(self): """Return the name of the sensor.""" - return "{} {}".format(self._name, SENSOR_TYPES[self.type][0]) + return f"{self._name} {self._sensor_name}" + + @property + def unique_id(self): + """Set unique_id for sensor.""" + return f"{self.glances_data.host}-{self.name}" @property def icon(self): @@ -142,17 +65,31 @@ class GlancesSensor(Entity): @property def available(self): """Could the device be accessed during the last update call.""" - return self.glances.available + return self.glances_data.available @property def state(self): """Return the state of the resources.""" return self._state + @property + def should_poll(self): + """Return the polling requirement for this sensor.""" + return False + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + async_dispatcher_connect( + self.hass, DATA_UPDATED, self._schedule_immediate_update + ) + + @callback + def _schedule_immediate_update(self): + self.async_schedule_update_ha_state(True) + async def async_update(self): """Get the latest data from REST API.""" - await self.glances.async_update() - value = self.glances.api.data + value = self.glances_data.api.data if value is not None: if self.type == "disk_use_percent": @@ -249,24 +186,3 @@ class GlancesSensor(Entity): self._state = round(mem_use / 1024 ** 2, 1) except KeyError: self._state = STATE_UNAVAILABLE - - -class GlancesData: - """The class for handling the data retrieval.""" - - def __init__(self, api): - """Initialize the data object.""" - self.api = api - self.available = True - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """Get the latest data from the Glances REST API.""" - from glances_api.exceptions import GlancesApiError - - try: - await self.api.get_data() - self.available = True - except GlancesApiError: - _LOGGER.error("Unable to fetch data from Glances") - self.available = False diff --git a/homeassistant/components/glances/strings.json b/homeassistant/components/glances/strings.json new file mode 100644 index 00000000000..1bd7275daef --- /dev/null +++ b/homeassistant/components/glances/strings.json @@ -0,0 +1,37 @@ +{ + "config": { + "title": "Glances", + "step": { + "user": { + "title": "Setup Glances", + "data": { + "name": "Name", + "host": "Host", + "username": "Username", + "password": "Password", + "port": "Port", + "version": "Glances API Version (2 or 3)", + "ssl": "Use SSL/TLS to connect to the Glances system", + "verify_ssl": "Verify the certification of the system" + } + } + }, + "error": { + "cannot_connect": "Unable to connect to host", + "wrong_version": "Version not supported (2 or 3 only)" + }, + "abort": { + "already_configured": "Host is already configured." + } + }, + "options": { + "step": { + "init": { + "description": "Configure options for Glances", + "data": { + "scan_interval": "Update frequency" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 664e83fba33..60aa610ec07 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -22,6 +22,7 @@ FLOWS = [ "esphome", "geofency", "geonetnz_quakes", + "glances", "gpslogger", "hangouts", "heos", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7912de963ab..754a7d72a64 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -208,6 +208,9 @@ georss_qld_bushfire_alert_client==0.3 # homeassistant.components.nmap_tracker getmac==0.8.1 +# homeassistant.components.glances +glances_api==0.2.0 + # homeassistant.components.google google-api-python-client==1.6.4 diff --git a/tests/components/glances/__init__.py b/tests/components/glances/__init__.py new file mode 100644 index 00000000000..488265f970b --- /dev/null +++ b/tests/components/glances/__init__.py @@ -0,0 +1 @@ +"""Tests for Glances.""" diff --git a/tests/components/glances/test_config_flow.py b/tests/components/glances/test_config_flow.py new file mode 100644 index 00000000000..e5be52e6b33 --- /dev/null +++ b/tests/components/glances/test_config_flow.py @@ -0,0 +1,102 @@ +"""Tests for Glances config flow.""" +from unittest.mock import patch + +from glances_api import Glances + +from homeassistant.components.glances import config_flow +from homeassistant.components.glances.const import DOMAIN +from homeassistant.const import CONF_SCAN_INTERVAL + +from tests.common import MockConfigEntry, mock_coro + +NAME = "Glances" +HOST = "0.0.0.0" +USERNAME = "username" +PASSWORD = "password" +PORT = 61208 +VERSION = 3 +SCAN_INTERVAL = 10 + +DEMO_USER_INPUT = { + "name": NAME, + "host": HOST, + "username": USERNAME, + "password": PASSWORD, + "version": VERSION, + "port": PORT, + "ssl": False, + "verify_ssl": True, +} + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.GlancesFlowHandler() + flow.hass = hass + return flow + + +async def test_form(hass): + """Test config entry configured successfully.""" + flow = init_config_flow(hass) + + with patch("glances_api.Glances"), patch.object( + Glances, "get_data", return_value=mock_coro() + ): + + result = await flow.async_step_user(DEMO_USER_INPUT) + + assert result["type"] == "create_entry" + assert result["title"] == NAME + assert result["data"] == DEMO_USER_INPUT + + +async def test_form_cannot_connect(hass): + """Test to return error if we cannot connect.""" + flow = init_config_flow(hass) + + with patch("glances_api.Glances"): + result = await flow.async_step_user(DEMO_USER_INPUT) + + assert result["type"] == "form" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_form_wrong_version(hass): + """Test to check if wrong version is entered.""" + flow = init_config_flow(hass) + + user_input = DEMO_USER_INPUT.copy() + user_input.update(version=1) + result = await flow.async_step_user(user_input) + + assert result["type"] == "form" + assert result["errors"] == {"version": "wrong_version"} + + +async def test_form_already_configured(hass): + """Test host is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} + ) + entry.add_to_hass(hass) + + flow = init_config_flow(hass) + result = await flow.async_step_user(DEMO_USER_INPUT) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_options(hass): + """Test options for Glances.""" + entry = MockConfigEntry( + domain=DOMAIN, data=DEMO_USER_INPUT, options={CONF_SCAN_INTERVAL: 60} + ) + entry.add_to_hass(hass) + flow = init_config_flow(hass) + options_flow = flow.async_get_options_flow(entry) + + result = await options_flow.async_step_init({CONF_SCAN_INTERVAL: 10}) + assert result["type"] == "create_entry" + assert result["data"][CONF_SCAN_INTERVAL] == 10 From 9fa99eaea905cf9ae7f1c20976cf86157ba39ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:21:40 -0300 Subject: [PATCH 523/639] Move imports in konnected component (#28009) --- .../components/konnected/__init__.py | 59 +++++++++---------- .../components/konnected/handlers.py | 8 +-- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 4cc872fb78b..624d359e154 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -4,59 +4,58 @@ import hmac import json import logging -import voluptuous as vol - from aiohttp.hdrs import AUTHORIZATION from aiohttp.web import Request, Response +import konnected +import voluptuous as vol from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA from homeassistant.components.discovery import SERVICE_KONNECTED from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_STATE, + CONF_ACCESS_TOKEN, + CONF_BINARY_SENSORS, + CONF_DEVICES, + CONF_HOST, + CONF_ID, + CONF_NAME, + CONF_PIN, + CONF_PORT, + CONF_SENSORS, + CONF_SWITCHES, + CONF_TYPE, + CONF_ZONE, EVENT_HOMEASSISTANT_START, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, - CONF_DEVICES, - CONF_BINARY_SENSORS, - CONF_SENSORS, - CONF_SWITCHES, - CONF_HOST, - CONF_PORT, - CONF_ID, - CONF_NAME, - CONF_TYPE, - CONF_PIN, - CONF_ZONE, - CONF_ACCESS_TOKEN, - ATTR_ENTITY_ID, - ATTR_STATE, STATE_ON, ) +from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers import discovery -from homeassistant.helpers import config_validation as cv from .const import ( CONF_ACTIVATION, CONF_API_HOST, + CONF_BLINK, + CONF_DHT_SENSORS, + CONF_DISCOVERY, + CONF_DS18B20_SENSORS, + CONF_INVERSE, CONF_MOMENTARY, CONF_PAUSE, CONF_POLL_INTERVAL, CONF_REPEAT, - CONF_INVERSE, - CONF_BLINK, - CONF_DISCOVERY, - CONF_DHT_SENSORS, - CONF_DS18B20_SENSORS, DOMAIN, - STATE_LOW, - STATE_HIGH, - PIN_TO_ZONE, - ZONE_TO_PIN, ENDPOINT_ROOT, - UPDATE_ENDPOINT, + PIN_TO_ZONE, SIGNAL_SENSOR_UPDATE, + STATE_HIGH, + STATE_LOW, + UPDATE_ENDPOINT, + ZONE_TO_PIN, ) from .handlers import HANDLERS @@ -141,8 +140,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Konnected platform.""" - import konnected - cfg = config.get(DOMAIN) if cfg is None: cfg = {} @@ -336,8 +333,6 @@ class DiscoveredDevice: self.host = host self.port = port - import konnected - self.client = konnected.Client(host, str(port)) self.status = self.client.get_status() diff --git a/homeassistant/components/konnected/handlers.py b/homeassistant/components/konnected/handlers.py index a355cabba56..a8914853e84 100644 --- a/homeassistant/components/konnected/handlers.py +++ b/homeassistant/components/konnected/handlers.py @@ -1,16 +1,16 @@ """Handle Konnected messages.""" import logging -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.util import decorator from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_STATE, - DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, ) +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.util import decorator -from .const import CONF_INVERSE, SIGNAL_SENSOR_UPDATE, SIGNAL_DS18B20_NEW +from .const import CONF_INVERSE, SIGNAL_DS18B20_NEW, SIGNAL_SENSOR_UPDATE _LOGGER = logging.getLogger(__name__) HANDLERS = decorator.Registry() From 1a68591fe6d7398fb1d33ebcb35a4b40a9fe1346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:22:13 -0300 Subject: [PATCH 524/639] Move imports in juicenet component (#28006) --- homeassistant/components/juicenet/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index a176236b224..207dac7836a 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -1,12 +1,13 @@ """Support for Juicenet cloud.""" import logging +import pyjuicenet import voluptuous as vol -from homeassistant.helpers import discovery from homeassistant.const import CONF_ACCESS_TOKEN -from homeassistant.helpers.entity import Entity +from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -20,8 +21,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Juicenet component.""" - import pyjuicenet - hass.data[DOMAIN] = {} access_token = config[DOMAIN].get(CONF_ACCESS_TOKEN) From d5799d020aaaaa547e27f628c11ee11b3573c400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:22:34 -0300 Subject: [PATCH 525/639] Move imports in insteon component (#28001) --- homeassistant/components/insteon/__init__.py | 73 +++++++++----------- 1 file changed, 33 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index 4015d472ce8..11f224dbfcc 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -3,6 +3,38 @@ import collections import logging from typing import Dict +import insteonplm +from insteonplm.devices import ALDBStatus +from insteonplm.states.cover import Cover +from insteonplm.states.dimmable import ( + DimmableKeypadA, + DimmableRemote, + DimmableSwitch, + DimmableSwitch_Fan, +) +from insteonplm.states.onOff import ( + OnOffKeypad, + OnOffKeypadA, + OnOffSwitch, + OnOffSwitch_OutletBottom, + OnOffSwitch_OutletTop, + OpenClosedRelay, +) +from insteonplm.states.sensor import ( + IoLincSensor, + LeakSensorDryWet, + OnOffSensor, + SmokeCO2Sensor, + VariableSensor, +) +from insteonplm.states.x10 import ( + X10AllLightsOffSensor, + X10AllLightsOnSensor, + X10AllUnitsOffSensor, + X10DimmableSwitch, + X10OnOffSensor, + X10OnOffSwitch, +) import voluptuous as vol from homeassistant.const import ( @@ -16,8 +48,8 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import discovery -from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -240,8 +272,6 @@ STATE_NAME_LABEL_MAP = { async def async_setup(hass, config): """Set up the connection to the modem.""" - import insteonplm - ipdb = IPDB() insteon_modem = None @@ -496,41 +526,6 @@ class IPDB: def __init__(self): """Create the INSTEON Product Database (IPDB).""" - from insteonplm.states.cover import Cover - - from insteonplm.states.onOff import ( - OnOffSwitch, - OnOffSwitch_OutletTop, - OnOffSwitch_OutletBottom, - OpenClosedRelay, - OnOffKeypadA, - OnOffKeypad, - ) - - from insteonplm.states.dimmable import ( - DimmableSwitch, - DimmableSwitch_Fan, - DimmableRemote, - DimmableKeypadA, - ) - - from insteonplm.states.sensor import ( - VariableSensor, - OnOffSensor, - SmokeCO2Sensor, - IoLincSensor, - LeakSensorDryWet, - ) - - from insteonplm.states.x10 import ( - X10DimmableSwitch, - X10OnOffSwitch, - X10OnOffSensor, - X10AllUnitsOffSensor, - X10AllLightsOnSensor, - X10AllLightsOffSensor, - ) - self.states = [ State(Cover, "cover"), State(OnOffSwitch_OutletTop, "switch"), @@ -685,8 +680,6 @@ class InsteonEntity(Entity): def print_aldb_to_log(aldb): """Print the All-Link Database to the log file.""" - from insteonplm.devices import ALDBStatus - _LOGGER.info("ALDB load status is %s", aldb.status.name) if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]: _LOGGER.warning("Device All-Link database not loaded") From 36ff790a39f27bd94a85f303351f05160126b2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:22:52 -0300 Subject: [PATCH 526/639] Move imports in greenwave component (#27998) --- homeassistant/components/greenwave/light.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/greenwave/light.py b/homeassistant/components/greenwave/light.py index 5a6ce2c51c2..8b85de598b0 100644 --- a/homeassistant/components/greenwave/light.py +++ b/homeassistant/components/greenwave/light.py @@ -1,7 +1,9 @@ """Support for Greenwave Reality (TCP Connected) lights.""" -import logging from datetime import timedelta +import logging +import os +import greenwavereality as greenwave import voluptuous as vol from homeassistant.components.light import ( @@ -29,9 +31,6 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Greenwave Reality Platform.""" - import greenwavereality as greenwave - import os - host = config.get(CONF_HOST) tokenfile = hass.config.path(".greenwave") if config.get(CONF_VERSION) == 3: @@ -60,8 +59,6 @@ class GreenwaveLight(Light): def __init__(self, light, host, token, gatewaydata): """Initialize a Greenwave Reality Light.""" - import greenwavereality as greenwave - self._did = int(light["did"]) self._name = light["name"] self._state = int(light["state"]) @@ -98,22 +95,16 @@ class GreenwaveLight(Light): def turn_on(self, **kwargs): """Instruct the light to turn on.""" - import greenwavereality as greenwave - temp_brightness = int((kwargs.get(ATTR_BRIGHTNESS, 255) / 255) * 100) greenwave.set_brightness(self._host, self._did, temp_brightness, self._token) greenwave.turn_on(self._host, self._did, self._token) def turn_off(self, **kwargs): """Instruct the light to turn off.""" - import greenwavereality as greenwave - greenwave.turn_off(self._host, self._did, self._token) def update(self): """Fetch new state data for this light.""" - import greenwavereality as greenwave - self._gatewaydata.update() bulbs = self._gatewaydata.greenwave @@ -128,8 +119,6 @@ class GatewayData: def __init__(self, host, token): """Initialize the data object.""" - import greenwavereality as greenwave - self._host = host self._token = token self._greenwave = greenwave.grab_bulbs(host, token) @@ -142,7 +131,5 @@ class GatewayData: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data from the gateway.""" - import greenwavereality as greenwave - self._greenwave = greenwave.grab_bulbs(self._host, self._token) return self._greenwave From 322399c0af4e759b79c3f1581c4e7506e350636e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:24:06 -0300 Subject: [PATCH 527/639] Move imports in kira component (#28007) --- homeassistant/components/kira/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/kira/__init__.py b/homeassistant/components/kira/__init__.py index 77f91a50dfa..8948fbd0b8f 100644 --- a/homeassistant/components/kira/__init__.py +++ b/homeassistant/components/kira/__init__.py @@ -2,11 +2,13 @@ import logging import os +import pykira import voluptuous as vol from voluptuous.error import Error as VoluptuousError import yaml from homeassistant.const import ( + CONF_CODE, CONF_DEVICE, CONF_HOST, CONF_NAME, @@ -15,7 +17,6 @@ from homeassistant.const import ( CONF_TYPE, EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, - CONF_CODE, ) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv @@ -93,8 +94,6 @@ def load_codes(path): def setup(hass, config): """Set up the KIRA component.""" - import pykira - sensors = config.get(DOMAIN, {}).get(CONF_SENSORS, []) remotes = config.get(DOMAIN, {}).get(CONF_REMOTES, []) # If no sensors or remotes were specified, add a sensor From 5fb3f6038bee56b979c5ddd7a5459caeeedc7509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:24:26 -0300 Subject: [PATCH 528/639] Move imports in itach component (#28005) --- homeassistant/components/itach/remote.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/itach/remote.py b/homeassistant/components/itach/remote.py index 9895b54a50d..5390111890c 100644 --- a/homeassistant/components/itach/remote.py +++ b/homeassistant/components/itach/remote.py @@ -1,19 +1,20 @@ """Support for iTach IR devices.""" import logging +import pyitachip2ir import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components import remote -from homeassistant.const import ( - DEVICE_DEFAULT_NAME, - CONF_NAME, - CONF_MAC, - CONF_HOST, - CONF_PORT, - CONF_DEVICES, -) from homeassistant.components.remote import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_MAC, + CONF_NAME, + CONF_PORT, + DEVICE_DEFAULT_NAME, +) +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -55,8 +56,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the ITach connection and devices.""" - import pyitachip2ir - itachip2ir = pyitachip2ir.ITachIP2IR( config.get(CONF_MAC), config.get(CONF_HOST), int(config.get(CONF_PORT)) ) From 6844d203a14bdf1057ae81e8c8b78b8a33d828cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 05:41:20 -0300 Subject: [PATCH 529/639] Move imports in gpsd component (#27997) --- homeassistant/components/gpsd/sensor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/gpsd/sensor.py b/homeassistant/components/gpsd/sensor.py index 197e424ce86..8696dde72cb 100644 --- a/homeassistant/components/gpsd/sensor.py +++ b/homeassistant/components/gpsd/sensor.py @@ -1,6 +1,8 @@ """Support for GPSD.""" import logging +import socket +from gps3.agps3threaded import AGPS3mechanism import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -9,11 +11,11 @@ from homeassistant.const import ( ATTR_LONGITUDE, ATTR_MODE, CONF_HOST, - CONF_PORT, CONF_NAME, + CONF_PORT, ) -from homeassistant.helpers.entity import Entity import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -50,7 +52,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # except GPSError: # _LOGGER.warning('Not able to connect to GPSD') # return False - import socket sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: @@ -69,8 +70,6 @@ class GpsdSensor(Entity): def __init__(self, hass, name, host, port): """Initialize the GPSD sensor.""" - from gps3.agps3threaded import AGPS3mechanism - self.hass = hass self._name = name self._host = host From d1fcc5762b3456b8698c38dc96e789a1d37c4a04 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 21 Oct 2019 03:44:07 -0500 Subject: [PATCH 530/639] Make dispatch signals unique per server (#28029) --- homeassistant/components/plex/const.py | 4 ++-- homeassistant/components/plex/media_player.py | 4 +++- homeassistant/components/plex/sensor.py | 4 +++- homeassistant/components/plex/server.py | 12 ++++++++++-- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index c576f1d6a59..d3a3a866361 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -17,9 +17,9 @@ PLEX_CONFIG_FILE = "plex.conf" PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" PLEX_SERVER_CONFIG = "server_config" -PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal" +PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" -PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal" +PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" CONF_SERVER = "server" CONF_SERVER_IDENTIFIER = "server_id" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 4a48950a67c..32bf7b65fff 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -62,7 +62,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass, config_entry, async_add_entities, server_id, new_entities ) - unsub = async_dispatcher_connect(hass, PLEX_NEW_MP_SIGNAL, async_new_media_players) + unsub = async_dispatcher_connect( + hass, PLEX_NEW_MP_SIGNAL.format(server_id), async_new_media_players + ) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 3cde2adb8f4..287f0edf39a 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -49,7 +49,9 @@ class PlexSensor(Entity): """Run when about to be added to hass.""" server_id = self._server.machine_identifier unsub = async_dispatcher_connect( - self.hass, PLEX_UPDATE_SENSOR_SIGNAL, self.async_refresh_sensor + self.hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(server_id), + self.async_refresh_sensor, ) self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 128bcdd45c6..d7825ae82c3 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -147,9 +147,17 @@ class PlexServer: self.refresh_entity(client_id, None, None) if new_entity_configs: - dispatcher_send(self._hass, PLEX_NEW_MP_SIGNAL, new_entity_configs) + dispatcher_send( + self._hass, + PLEX_NEW_MP_SIGNAL.format(self.machine_identifier), + new_entity_configs, + ) - dispatcher_send(self._hass, PLEX_UPDATE_SENSOR_SIGNAL, sessions) + dispatcher_send( + self._hass, + PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier), + sessions, + ) @property def friendly_name(self): From 1e832dc9ec57c2523441f3f45756f39006571d70 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Mon, 21 Oct 2019 01:59:58 -0700 Subject: [PATCH 531/639] Bump teslajsonpy and add update switch (#27957) * bump teslajsonpy to 0.0.26 breaking change * add update switch to tesla * bump requirements_all.txt for teslajsonpy * address requested style changes * fix bug where update switch not loaded --- homeassistant/components/tesla/manifest.json | 8 +--- homeassistant/components/tesla/switch.py | 39 ++++++++++++++++++-- requirements_all.txt | 2 +- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 4071178c7c3..87d76c16f05 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -2,11 +2,7 @@ "domain": "tesla", "name": "Tesla", "documentation": "https://www.home-assistant.io/integrations/tesla", - "requirements": [ - "teslajsonpy==0.0.25" - ], + "requirements": ["teslajsonpy==0.0.26"], "dependencies": [], - "codeowners": [ - "@zabuldon" - ] + "codeowners": ["@zabuldon"] } diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 19ac76018ff..985194f87b2 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -11,11 +11,12 @@ _LOGGER = logging.getLogger(__name__) def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tesla switch platform.""" - controller = hass.data[TESLA_DOMAIN]["devices"]["controller"] + controller = hass.data[TESLA_DOMAIN]["controller"] devices = [] for device in hass.data[TESLA_DOMAIN]["devices"]["switch"]: if device.bin_type == 0x8: devices.append(ChargerSwitch(device, controller)) + devices.append(UpdateSwitch(device, controller)) elif device.bin_type == 0x9: devices.append(RangeSwitch(device, controller)) add_entities(devices, True) @@ -72,10 +73,42 @@ class RangeSwitch(TeslaDevice, SwitchDevice): @property def is_on(self): """Get whether the switch is in on state.""" - return self._state == STATE_ON + return self._state def update(self): """Update the state of the switch.""" _LOGGER.debug("Updating state for: %s", self._name) self.tesla_device.update() - self._state = STATE_ON if self.tesla_device.is_maxrange() else STATE_OFF + self._state = bool(self.tesla_device.is_maxrange()) + + +class UpdateSwitch(TeslaDevice, SwitchDevice): + """Representation of a Tesla update switch.""" + + def __init__(self, tesla_device, controller): + """Initialise of the switch.""" + self._state = None + super().__init__(tesla_device, controller) + self._name = self._name.replace("charger", "update") + self.tesla_id = self.tesla_id.replace("charger", "update") + + def turn_on(self, **kwargs): + """Send the on command.""" + _LOGGER.debug("Enable updates: %s %s", self._name, self.tesla_device.id()) + self.controller.set_updates(self.tesla_device.id(), True) + + def turn_off(self, **kwargs): + """Send the off command.""" + _LOGGER.debug("Disable updates: %s %s", self._name, self.tesla_device.id()) + self.controller.set_updates(self.tesla_device.id(), False) + + @property + def is_on(self): + """Get whether the switch is in on state.""" + return self._state + + def update(self): + """Update the state of the switch.""" + car_id = self.tesla_device.id() + _LOGGER.debug("Updating state for: %s %s", self._name, car_id) + self._state = bool(self.controller.get_updates(car_id)) diff --git a/requirements_all.txt b/requirements_all.txt index 3c649e91756..81e31c81817 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1876,7 +1876,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.0.25 +teslajsonpy==0.0.26 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 From 6c48abcaa55c36733c7c2db44a56ae6c806a9948 Mon Sep 17 00:00:00 2001 From: Matthew Turney Date: Mon, 21 Oct 2019 04:20:18 -0500 Subject: [PATCH 532/639] rest_command component should support PATCH method (#27989) Without PATCH the rest_command component lacks full RESTful API support. --- .../components/rest_command/__init__.py | 2 +- tests/components/rest_command/test_init.py | 37 ++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index 1607000e8d9..223dc7da7cc 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -28,7 +28,7 @@ DEFAULT_TIMEOUT = 10 DEFAULT_METHOD = "get" DEFAULT_VERIFY_SSL = True -SUPPORT_REST_METHODS = ["get", "post", "put", "delete"] +SUPPORT_REST_METHODS = ["get", "patch", "post", "put", "delete"] CONF_CONTENT_TYPE = "content_type" diff --git a/tests/components/rest_command/test_init.py b/tests/components/rest_command/test_init.py index 2f50f34f140..b7ac5a4be8a 100644 --- a/tests/components/rest_command/test_init.py +++ b/tests/components/rest_command/test_init.py @@ -51,6 +51,7 @@ class TestRestCommandComponent: self.config = { rc.DOMAIN: { "get_test": {"url": self.url, "method": "get"}, + "patch_test": {"url": self.url, "method": "patch"}, "post_test": {"url": self.url, "method": "post"}, "put_test": {"url": self.url, "method": "put"}, "delete_test": {"url": self.url, "method": "delete"}, @@ -65,7 +66,7 @@ class TestRestCommandComponent: def test_setup_tests(self): """Set up test config and test it.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) assert self.hass.services.has_service(rc.DOMAIN, "get_test") @@ -75,7 +76,7 @@ class TestRestCommandComponent: def test_rest_command_timeout(self, aioclient_mock): """Call a rest command with timeout.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, exc=asyncio.TimeoutError()) @@ -87,7 +88,7 @@ class TestRestCommandComponent: def test_rest_command_aiohttp_error(self, aioclient_mock): """Call a rest command with aiohttp exception.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, exc=aiohttp.ClientError()) @@ -99,7 +100,7 @@ class TestRestCommandComponent: def test_rest_command_http_error(self, aioclient_mock): """Call a rest command with status code 400.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, status=400) @@ -114,7 +115,7 @@ class TestRestCommandComponent: data = {"username": "test", "password": "123456"} self.config[rc.DOMAIN]["get_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, content=b"success") @@ -129,7 +130,7 @@ class TestRestCommandComponent: data = {"payload": "test"} self.config[rc.DOMAIN]["post_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.post(self.url, content=b"success") @@ -142,7 +143,7 @@ class TestRestCommandComponent: def test_rest_command_get(self, aioclient_mock): """Call a rest command with get.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.get(self.url, content=b"success") @@ -154,7 +155,7 @@ class TestRestCommandComponent: def test_rest_command_delete(self, aioclient_mock): """Call a rest command with delete.""" - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.delete(self.url, content=b"success") @@ -164,12 +165,28 @@ class TestRestCommandComponent: assert len(aioclient_mock.mock_calls) == 1 + def test_rest_command_patch(self, aioclient_mock): + """Call a rest command with patch.""" + data = {"payload": "data"} + self.config[rc.DOMAIN]["patch_test"].update(data) + + with assert_setup_component(5): + setup_component(self.hass, rc.DOMAIN, self.config) + + aioclient_mock.patch(self.url, content=b"success") + + self.hass.services.call(rc.DOMAIN, "patch_test", {}) + self.hass.block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[0][2] == b"data" + def test_rest_command_post(self, aioclient_mock): """Call a rest command with post.""" data = {"payload": "data"} self.config[rc.DOMAIN]["post_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.post(self.url, content=b"success") @@ -185,7 +202,7 @@ class TestRestCommandComponent: data = {"payload": "data"} self.config[rc.DOMAIN]["put_test"].update(data) - with assert_setup_component(4): + with assert_setup_component(5): setup_component(self.hass, rc.DOMAIN, self.config) aioclient_mock.put(self.url, content=b"success") From ea1401d0b6b5bfa26cf4057275fc43b5bd079f92 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 21 Oct 2019 13:48:08 +0200 Subject: [PATCH 533/639] Upgrade discord.py to 1.2.4 (#28054) --- homeassistant/components/discord/__init__.py | 2 +- homeassistant/components/discord/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/discord/__init__.py b/homeassistant/components/discord/__init__.py index a3cd87bc895..67b9f1b39ba 100644 --- a/homeassistant/components/discord/__init__.py +++ b/homeassistant/components/discord/__init__.py @@ -1 +1 @@ -"""The discord component.""" +"""The discord integration.""" diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 50c03bad25d..d00d26d2b5e 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -3,7 +3,7 @@ "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", "requirements": [ - "discord.py==1.2.3" + "discord.py==1.2.4" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 81e31c81817..881bbd119d3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -408,7 +408,7 @@ directpy==0.5 discogs_client==2.2.1 # homeassistant.components.discord -discord.py==1.2.3 +discord.py==1.2.4 # homeassistant.components.updater distro==1.4.0 From a05144bb8b45a8e0ed0f5daf7dbd48460e89f161 Mon Sep 17 00:00:00 2001 From: Ties de Kock Date: Mon, 21 Oct 2019 13:52:25 +0200 Subject: [PATCH 534/639] Fix buienradar component and add smoke tests (#27965) * Fixes the buienradar component and add smoke tests * Fix errors due to circular imports after imports were moved. * Add smoke test so this situation will be caught in the future. * Add buienradar.util to coveragerc * Refactor tests to standalone pytest test function style * Add __init__ to buienradar tests --- .coveragerc | 1 + homeassistant/components/buienradar/const.py | 7 + homeassistant/components/buienradar/sensor.py | 213 +--------------- homeassistant/components/buienradar/util.py | 228 ++++++++++++++++++ .../components/buienradar/weather.py | 4 +- tests/components/buienradar/__init__.py | 1 + tests/components/buienradar/test_sensor.py | 26 ++ tests/components/buienradar/test_weather.py | 25 ++ 8 files changed, 294 insertions(+), 211 deletions(-) create mode 100644 homeassistant/components/buienradar/const.py create mode 100644 homeassistant/components/buienradar/util.py create mode 100644 tests/components/buienradar/__init__.py create mode 100644 tests/components/buienradar/test_sensor.py create mode 100644 tests/components/buienradar/test_weather.py diff --git a/.coveragerc b/.coveragerc index 7c35022c355..f40b0c30342 100644 --- a/.coveragerc +++ b/.coveragerc @@ -100,6 +100,7 @@ omit = homeassistant/components/bt_home_hub_5/device_tracker.py homeassistant/components/bt_smarthub/device_tracker.py homeassistant/components/buienradar/sensor.py + homeassistant/components/buienradar/util.py homeassistant/components/buienradar/weather.py homeassistant/components/caldav/calendar.py homeassistant/components/canary/alarm_control_panel.py diff --git a/homeassistant/components/buienradar/const.py b/homeassistant/components/buienradar/const.py new file mode 100644 index 00000000000..b91d2497d77 --- /dev/null +++ b/homeassistant/components/buienradar/const.py @@ -0,0 +1,7 @@ +"""Constants for buienradar component.""" +DEFAULT_TIMEFRAME = 60 + +"""Schedule next call after (minutes).""" +SCHEDULE_OK = 10 +"""When an error occurred, new call after (minutes).""" +SCHEDULE_NOK = 2 diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index 300bcbf2243..5fe97b6fb38 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -1,38 +1,23 @@ """Support for Buienradar.nl weather service.""" -import asyncio -from datetime import datetime, timedelta import logging -import aiohttp -import async_timeout -from buienradar.buienradar import parse_data from buienradar.constants import ( ATTRIBUTION, CONDCODE, CONDITION, - CONTENT, - DATA, DETAILED, EXACT, EXACTNL, FORECAST, - HUMIDITY, IMAGE, MEASURED, - MESSAGE, PRECIPITATION_FORECAST, - PRESSURE, STATIONNAME, - STATUS_CODE, - SUCCESS, - TEMPERATURE, TIMEFRAME, VISIBILITY, - WINDAZIMUTH, WINDGUST, WINDSPEED, ) -from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -44,13 +29,14 @@ from homeassistant.const import ( CONF_NAME, TEMP_CELSIUS, ) -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util import dt as dt_util -from .weather import DEFAULT_TIMEFRAME + +from .const import DEFAULT_TIMEFRAME +from .util import BrData + _LOGGER = logging.getLogger(__name__) @@ -465,194 +451,3 @@ class BrSensor(Entity): def force_update(self): """Return true for continuous sensors, false for discrete sensors.""" return self._force_update - - -class BrData: - """Get the latest data and updates the states.""" - - def __init__(self, hass, coordinates, timeframe, devices): - """Initialize the data object.""" - self.devices = devices - self.data = {} - self.hass = hass - self.coordinates = coordinates - self.timeframe = timeframe - - async def update_devices(self): - """Update all devices/sensors.""" - if self.devices: - tasks = [] - # Update all devices - for dev in self.devices: - if dev.load_data(self.data): - tasks.append(dev.async_update_ha_state()) - - if tasks: - await asyncio.wait(tasks) - - async def schedule_update(self, minute=1): - """Schedule an update after minute minutes.""" - _LOGGER.debug("Scheduling next update in %s minutes.", minute) - nxt = dt_util.utcnow() + timedelta(minutes=minute) - async_track_point_in_utc_time(self.hass, self.async_update, nxt) - - async def get_data(self, url): - """Load data from specified url.""" - - _LOGGER.debug("Calling url: %s...", url) - result = {SUCCESS: False, MESSAGE: None} - resp = None - try: - websession = async_get_clientsession(self.hass) - with async_timeout.timeout(10): - resp = await websession.get(url) - - result[STATUS_CODE] = resp.status - result[CONTENT] = await resp.text() - if resp.status == 200: - result[SUCCESS] = True - else: - result[MESSAGE] = "Got http statuscode: %d" % (resp.status) - - return result - except (asyncio.TimeoutError, aiohttp.ClientError) as err: - result[MESSAGE] = "%s" % err - return result - finally: - if resp is not None: - await resp.release() - - async def async_update(self, *_): - """Update the data from buienradar.""" - - content = await self.get_data(JSON_FEED_URL) - - if content.get(SUCCESS) is not True: - # unable to get the data - _LOGGER.warning( - "Unable to retrieve json data from Buienradar." - "(Msg: %s, status: %s,)", - content.get(MESSAGE), - content.get(STATUS_CODE), - ) - # schedule new call - await self.schedule_update(SCHEDULE_NOK) - return - - # rounding coordinates prevents unnecessary redirects/calls - lat = self.coordinates[CONF_LATITUDE] - lon = self.coordinates[CONF_LONGITUDE] - rainurl = json_precipitation_forecast_url(lat, lon) - raincontent = await self.get_data(rainurl) - - if raincontent.get(SUCCESS) is not True: - # unable to get the data - _LOGGER.warning( - "Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)", - raincontent.get(MESSAGE), - raincontent.get(STATUS_CODE), - ) - # schedule new call - await self.schedule_update(SCHEDULE_NOK) - return - - result = parse_data( - content.get(CONTENT), - raincontent.get(CONTENT), - self.coordinates[CONF_LATITUDE], - self.coordinates[CONF_LONGITUDE], - self.timeframe, - False, - ) - - _LOGGER.debug("Buienradar parsed data: %s", result) - if result.get(SUCCESS) is not True: - if int(datetime.now().strftime("%H")) > 0: - _LOGGER.warning( - "Unable to parse data from Buienradar." "(Msg: %s)", - result.get(MESSAGE), - ) - await self.schedule_update(SCHEDULE_NOK) - return - - self.data = result.get(DATA) - await self.update_devices() - await self.schedule_update(SCHEDULE_OK) - - @property - def attribution(self): - """Return the attribution.""" - - return self.data.get(ATTRIBUTION) - - @property - def stationname(self): - """Return the name of the selected weatherstation.""" - - return self.data.get(STATIONNAME) - - @property - def condition(self): - """Return the condition.""" - - return self.data.get(CONDITION) - - @property - def temperature(self): - """Return the temperature, or None.""" - - try: - return float(self.data.get(TEMPERATURE)) - except (ValueError, TypeError): - return None - - @property - def pressure(self): - """Return the pressure, or None.""" - - try: - return float(self.data.get(PRESSURE)) - except (ValueError, TypeError): - return None - - @property - def humidity(self): - """Return the humidity, or None.""" - - try: - return int(self.data.get(HUMIDITY)) - except (ValueError, TypeError): - return None - - @property - def visibility(self): - """Return the visibility, or None.""" - - try: - return int(self.data.get(VISIBILITY)) - except (ValueError, TypeError): - return None - - @property - def wind_speed(self): - """Return the windspeed, or None.""" - - try: - return float(self.data.get(WINDSPEED)) - except (ValueError, TypeError): - return None - - @property - def wind_bearing(self): - """Return the wind bearing, or None.""" - - try: - return int(self.data.get(WINDAZIMUTH)) - except (ValueError, TypeError): - return None - - @property - def forecast(self): - """Return the forecast data.""" - - return self.data.get(FORECAST) diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py new file mode 100644 index 00000000000..579b3418271 --- /dev/null +++ b/homeassistant/components/buienradar/util.py @@ -0,0 +1,228 @@ +"""Shared utilities for different supported platforms.""" +import asyncio +from datetime import datetime, timedelta +import logging + +import aiohttp +import async_timeout + +from buienradar.buienradar import parse_data +from buienradar.constants import ( + ATTRIBUTION, + CONDITION, + CONTENT, + DATA, + FORECAST, + HUMIDITY, + MESSAGE, + PRESSURE, + STATIONNAME, + STATUS_CODE, + SUCCESS, + TEMPERATURE, + VISIBILITY, + WINDAZIMUTH, + WINDSPEED, +) +from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url + +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import async_track_point_in_utc_time +from homeassistant.util import dt as dt_util + + +from .const import SCHEDULE_OK, SCHEDULE_NOK + + +_LOGGER = logging.getLogger(__name__) + + +class BrData: + """Get the latest data and updates the states.""" + + def __init__(self, hass, coordinates, timeframe, devices): + """Initialize the data object.""" + self.devices = devices + self.data = {} + self.hass = hass + self.coordinates = coordinates + self.timeframe = timeframe + + async def update_devices(self): + """Update all devices/sensors.""" + if self.devices: + tasks = [] + # Update all devices + for dev in self.devices: + if dev.load_data(self.data): + tasks.append(dev.async_update_ha_state()) + + if tasks: + await asyncio.wait(tasks) + + async def schedule_update(self, minute=1): + """Schedule an update after minute minutes.""" + _LOGGER.debug("Scheduling next update in %s minutes.", minute) + nxt = dt_util.utcnow() + timedelta(minutes=minute) + async_track_point_in_utc_time(self.hass, self.async_update, nxt) + + async def get_data(self, url): + """Load data from specified url.""" + _LOGGER.debug("Calling url: %s...", url) + result = {SUCCESS: False, MESSAGE: None} + resp = None + try: + websession = async_get_clientsession(self.hass) + with async_timeout.timeout(10): + resp = await websession.get(url) + + result[STATUS_CODE] = resp.status + result[CONTENT] = await resp.text() + if resp.status == 200: + result[SUCCESS] = True + else: + result[MESSAGE] = "Got http statuscode: %d" % (resp.status) + + return result + except (asyncio.TimeoutError, aiohttp.ClientError) as err: + result[MESSAGE] = "%s" % err + return result + finally: + if resp is not None: + await resp.release() + + async def async_update(self, *_): + """Update the data from buienradar.""" + + content = await self.get_data(JSON_FEED_URL) + + if content.get(SUCCESS) is not True: + # unable to get the data + _LOGGER.warning( + "Unable to retrieve json data from Buienradar." + "(Msg: %s, status: %s,)", + content.get(MESSAGE), + content.get(STATUS_CODE), + ) + # schedule new call + await self.schedule_update(SCHEDULE_NOK) + return + + # rounding coordinates prevents unnecessary redirects/calls + lat = self.coordinates[CONF_LATITUDE] + lon = self.coordinates[CONF_LONGITUDE] + rainurl = json_precipitation_forecast_url(lat, lon) + raincontent = await self.get_data(rainurl) + + if raincontent.get(SUCCESS) is not True: + # unable to get the data + _LOGGER.warning( + "Unable to retrieve raindata from Buienradar." "(Msg: %s, status: %s,)", + raincontent.get(MESSAGE), + raincontent.get(STATUS_CODE), + ) + # schedule new call + await self.schedule_update(SCHEDULE_NOK) + return + + result = parse_data( + content.get(CONTENT), + raincontent.get(CONTENT), + self.coordinates[CONF_LATITUDE], + self.coordinates[CONF_LONGITUDE], + self.timeframe, + False, + ) + + _LOGGER.debug("Buienradar parsed data: %s", result) + if result.get(SUCCESS) is not True: + if int(datetime.now().strftime("%H")) > 0: + _LOGGER.warning( + "Unable to parse data from Buienradar." "(Msg: %s)", + result.get(MESSAGE), + ) + await self.schedule_update(SCHEDULE_NOK) + return + + self.data = result.get(DATA) + await self.update_devices() + await self.schedule_update(SCHEDULE_OK) + + @property + def attribution(self): + """Return the attribution.""" + + return self.data.get(ATTRIBUTION) + + @property + def stationname(self): + """Return the name of the selected weatherstation.""" + + return self.data.get(STATIONNAME) + + @property + def condition(self): + """Return the condition.""" + + return self.data.get(CONDITION) + + @property + def temperature(self): + """Return the temperature, or None.""" + + try: + return float(self.data.get(TEMPERATURE)) + except (ValueError, TypeError): + return None + + @property + def pressure(self): + """Return the pressure, or None.""" + + try: + return float(self.data.get(PRESSURE)) + except (ValueError, TypeError): + return None + + @property + def humidity(self): + """Return the humidity, or None.""" + + try: + return int(self.data.get(HUMIDITY)) + except (ValueError, TypeError): + return None + + @property + def visibility(self): + """Return the visibility, or None.""" + + try: + return int(self.data.get(VISIBILITY)) + except (ValueError, TypeError): + return None + + @property + def wind_speed(self): + """Return the windspeed, or None.""" + + try: + return float(self.data.get(WINDSPEED)) + except (ValueError, TypeError): + return None + + @property + def wind_bearing(self): + """Return the wind bearing, or None.""" + + try: + return int(self.data.get(WINDAZIMUTH)) + except (ValueError, TypeError): + return None + + @property + def forecast(self): + """Return the forecast data.""" + + return self.data.get(FORECAST) diff --git a/homeassistant/components/buienradar/weather.py b/homeassistant/components/buienradar/weather.py index 745bf12ffd8..c95e57807c4 100644 --- a/homeassistant/components/buienradar/weather.py +++ b/homeassistant/components/buienradar/weather.py @@ -28,13 +28,13 @@ from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_C from homeassistant.helpers import config_validation as cv # Reuse data and API logic from the sensor implementation -from .sensor import BrData +from .util import BrData +from .const import DEFAULT_TIMEFRAME _LOGGER = logging.getLogger(__name__) DATA_CONDITION = "buienradar_condition" -DEFAULT_TIMEFRAME = 60 CONF_FORECAST = "forecast" diff --git a/tests/components/buienradar/__init__.py b/tests/components/buienradar/__init__.py new file mode 100644 index 00000000000..15cdd8646d2 --- /dev/null +++ b/tests/components/buienradar/__init__.py @@ -0,0 +1 @@ +"""Tests for the buienradar component.""" diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py new file mode 100644 index 00000000000..c1569e4576b --- /dev/null +++ b/tests/components/buienradar/test_sensor.py @@ -0,0 +1,26 @@ +"""The tests for the Buienradar sensor platform.""" +from homeassistant.setup import async_setup_component +from homeassistant.components import sensor + + +CONDITIONS = ["stationname", "temperature"] +BASE_CONFIG = { + "sensor": [ + { + "platform": "buienradar", + "name": "volkel", + "latitude": 51.65, + "longitude": 5.7, + "monitored_conditions": CONDITIONS, + } + ] +} + + +async def test_smoke_test_setup_component(hass): + """Smoke test for successfully set-up with default config.""" + assert await async_setup_component(hass, sensor.DOMAIN, BASE_CONFIG) + + for cond in CONDITIONS: + state = hass.states.get(f"sensor.volkel_{cond}") + assert state.state == "unknown" diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py new file mode 100644 index 00000000000..1a8c94e1712 --- /dev/null +++ b/tests/components/buienradar/test_weather.py @@ -0,0 +1,25 @@ +"""The tests for the buienradar weather component.""" +from homeassistant.components import weather +from homeassistant.setup import async_setup_component + + +# Example config snippet from documentation. +BASE_CONFIG = { + "weather": [ + { + "platform": "buienradar", + "name": "volkel", + "latitude": 51.65, + "longitude": 5.7, + "forecast": True, + } + ] +} + + +async def test_smoke_test_setup_component(hass): + """Smoke test for successfully set-up with default config.""" + assert await async_setup_component(hass, weather.DOMAIN, BASE_CONFIG) + + state = hass.states.get("weather.volkel") + assert state.state == "unknown" From 6ba437d83ac5db09833204fbb4fc0016993934bd Mon Sep 17 00:00:00 2001 From: Pascal Roeleven Date: Mon, 21 Oct 2019 13:56:03 +0200 Subject: [PATCH 535/639] Code cleanup for orangepi_gpio (#27958) * Code cleanup for orangepi_gpio * Move constants to const.py * Use async wherever possible * Remove obsolute functions * Use relative package integration imports * Move callbacks to async_added_to_hass * Avoid side effects in init * Prevent blocking I/O in coroutines * Make sure entity is setup before added --- .../components/orangepi_gpio/__init__.py | 39 ++++--------- .../components/orangepi_gpio/binary_sensor.py | 56 ++++++++++--------- .../components/orangepi_gpio/const.py | 6 +- 3 files changed, 43 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/orangepi_gpio/__init__.py b/homeassistant/components/orangepi_gpio/__init__.py index 7547342d898..7b686399d0f 100644 --- a/homeassistant/components/orangepi_gpio/__init__.py +++ b/homeassistant/components/orangepi_gpio/__init__.py @@ -1,18 +1,18 @@ """Support for controlling GPIO pins of a Orange Pi.""" + import logging +from OPi import GPIO + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) -CONF_PIN_MODE = "pin_mode" DOMAIN = "orangepi_gpio" -PIN_MODES = ["pc", "zeroplus", "zeroplus2", "deo", "neocore2"] -def setup(hass, config): +async def async_setup(hass, config): """Set up the Orange Pi GPIO component.""" - from OPi import GPIO def cleanup_gpio(event): """Stuff to do before stopping.""" @@ -20,16 +20,16 @@ def setup(hass, config): def prepare_gpio(event): """Stuff to do when home assistant starts.""" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) return True def setup_mode(mode): """Set GPIO pin mode.""" - from OPi import GPIO - + _LOGGER.debug("Setting GPIO pin mode as %s", mode) if mode == "pc": import orangepi.pc @@ -52,36 +52,19 @@ def setup_mode(mode): GPIO.setmode(nanopi.neocore2.BOARD) -def setup_output(port): - """Set up a GPIO as output.""" - from OPi import GPIO - - GPIO.setup(port, GPIO.OUT) - - def setup_input(port): """Set up a GPIO as input.""" - from OPi import GPIO - + _LOGGER.debug("Setting up GPIO pin %i as input", port) GPIO.setup(port, GPIO.IN) -def write_output(port, value): - """Write a value to a GPIO.""" - from OPi import GPIO - - GPIO.output(port, value) - - def read_input(port): """Read a value from a GPIO.""" - from OPi import GPIO - + _LOGGER.debug("Reading GPIO pin %i", port) return GPIO.input(port) def edge_detect(port, event_callback): """Add detection for RISING and FALLING events.""" - from OPi import GPIO - + _LOGGER.debug("Add callback for GPIO pin %i", port) GPIO.add_event_detect(port, GPIO.BOTH, callback=event_callback) diff --git a/homeassistant/components/orangepi_gpio/binary_sensor.py b/homeassistant/components/orangepi_gpio/binary_sensor.py index b89faf3e7d4..b89442a571c 100644 --- a/homeassistant/components/orangepi_gpio/binary_sensor.py +++ b/homeassistant/components/orangepi_gpio/binary_sensor.py @@ -1,50 +1,52 @@ """Support for binary sensor using Orange Pi GPIO.""" -import logging -from homeassistant.components import orangepi_gpio -from homeassistant.components.binary_sensor import BinarySensorDevice, PLATFORM_SCHEMA -from homeassistant.const import DEVICE_DEFAULT_NAME +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice -from . import CONF_PIN_MODE -from .const import CONF_INVERT_LOGIC, CONF_PORTS, PORT_SCHEMA - -_LOGGER = logging.getLogger(__name__) +from . import edge_detect, read_input, setup_input, setup_mode +from .const import CONF_INVERT_LOGIC, CONF_PIN_MODE, CONF_PORTS, PORT_SCHEMA PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(PORT_SCHEMA) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Orange Pi GPIO devices.""" - pin_mode = config[CONF_PIN_MODE] - orangepi_gpio.setup_mode(pin_mode) - - invert_logic = config[CONF_INVERT_LOGIC] - +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Orange Pi GPIO platform.""" binary_sensors = [] + invert_logic = config[CONF_INVERT_LOGIC] + pin_mode = config[CONF_PIN_MODE] ports = config[CONF_PORTS] + + setup_mode(pin_mode) + for port_num, port_name in ports.items(): - binary_sensors.append(OPiGPIOBinarySensor(port_name, port_num, invert_logic)) - add_entities(binary_sensors, True) + binary_sensors.append( + OPiGPIOBinarySensor(hass, port_name, port_num, invert_logic) + ) + async_add_entities(binary_sensors) class OPiGPIOBinarySensor(BinarySensorDevice): """Represent a binary sensor that uses Orange Pi GPIO.""" - def __init__(self, name, port, invert_logic): + def __init__(self, hass, name, port, invert_logic): """Initialize the Orange Pi binary sensor.""" - self._name = name or DEVICE_DEFAULT_NAME + self._name = name self._port = port self._invert_logic = invert_logic self._state = None - orangepi_gpio.setup_input(self._port) + async def async_added_to_hass(self): + """Run when entity about to be added to hass.""" - def read_gpio(port): - """Read state from GPIO.""" - self._state = orangepi_gpio.read_input(self._port) - self.schedule_update_ha_state() + def gpio_edge_listener(port): + """Update GPIO when edge change is detected.""" + self.schedule_update_ha_state(True) - orangepi_gpio.edge_detect(self._port, read_gpio) + def setup_entity(): + setup_input(self._port) + edge_detect(self._port, gpio_edge_listener) + self.schedule_update_ha_state(True) + + await self.hass.async_add_executor_job(setup_entity) @property def should_poll(self): @@ -62,5 +64,5 @@ class OPiGPIOBinarySensor(BinarySensorDevice): return self._state != self._invert_logic def update(self): - """Update the GPIO state.""" - self._state = orangepi_gpio.read_input(self._port) + """Update state with new GPIO data.""" + self._state = read_input(self._port) diff --git a/homeassistant/components/orangepi_gpio/const.py b/homeassistant/components/orangepi_gpio/const.py index 6bb9ab1df1e..928d75a1f98 100644 --- a/homeassistant/components/orangepi_gpio/const.py +++ b/homeassistant/components/orangepi_gpio/const.py @@ -1,14 +1,14 @@ """Constants for Orange Pi GPIO.""" + import voluptuous as vol from homeassistant.helpers import config_validation as cv -from . import CONF_PIN_MODE, PIN_MODES - CONF_INVERT_LOGIC = "invert_logic" +CONF_PIN_MODE = "pin_mode" CONF_PORTS = "ports" - DEFAULT_INVERT_LOGIC = False +PIN_MODES = ["pc", "zeroplus", "zeroplus2", "deo", "neocore2"] _SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) From ac467d0b3fa529b588d1505addf3cf5f5b63cf6f Mon Sep 17 00:00:00 2001 From: Quentame Date: Mon, 21 Oct 2019 14:30:49 +0200 Subject: [PATCH 536/639] Not slugify cert_expiry name (#28055) --- .../components/cert_expiry/config_flow.py | 10 +++++----- tests/components/cert_expiry/test_config_flow.py | 14 +++++++------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index d73762ce882..43931fe5830 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -5,7 +5,6 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST from homeassistant.core import HomeAssistant, callback -from homeassistant.util import slugify from .const import DOMAIN, DEFAULT_PORT, DEFAULT_NAME from .helper import get_cert @@ -62,11 +61,12 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._errors[CONF_HOST] = "host_port_exists" else: if await self._test_connection(user_input): - host = user_input[CONF_HOST] - name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) - prt = user_input.get(CONF_PORT, DEFAULT_PORT) return self.async_create_entry( - title=name, data={CONF_HOST: host, CONF_PORT: prt} + title=user_input.get(CONF_NAME, DEFAULT_NAME), + data={ + CONF_HOST: user_input[CONF_HOST], + CONF_PORT: user_input.get(CONF_PORT, DEFAULT_PORT), + }, ) else: user_input = {} diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index f44e65512e3..988f3e97106 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -5,7 +5,7 @@ from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.cert_expiry import config_flow -from homeassistant.components.cert_expiry.const import DEFAULT_PORT +from homeassistant.components.cert_expiry.const import DEFAULT_NAME, DEFAULT_PORT from homeassistant.const import CONF_PORT, CONF_NAME, CONF_HOST from tests.common import MockConfigEntry, mock_coro @@ -45,7 +45,7 @@ async def test_user(hass, test_connect): {CONF_NAME: NAME, CONF_HOST: HOST, CONF_PORT: PORT} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -57,21 +57,21 @@ async def test_import(hass, test_connect): # import with only host result = await flow.async_step_import({CONF_HOST: HOST}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "ssl_certificate_expiry" + assert result["title"] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT # import with host and name result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT # improt with host and port result = await flow.async_step_import({CONF_HOST: HOST, CONF_PORT: PORT}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "ssl_certificate_expiry" + assert result["title"] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -80,7 +80,7 @@ async def test_import(hass, test_connect): {CONF_HOST: HOST, CONF_PORT: PORT, CONF_NAME: NAME} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT @@ -112,7 +112,7 @@ async def test_abort_if_already_setup(hass, test_connect): {CONF_HOST: HOST, CONF_NAME: NAME, CONF_PORT: 888} ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "cert_expiry_test_1_2_3" + assert result["title"] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == 888 From 269c8f1d1477053f4aec683f2850cf34b5a7a012 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 21 Oct 2019 14:04:56 +0100 Subject: [PATCH 537/639] Add hvac_action to geniushub (#28056) * add hvac_action() to climate zones --- homeassistant/components/geniushub/climate.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/homeassistant/components/geniushub/climate.py b/homeassistant/components/geniushub/climate.py index 5acc25a36ee..9a19edd9f8b 100644 --- a/homeassistant/components/geniushub/climate.py +++ b/homeassistant/components/geniushub/climate.py @@ -3,6 +3,9 @@ from typing import List, Optional from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_ACTIVITY, @@ -68,6 +71,17 @@ class GeniusClimateZone(GeniusZone, ClimateDevice): """Return the list of available hvac operation modes.""" return list(HA_HVAC_TO_GH) + @property + def hvac_action(self) -> Optional[str]: + """Return the current running hvac operation if supported.""" + if "_state" in self._zone.data: # only for v3 API + if not self._zone.data["_state"].get("bIsActive"): + return CURRENT_HVAC_OFF + if self._zone.data["_state"].get("bOutRequestHeat"): + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + return None + @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" From ba10d5d604e425a2ba17fcbf0d1d360ef8dd5bd8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 Oct 2019 16:06:57 +0200 Subject: [PATCH 538/639] Add ESPHome sensor force_update option (#28059) * Add ESPHome sensor force_update option * Update aioesphomeapi to 2.4.0 --- homeassistant/components/esphome/manifest.json | 2 +- homeassistant/components/esphome/sensor.py | 5 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index bde64762121..b2286b8ab67 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.2.0" + "aioesphomeapi==2.4.0" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/homeassistant/components/esphome/sensor.py b/homeassistant/components/esphome/sensor.py index 2b7a8b94f1e..b6adbf93c41 100644 --- a/homeassistant/components/esphome/sensor.py +++ b/homeassistant/components/esphome/sensor.py @@ -57,6 +57,11 @@ class EsphomeSensor(EsphomeEntity): """Return the icon.""" return self._static_info.icon + @property + def force_update(self) -> bool: + """Return if this sensor should force a state update.""" + return self._static_info.force_update + @esphome_state_property def state(self) -> Optional[str]: """Return the state of the entity.""" diff --git a/requirements_all.txt b/requirements_all.txt index 881bbd119d3..62e53c59a64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,7 +139,7 @@ aiobotocore==0.10.2 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.2.0 +aioesphomeapi==2.4.0 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 754a7d72a64..f7c4b68306c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -70,7 +70,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.2 # homeassistant.components.esphome -aioesphomeapi==2.2.0 +aioesphomeapi==2.4.0 # homeassistant.components.emulated_hue # homeassistant.components.http From 70ddab2f3cb21bc431a46ddc4c1ac0254553f17c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Mon, 21 Oct 2019 17:54:59 +0300 Subject: [PATCH 539/639] Helpers type hint additions and improvements (#27986) * Helpers type hint additions and improvements * Fix async setup dump callback signature --- homeassistant/helpers/event.py | 71 +++++++++++++++++--------- homeassistant/helpers/restore_state.py | 17 +++--- homeassistant/helpers/storage.py | 10 ++-- 3 files changed, 57 insertions(+), 41 deletions(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b7707b844d4..e819da9873a 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1,13 +1,13 @@ """Helpers for listening to events.""" from datetime import datetime, timedelta import functools as ft -from typing import Callable +from typing import Any, Callable, Iterable, Optional, Union import attr from homeassistant.loader import bind_hass from homeassistant.helpers.sun import get_astral_event_next -from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE +from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE, Event from homeassistant.const import ( ATTR_NOW, EVENT_STATE_CHANGED, @@ -240,7 +240,9 @@ track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_tim @callback @bind_hass -def async_call_later(hass, delay, action): +def async_call_later( + hass: HomeAssistant, delay: float, action: Callable[..., None] +) -> CALLBACK_TYPE: """Add a listener that is called in .""" return async_track_point_in_utc_time( hass, action, dt_util.utcnow() + timedelta(seconds=delay) @@ -252,7 +254,9 @@ call_later = threaded_listener_factory(async_call_later) @callback @bind_hass -def async_track_time_interval(hass, action, interval): +def async_track_time_interval( + hass: HomeAssistant, action: Callable[..., None], interval: timedelta +) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" remove = None @@ -284,14 +288,14 @@ class SunListener: """Helper class to help listen to sun events.""" hass = attr.ib(type=HomeAssistant) - action = attr.ib(type=Callable) - event = attr.ib(type=str) - offset = attr.ib(type=timedelta) - _unsub_sun: CALLBACK_TYPE = attr.ib(default=None) - _unsub_config: CALLBACK_TYPE = attr.ib(default=None) + action: Callable[..., None] = attr.ib() + event: str = attr.ib() + offset: Optional[timedelta] = attr.ib() + _unsub_sun: Optional[CALLBACK_TYPE] = attr.ib(default=None) + _unsub_config: Optional[CALLBACK_TYPE] = attr.ib(default=None) @callback - def async_attach(self): + def async_attach(self) -> None: """Attach a sun listener.""" assert self._unsub_config is None @@ -302,7 +306,7 @@ class SunListener: self._listen_next_sun_event() @callback - def async_detach(self): + def async_detach(self) -> None: """Detach the sun listener.""" assert self._unsub_sun is not None assert self._unsub_config is not None @@ -313,7 +317,7 @@ class SunListener: self._unsub_config = None @callback - def _listen_next_sun_event(self): + def _listen_next_sun_event(self) -> None: """Set up the sun event listener.""" assert self._unsub_sun is None @@ -324,14 +328,14 @@ class SunListener: ) @callback - def _handle_sun_event(self, _now): + def _handle_sun_event(self, _now: Any) -> None: """Handle solar event.""" self._unsub_sun = None self._listen_next_sun_event() self.hass.async_run_job(self.action) @callback - def _handle_config_event(self, _event): + def _handle_config_event(self, _event: Any) -> None: """Handle core config update.""" assert self._unsub_sun is not None self._unsub_sun() @@ -341,7 +345,9 @@ class SunListener: @callback @bind_hass -def async_track_sunrise(hass, action, offset=None): +def async_track_sunrise( + hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None +) -> CALLBACK_TYPE: """Add a listener that will fire a specified offset from sunrise daily.""" listener = SunListener(hass, action, SUN_EVENT_SUNRISE, offset) listener.async_attach() @@ -353,7 +359,9 @@ track_sunrise = threaded_listener_factory(async_track_sunrise) @callback @bind_hass -def async_track_sunset(hass, action, offset=None): +def async_track_sunset( + hass: HomeAssistant, action: Callable[..., None], offset: Optional[timedelta] = None +) -> CALLBACK_TYPE: """Add a listener that will fire a specified offset from sunset daily.""" listener = SunListener(hass, action, SUN_EVENT_SUNSET, offset) listener.async_attach() @@ -366,8 +374,13 @@ track_sunset = threaded_listener_factory(async_track_sunset) @callback @bind_hass def async_track_utc_time_change( - hass, action, hour=None, minute=None, second=None, local=False -): + hass: HomeAssistant, + action: Callable[..., None], + hour: Optional[Any] = None, + minute: Optional[Any] = None, + second: Optional[Any] = None, + local: bool = False, +) -> CALLBACK_TYPE: """Add a listener that will fire if time matches a pattern.""" # We do not have to wrap the function with time pattern matching logic # if no pattern given @@ -386,7 +399,7 @@ def async_track_utc_time_change( next_time = None - def calculate_next(now): + def calculate_next(now: datetime) -> None: """Calculate and set the next time the trigger should fire.""" nonlocal next_time @@ -397,10 +410,10 @@ def async_track_utc_time_change( # Make sure rolling back the clock doesn't prevent the timer from # triggering. - last_now = None + last_now: Optional[datetime] = None @callback - def pattern_time_change_listener(event): + def pattern_time_change_listener(event: Event) -> None: """Listen for matching time_changed events.""" nonlocal next_time, last_now @@ -427,7 +440,13 @@ track_utc_time_change = threaded_listener_factory(async_track_utc_time_change) @callback @bind_hass -def async_track_time_change(hass, action, hour=None, minute=None, second=None): +def async_track_time_change( + hass: HomeAssistant, + action: Callable[..., None], + hour: Optional[Any] = None, + minute: Optional[Any] = None, + second: Optional[Any] = None, +) -> CALLBACK_TYPE: """Add a listener that will fire if UTC time matches a pattern.""" return async_track_utc_time_change(hass, action, hour, minute, second, local=True) @@ -435,7 +454,9 @@ def async_track_time_change(hass, action, hour=None, minute=None, second=None): track_time_change = threaded_listener_factory(async_track_time_change) -def _process_state_match(parameter): +def _process_state_match( + parameter: Union[None, str, Iterable[str]] +) -> Callable[[str], bool]: """Convert parameter to function that matches input against parameter.""" if parameter is None or parameter == MATCH_ALL: return lambda _: True @@ -443,5 +464,5 @@ def _process_state_match(parameter): if isinstance(parameter, str) or not hasattr(parameter, "__iter__"): return lambda state: state == parameter - parameter = tuple(parameter) - return lambda state: state in parameter + parameter_tuple = tuple(parameter) + return lambda state: state in parameter_tuple diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index fdf52c99075..5d47f34b002 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -164,23 +164,20 @@ class RestoreStateData: @callback def async_setup_dump(self, *args: Any) -> None: """Set up the restore state listeners.""" + + def _async_dump_states(*_: Any) -> None: + self.hass.async_create_task(self.async_dump_states()) + # Dump the initial states now. This helps minimize the risk of having # old states loaded by overwritting the last states once home assistant # has started and the old states have been read. - self.hass.async_create_task(self.async_dump_states()) + _async_dump_states() # Dump states periodically - async_track_time_interval( - self.hass, - lambda *_: self.hass.async_create_task(self.async_dump_states()), - STATE_DUMP_INTERVAL, - ) + async_track_time_interval(self.hass, _async_dump_states, STATE_DUMP_INTERVAL) # Dump states when stopping hass - self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, - lambda *_: self.hass.async_create_task(self.async_dump_states()), - ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_dump_states) @callback def async_restore_entity_added(self, entity_id: str) -> None: diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 72458d24c82..bd18eebfb25 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -6,7 +6,7 @@ import os from typing import Dict, List, Optional, Callable, Union, Any, Type from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, callback, CALLBACK_TYPE from homeassistant.loader import bind_hass from homeassistant.util import json as json_util from homeassistant.helpers.event import async_call_later @@ -72,8 +72,8 @@ class Store: self.hass = hass self._private = private self._data: Optional[Dict[str, Any]] = None - self._unsub_delay_listener = None - self._unsub_stop_listener = None + self._unsub_delay_listener: Optional[CALLBACK_TYPE] = None + self._unsub_stop_listener: Optional[CALLBACK_TYPE] = None self._write_lock = asyncio.Lock() self._load_task: Optional[asyncio.Future] = None self._encoder = encoder @@ -137,9 +137,7 @@ class Store: await self._async_handle_write_data() @callback - def async_delay_save( - self, data_func: Callable[[], Dict], delay: Optional[int] = None - ) -> None: + def async_delay_save(self, data_func: Callable[[], Dict], delay: float = 0) -> None: """Save data with an optional delay.""" self._data = {"version": self.version, "key": self.key, "data_func": data_func} From 643257d911fd4272ed9805319ce31e3d2017ad30 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 21 Oct 2019 17:34:58 +0200 Subject: [PATCH 540/639] Include subscriber information when MQTT message can't be decoded (#28062) --- homeassistant/components/mqtt/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 119c9b520d7..2fab599ac3f 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -925,10 +925,11 @@ class MQTT: payload = msg.payload.decode(subscription.encoding) except (AttributeError, UnicodeDecodeError): _LOGGER.warning( - "Can't decode payload %s on %s with encoding %s", + "Can't decode payload %s on %s with encoding %s (for %s)", msg.payload, msg.topic, subscription.encoding, + subscription.callback, ) continue From a0c50f4794b96fe2aec0b5e8b8914ae5a99a1091 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 21 Oct 2019 13:14:17 -0400 Subject: [PATCH 541/639] Leverage zigpy for IEEE address conversions (#27972) * Refactor EUI64 conversions. * Update ZHA dependencies. * Update tests. --- homeassistant/components/zha/api.py | 39 ++++++++++---------- homeassistant/components/zha/core/helpers.py | 13 ++----- homeassistant/components/zha/manifest.json | 8 ++-- requirements_all.txt | 8 ++-- requirements_test_all.txt | 8 ++-- tests/components/zha/common.py | 17 +++++++-- tests/components/zha/test_binary_sensor.py | 16 +++++--- tests/components/zha/test_device_tracker.py | 17 ++++++--- tests/components/zha/test_fan.py | 27 ++++++++++---- tests/components/zha/test_light.py | 14 +++++-- tests/components/zha/test_lock.py | 17 ++++++--- tests/components/zha/test_sensor.py | 13 +++++-- tests/components/zha/test_switch.py | 19 +++++++--- 13 files changed, 135 insertions(+), 81 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index ff9f27d4843..ece644f8168 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -4,6 +4,7 @@ import asyncio import logging import voluptuous as vol +from zigpy.types.named import EUI64 from homeassistant.components import websocket_api from homeassistant.core import callback @@ -44,7 +45,7 @@ from .core.const import ( WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) -from .core.helpers import async_is_bindable_target, convert_ieee, get_matched_clusters +from .core.helpers import async_is_bindable_target, get_matched_clusters _LOGGER = logging.getLogger(__name__) @@ -76,16 +77,16 @@ IEEE_SERVICE = "ieee_based_service" SERVICE_SCHEMAS = { SERVICE_PERMIT: vol.Schema( { - vol.Optional(ATTR_IEEE_ADDRESS, default=None): convert_ieee, + vol.Optional(ATTR_IEEE_ADDRESS, default=None): EUI64.convert, vol.Optional(ATTR_DURATION, default=60): vol.All( vol.Coerce(int), vol.Range(0, 254) ), } ), - IEEE_SERVICE: vol.Schema({vol.Required(ATTR_IEEE_ADDRESS): convert_ieee}), + IEEE_SERVICE: vol.Schema({vol.Required(ATTR_IEEE_ADDRESS): EUI64.convert}), SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -96,7 +97,7 @@ SERVICE_SCHEMAS = { ), SERVICE_WARNING_DEVICE_SQUAWK: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_SQUAWK_MODE_ARMED ): cv.positive_int, @@ -110,7 +111,7 @@ SERVICE_SCHEMAS = { ), SERVICE_WARNING_DEVICE_WARN: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Optional( ATTR_WARNING_DEVICE_MODE, default=WARNING_DEVICE_MODE_EMERGENCY ): cv.positive_int, @@ -131,7 +132,7 @@ SERVICE_SCHEMAS = { ), SERVICE_ISSUE_ZIGBEE_CLUSTER_COMMAND: vol.Schema( { - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): cv.positive_int, vol.Required(ATTR_CLUSTER_ID): cv.positive_int, vol.Optional(ATTR_CLUSTER_TYPE, default=CLUSTER_TYPE_IN): cv.string, @@ -149,7 +150,7 @@ SERVICE_SCHEMAS = { @websocket_api.websocket_command( { vol.Required("type"): "zha/devices/permit", - vol.Optional(ATTR_IEEE, default=None): convert_ieee, + vol.Optional(ATTR_IEEE, default=None): EUI64.convert, vol.Optional(ATTR_DURATION, default=60): vol.All( vol.Coerce(int), vol.Range(0, 254) ), @@ -200,7 +201,7 @@ async def websocket_get_devices(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): convert_ieee} + {vol.Required(TYPE): "zha/device", vol.Required(ATTR_IEEE): EUI64.convert} ) async def websocket_get_device(hass, connection, msg): """Get ZHA devices.""" @@ -252,7 +253,7 @@ def async_get_device_info(hass, device, ha_device_registry=None): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/reconfigure", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, } ) async def websocket_reconfigure_node(hass, connection, msg): @@ -267,7 +268,7 @@ async def websocket_reconfigure_node(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): convert_ieee} + {vol.Required(TYPE): "zha/devices/clusters", vol.Required(ATTR_IEEE): EUI64.convert} ) async def websocket_device_clusters(hass, connection, msg): """Return a list of device clusters.""" @@ -305,7 +306,7 @@ async def websocket_device_clusters(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -346,7 +347,7 @@ async def websocket_device_cluster_attributes(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/commands", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -400,7 +401,7 @@ async def websocket_device_cluster_commands(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/clusters/attributes/value", - vol.Required(ATTR_IEEE): convert_ieee, + vol.Required(ATTR_IEEE): EUI64.convert, vol.Required(ATTR_ENDPOINT_ID): int, vol.Required(ATTR_CLUSTER_ID): int, vol.Required(ATTR_CLUSTER_TYPE): str, @@ -444,7 +445,7 @@ async def websocket_read_zigbee_cluster_attributes(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( - {vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): convert_ieee} + {vol.Required(TYPE): "zha/devices/bindable", vol.Required(ATTR_IEEE): EUI64.convert} ) async def websocket_get_bindable_devices(hass, connection, msg): """Directly bind devices.""" @@ -472,8 +473,8 @@ async def websocket_get_bindable_devices(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/bind", - vol.Required(ATTR_SOURCE_IEEE): convert_ieee, - vol.Required(ATTR_TARGET_IEEE): convert_ieee, + vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(ATTR_TARGET_IEEE): EUI64.convert, } ) async def websocket_bind_devices(hass, connection, msg): @@ -494,8 +495,8 @@ async def websocket_bind_devices(hass, connection, msg): @websocket_api.websocket_command( { vol.Required(TYPE): "zha/devices/unbind", - vol.Required(ATTR_SOURCE_IEEE): convert_ieee, - vol.Required(ATTR_TARGET_IEEE): convert_ieee, + vol.Required(ATTR_SOURCE_IEEE): EUI64.convert, + vol.Required(ATTR_TARGET_IEEE): EUI64.convert, } ) async def websocket_unbind_devices(hass, connection, msg): diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 88a472716cc..14103a5ea38 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -8,6 +8,8 @@ import asyncio import collections import logging +from zigpy.types.named import EUI64 + from homeassistant.core import callback from .const import ( @@ -78,15 +80,6 @@ async def check_zigpy_connection(usb_path, radio_type, database_path): return True -def convert_ieee(ieee_str): - """Convert given ieee string to EUI64.""" - from zigpy.types import EUI64, uint8_t - - if ieee_str is None: - return None - return EUI64([uint8_t(p, base=16) for p in ieee_str.split(":")]) - - def get_attr_id_by_name(cluster, attr_name): """Get the attribute id for a cluster attribute by its name.""" return next( @@ -145,7 +138,7 @@ async def async_get_zha_device(hass, device_id): registry_device = device_registry.async_get(device_id) zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = convert_ieee(ieee_address) + ieee = EUI64.convert(ieee_address) return zha_gateway.devices[ieee] diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 9790fbffd06..9821ec2025b 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,10 +6,10 @@ "requirements": [ "bellows-homeassistant==0.10.0", "zha-quirks==0.0.26", - "zigpy-deconz==0.5.0", - "zigpy-homeassistant==0.9.0", - "zigpy-xbee-homeassistant==0.5.0", - "zigpy-zigate==0.4.1" + "zigpy-deconz==0.6.0", + "zigpy-homeassistant==0.10.0", + "zigpy-xbee-homeassistant==0.6.0", + "zigpy-zigate==0.5.0" ], "dependencies": [], "codeowners": ["@dmulcahey", "@adminiuga"] diff --git a/requirements_all.txt b/requirements_all.txt index 62e53c59a64..982c20e5972 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2032,16 +2032,16 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-deconz==0.5.0 +zigpy-deconz==0.6.0 # homeassistant.components.zha -zigpy-homeassistant==0.9.0 +zigpy-homeassistant==0.10.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.5.0 +zigpy-xbee-homeassistant==0.6.0 # homeassistant.components.zha -zigpy-zigate==0.4.1 +zigpy-zigate==0.5.0 # homeassistant.components.zoneminder zm-py==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f7c4b68306c..047ed96bf10 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -641,13 +641,13 @@ zeroconf==0.23.0 zha-quirks==0.0.26 # homeassistant.components.zha -zigpy-deconz==0.5.0 +zigpy-deconz==0.6.0 # homeassistant.components.zha -zigpy-homeassistant==0.9.0 +zigpy-homeassistant==0.10.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.5.0 +zigpy-xbee-homeassistant==0.6.0 # homeassistant.components.zha -zigpy-zigate==0.4.1 +zigpy-zigate==0.5.0 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index fc29e4012cd..9a559aae9b6 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -3,6 +3,8 @@ import time from unittest.mock import Mock, patch from asynctest import CoroutineMock +from zigpy.types.named import EUI64 +import zigpy.zcl.foundation as zcl_f from homeassistant.components.zha.core.const import ( DATA_ZHA, @@ -10,7 +12,6 @@ from homeassistant.components.zha.core.const import ( DATA_ZHA_CONFIG, DATA_ZHA_DISPATCHERS, ) -from homeassistant.components.zha.core.helpers import convert_ieee from homeassistant.util import slugify from tests.common import mock_coro @@ -21,7 +22,7 @@ class FakeApplication: def __init__(self): """Init fake application.""" - self.ieee = convert_ieee("00:15:8d:00:02:32:4f:32") + self.ieee = EUI64.convert("00:15:8d:00:02:32:4f:32") self.nwk = 0x087D @@ -71,7 +72,6 @@ def patch_cluster(cluster): cluster.configure_reporting = CoroutineMock(return_value=[0]) cluster.deserialize = Mock() cluster.handle_cluster_request = Mock() - cluster.handle_cluster_general_request = Mock() cluster.read_attributes = CoroutineMock() cluster.read_attributes_raw = Mock() cluster.unbind = CoroutineMock(return_value=[0]) @@ -83,7 +83,7 @@ class FakeDevice: def __init__(self, ieee, manufacturer, model): """Init fake device.""" self._application = APPLICATION - self.ieee = convert_ieee(ieee) + self.ieee = EUI64.convert(ieee) self.nwk = 0xB79C self.zdo = Mock() self.endpoints = {0: self.zdo} @@ -230,3 +230,12 @@ async def async_test_device_join( domain, zigpy_device, cluster, use_suffix=device_type is None ) assert hass.states.get(entity_id) is not None + + +def make_zcl_header(command_id: int, global_command: bool = True) -> zcl_f.ZCLHeader: + """Cluster.handle_message() ZCL Header helper.""" + if global_command: + frc = zcl_f.FrameControl(zcl_f.FrameType.GLOBAL_COMMAND) + else: + frc = zcl_f.FrameControl(zcl_f.FrameType.CLUSTER_COMMAND) + return zcl_f.ZCLHeader(frc, tsn=1, command_id=command_id) diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 47f81787acd..4fca5505d29 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,12 +1,16 @@ """Test zha binary sensor.""" +from zigpy.zcl.foundation import Command + from homeassistant.components.binary_sensor import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) @@ -74,13 +78,15 @@ async def async_test_binary_sensor_on_off(hass, cluster, entity_id): """Test getting on and off messages for binary sensors.""" # binary sensor on attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # binary sensor off attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 6a7638d9f86..0c27c1514f1 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -1,19 +1,25 @@ """Test ZHA Device Tracker.""" from datetime import timedelta import time + +from zigpy.zcl.foundation import Command + from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE from homeassistant.components.zha.core.registries import ( SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, ) +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) + from tests.common import async_fire_time_changed @@ -67,10 +73,11 @@ async def test_device_tracker(hass, config_entry, zha_gateway): # turn state flip attr = make_attribute(0x0020, 23) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) attr = make_attribute(0x0021, 200) - cluster.handle_message(1, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) zigpy_device.last_seen = time.time() + 10 next_update = dt_util.utcnow() + timedelta(seconds=30) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 3fe5e7937c8..1704ab2196b 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,18 +1,30 @@ """Test zha fan.""" from unittest.mock import call, patch + +from zigpy.zcl.foundation import Command + from homeassistant.components import fan -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE from homeassistant.components.fan import ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF -from tests.common import mock_coro +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) +from tests.common import mock_coro + async def test_fan(hass, config_entry, zha_gateway): """Test zha fan platform.""" @@ -44,13 +56,14 @@ async def test_fan(hass, config_entry, zha_gateway): # turn on at fan attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at fan attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 08c6cfe18cf..567f61ad1e1 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -2,6 +2,8 @@ import asyncio from unittest.mock import MagicMock, call, patch, sentinel +from zigpy.zcl.foundation import Command + from homeassistant.components.light import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -11,6 +13,7 @@ from .common import ( async_test_device_join, make_attribute, make_entity_id, + make_zcl_header, ) from tests.common import mock_coro @@ -123,13 +126,14 @@ async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at light attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -138,7 +142,8 @@ async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -243,7 +248,8 @@ async def async_test_level_on_off_from_hass( async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): """Test dimmer functionality from the light.""" attr = make_attribute(0, level) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == expected_state # hass uses None for brightness of 0 in state attributes diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 7381b557310..c7cc5bdd2a9 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -1,15 +1,21 @@ """Test zha lock.""" from unittest.mock import patch -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, STATE_UNAVAILABLE + +from zigpy.zcl.foundation import Command + from homeassistant.components.lock import DOMAIN -from tests.common import mock_coro +from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED + from .common import ( + async_enable_traffic, async_init_zigpy_device, make_attribute, make_entity_id, - async_enable_traffic, + make_zcl_header, ) +from tests.common import mock_coro + LOCK_DOOR = 0 UNLOCK_DOOR = 1 @@ -43,13 +49,14 @@ async def test_lock(hass, config_entry, zha_gateway): # set state to locked attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_LOCKED # set state to unlocked attr.value.value = 2 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNLOCKED diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index faa44f34927..37d412e6a25 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,12 +1,16 @@ """Test zha sensor.""" +from zigpy.zcl.foundation import Command + from homeassistant.components.sensor import DOMAIN -from homeassistant.const import STATE_UNKNOWN, STATE_UNAVAILABLE +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) @@ -177,7 +181,8 @@ async def send_attribute_report(hass, cluster, attrid, value): device is paired to the zigbee network. """ attr = make_attribute(attrid, value) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index ac6bc73b809..9c2b2da9c95 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -1,16 +1,22 @@ """Test zha switch.""" from unittest.mock import call, patch + +from zigpy.zcl.foundation import Command + from homeassistant.components.switch import DOMAIN -from homeassistant.const import STATE_ON, STATE_OFF, STATE_UNAVAILABLE -from tests.common import mock_coro +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE + from .common import ( + async_enable_traffic, async_init_zigpy_device, + async_test_device_join, make_attribute, make_entity_id, - async_test_device_join, - async_enable_traffic, + make_zcl_header, ) +from tests.common import mock_coro + ON = 1 OFF = 0 @@ -44,13 +50,14 @@ async def test_switch(hass, config_entry, zha_gateway): # turn on at switch attr = make_attribute(0, 1) - cluster.handle_message(1, 0x0A, [[attr]]) + hdr = make_zcl_header(Command.Report_Attributes) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at switch attr.value.value = 0 - cluster.handle_message(0, 0x0A, [[attr]]) + cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF From 86347a3d5f0f8b73e9fc519989524fcf53a66189 Mon Sep 17 00:00:00 2001 From: Patrik <21142447+ggravlingen@users.noreply.github.com> Date: Mon, 21 Oct 2019 21:42:17 +0200 Subject: [PATCH 542/639] Refactor Tradfri light group (#27714) * Set same manufacturer name of gateway as of devices * Refactor Tradfri light group * Restore should_poll and async_update --- .../components/tradfri/base_class.py | 23 ++++-- homeassistant/components/tradfri/const.py | 2 +- homeassistant/components/tradfri/light.py | 82 +++++-------------- 3 files changed, 38 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/tradfri/base_class.py b/homeassistant/components/tradfri/base_class.py index 632ce6b164e..ba90fe05d1e 100644 --- a/homeassistant/components/tradfri/base_class.py +++ b/homeassistant/components/tradfri/base_class.py @@ -18,7 +18,6 @@ class TradfriBaseClass(Entity): def __init__(self, device, api, gateway_id): """Initialize a device.""" - self._available = True self._api = api self._device = None self._device_control = None @@ -33,7 +32,6 @@ class TradfriBaseClass(Entity): def _async_start_observe(self, exc=None): """Start observation of device.""" if exc: - self._available = False self.async_schedule_update_ha_state() _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) @@ -52,11 +50,6 @@ class TradfriBaseClass(Entity): """Start thread when added to hass.""" self._async_start_observe() - @property - def available(self): - """Return True if entity is available.""" - return self._available - @property def name(self): """Return the display name of this device.""" @@ -82,7 +75,6 @@ class TradfriBaseClass(Entity): """Refresh the device data.""" self._device = device self._name = device.name - self._available = device.reachable class TradfriBaseDevice(TradfriBaseClass): @@ -91,6 +83,16 @@ class TradfriBaseDevice(TradfriBaseClass): All devices should inherit from this class. """ + def __init__(self, device, api, gateway_id): + """Initialize a device.""" + super().__init__(device, api, gateway_id) + self._available = True + + @property + def available(self): + """Return True if entity is available.""" + return self._available + @property def device_info(self): """Return the device info.""" @@ -104,3 +106,8 @@ class TradfriBaseDevice(TradfriBaseClass): "sw_version": info.firmware_version, "via_device": (DOMAIN, self._gateway_id), } + + def _refresh(self, device): + """Refresh the device data.""" + super()._refresh(device) + self._available = device.reachable diff --git a/homeassistant/components/tradfri/const.py b/homeassistant/components/tradfri/const.py index a7acfcbf876..038f0e91c76 100644 --- a/homeassistant/components/tradfri/const.py +++ b/homeassistant/components/tradfri/const.py @@ -7,7 +7,7 @@ ATTR_HUE = "hue" ATTR_SAT = "saturation" ATTR_TRADFRI_GATEWAY = "Gateway" ATTR_TRADFRI_GATEWAY_MODEL = "E1526" -ATTR_TRADFRI_MANUFACTURER = "IKEA" +ATTR_TRADFRI_MANUFACTURER = "IKEA of Sweden" ATTR_TRANSITION_TIME = "transition_time" CONF_ALLOW_TRADFRI_GROUPS = "allow_tradfri_groups" CONF_IDENTITY = "identity" diff --git a/homeassistant/components/tradfri/light.py b/homeassistant/components/tradfri/light.py index 089f80223e8..9ee3c5d6a8c 100644 --- a/homeassistant/components/tradfri/light.py +++ b/homeassistant/components/tradfri/light.py @@ -1,8 +1,6 @@ """Support for IKEA Tradfri lights.""" import logging -from pytradfri.error import PytradfriError - import homeassistant.util.color as color_util from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -14,8 +12,7 @@ from homeassistant.components.light import ( SUPPORT_COLOR, SUPPORT_COLOR_TEMP, ) -from homeassistant.core import callback -from .base_class import TradfriBaseDevice +from .base_class import TradfriBaseDevice, TradfriBaseClass from .const import ( ATTR_DIMMER, ATTR_HUE, @@ -51,50 +48,47 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(TradfriGroup(group, api, gateway_id) for group in groups) -class TradfriGroup(Light): - """The platform class required by hass.""" +class TradfriGroup(TradfriBaseClass, Light): + """The platform class for light groups required by hass.""" - def __init__(self, group, api, gateway_id): + def __init__(self, device, api, gateway_id): """Initialize a Group.""" - self._api = api - self._unique_id = f"group-{gateway_id}-{group.id}" - self._group = group - self._name = group.name + super().__init__(device, api, gateway_id) - self._refresh(group) + self._unique_id = f"group-{gateway_id}-{device.id}" - async def async_added_to_hass(self): - """Start thread when added to hass.""" - self._async_start_observe() + self._refresh(device) @property - def unique_id(self): - """Return unique ID for this group.""" - return self._unique_id + def should_poll(self): + """Poll needed for tradfri groups.""" + return True + + async def async_update(self): + """Fetch new state data for the group. + + This method is required for groups to update properly. + """ + await self._api(self._device.update()) @property def supported_features(self): """Flag supported features.""" return SUPPORTED_GROUP_FEATURES - @property - def name(self): - """Return the display name of this group.""" - return self._name - @property def is_on(self): """Return true if group lights are on.""" - return self._group.state + return self._device.state @property def brightness(self): """Return the brightness of the group lights.""" - return self._group.dimmer + return self._device.dimmer async def async_turn_off(self, **kwargs): """Instruct the group lights to turn off.""" - await self._api(self._group.set_state(0)) + await self._api(self._device.set_state(0)) async def async_turn_on(self, **kwargs): """Instruct the group lights to turn on, or dim.""" @@ -106,41 +100,9 @@ class TradfriGroup(Light): if kwargs[ATTR_BRIGHTNESS] == 255: kwargs[ATTR_BRIGHTNESS] = 254 - await self._api(self._group.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) + await self._api(self._device.set_dimmer(kwargs[ATTR_BRIGHTNESS], **keys)) else: - await self._api(self._group.set_state(1)) - - @callback - def _async_start_observe(self, exc=None): - """Start observation of light.""" - if exc: - _LOGGER.warning("Observation failed for %s", self._name, exc_info=exc) - - try: - cmd = self._group.observe( - callback=self._observe_update, - err_callback=self._async_start_observe, - duration=0, - ) - self.hass.async_create_task(self._api(cmd)) - except PytradfriError as err: - _LOGGER.warning("Observation failed, trying again", exc_info=err) - self._async_start_observe() - - def _refresh(self, group): - """Refresh the light data.""" - self._group = group - self._name = group.name - - @callback - def _observe_update(self, tradfri_device): - """Receive new state data for this light.""" - self._refresh(tradfri_device) - self.async_schedule_update_ha_state() - - async def async_update(self): - """Fetch new state data for the group.""" - await self._api(self._group.update()) + await self._api(self._device.set_state(1)) class TradfriLight(TradfriBaseDevice, Light): From 86b204df0e8d8c988c3de7cc696536b1b7c30ee5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 21 Oct 2019 14:46:39 -0600 Subject: [PATCH 543/639] Update pymyq to 2.0.0 (#28069) * Update pymyq to 2.0.0 * Removed unnecessary update * Restore `type` parameter (as optional) --- homeassistant/components/myq/cover.py | 19 ++++++++----------- homeassistant/components/myq/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index e9c3237490a..b6da7174f05 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,5 +1,8 @@ """Support for MyQ-Enabled Garage Doors.""" import logging + +from pymyq import login +from pymyq.errors import MyQError import voluptuous as vol from homeassistant.components.cover import ( @@ -30,35 +33,29 @@ MYQ_TO_HASS = { PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_TYPE): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + # This parameter is no longer used; keeping it to avoid a breaking change in + # a hotfix, but in a future main release, this should be removed: + vol.Optional(CONF_TYPE): cv.string, } ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the platform.""" - from pymyq import login - from pymyq.errors import MyQError, UnsupportedBrandError - websession = aiohttp_client.async_get_clientsession(hass) username = config[CONF_USERNAME] password = config[CONF_PASSWORD] - brand = config[CONF_TYPE] try: - myq = await login(username, password, brand, websession) - except UnsupportedBrandError: - _LOGGER.error("Unsupported brand: %s", brand) - return + myq = await login(username, password, websession) except MyQError as err: _LOGGER.error("There was an error while logging in: %s", err) return - devices = await myq.get_devices() - async_add_entities([MyQDevice(device) for device in devices], True) + async_add_entities([MyQDevice(device) for device in myq.covers.values()], True) class MyQDevice(CoverDevice): diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 213679b320a..73265b61c83 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -3,7 +3,7 @@ "name": "Myq", "documentation": "https://www.home-assistant.io/integrations/myq", "requirements": [ - "pymyq==1.2.1" + "pymyq==2.0.0" ], "dependencies": [], "codeowners": [] diff --git a/requirements_all.txt b/requirements_all.txt index 982c20e5972..0330d7d8f60 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1322,7 +1322,7 @@ pymonoprice==0.3 pymusiccast==0.1.6 # homeassistant.components.myq -pymyq==1.2.1 +pymyq==2.0.0 # homeassistant.components.mysensors pymysensors==0.18.0 From ef194d1b82ae2e0cc6124ece88d41d9660e61b11 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 21 Oct 2019 22:56:02 +0200 Subject: [PATCH 544/639] Fix mypy missing from dev install script (#28060) * Fix mypy missing * Update bootstrap * Update script/bootstrap Co-Authored-By: cgtobi --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index ed6cd55be36..ba594cbb341 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -7,4 +7,4 @@ set -e cd "$(dirname "$0")/.." echo "Installing test dependencies..." -python3 -m pip install tox colorlog pre-commit +python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) From 6c39d77e2362000c29597de5452dbd61d0db26f0 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Mon, 21 Oct 2019 23:06:50 +0200 Subject: [PATCH 545/639] Upgrade youtube_dl to version 2019.10.22 (#28070) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index de3d4546ca0..4fd5470ebdf 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -3,7 +3,7 @@ "name": "Media extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", "requirements": [ - "youtube_dl==2019.10.16" + "youtube_dl==2019.10.22" ], "dependencies": [ "media_player" diff --git a/requirements_all.txt b/requirements_all.txt index 0330d7d8f60..9a7ff3bb14b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2014,7 +2014,7 @@ yeelight==0.5.0 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2019.10.16 +youtube_dl==2019.10.22 # homeassistant.components.zengge zengge==0.2 From 4935ef5233d022516e476b373941d498e040c84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Mon, 21 Oct 2019 21:30:17 +0000 Subject: [PATCH 546/639] Move imports in piglow component (#28046) * Move imports in piglow component * Fix pylint --- homeassistant/components/piglow/light.py | 28 +++++++++++------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/piglow/light.py b/homeassistant/components/piglow/light.py index 31ece4a36a9..27bbb81d31f 100644 --- a/homeassistant/components/piglow/light.py +++ b/homeassistant/components/piglow/light.py @@ -2,18 +2,19 @@ import logging import subprocess +import piglow import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, ATTR_HS_COLOR, + PLATFORM_SCHEMA, + SUPPORT_BRIGHTNESS, SUPPORT_COLOR, Light, - PLATFORM_SCHEMA, ) from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv import homeassistant.util.color as color_util _LOGGER = logging.getLogger(__name__) @@ -29,23 +30,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Piglow Light platform.""" - import piglow - if subprocess.getoutput("i2cdetect -q -y 1 | grep -o 54") != "54": _LOGGER.error("A Piglow device was not found") return False name = config.get(CONF_NAME) - add_entities([PiglowLight(piglow, name)]) + add_entities([PiglowLight(name)]) class PiglowLight(Light): """Representation of an Piglow Light.""" - def __init__(self, piglow, name): + def __init__(self, name): """Initialize an PiglowLight.""" - self._piglow = piglow self._name = name self._is_on = False self._brightness = 255 @@ -88,7 +86,7 @@ class PiglowLight(Light): def turn_on(self, **kwargs): """Instruct the light to turn on.""" - self._piglow.clear() + piglow.clear() if ATTR_BRIGHTNESS in kwargs: self._brightness = kwargs[ATTR_BRIGHTNESS] @@ -99,16 +97,16 @@ class PiglowLight(Light): rgb = color_util.color_hsv_to_RGB( self._hs_color[0], self._hs_color[1], self._brightness / 255 * 100 ) - self._piglow.red(rgb[0]) - self._piglow.green(rgb[1]) - self._piglow.blue(rgb[2]) - self._piglow.show() + piglow.red(rgb[0]) + piglow.green(rgb[1]) + piglow.blue(rgb[2]) + piglow.show() self._is_on = True self.schedule_update_ha_state() def turn_off(self, **kwargs): """Instruct the light to turn off.""" - self._piglow.clear() - self._piglow.show() + piglow.clear() + piglow.show() self._is_on = False self.schedule_update_ha_state() From 92508af25325a284305e1f269a94c220f08dde13 Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 22 Oct 2019 00:01:35 +0200 Subject: [PATCH 547/639] Counter configure with value (#28066) --- homeassistant/components/counter/__init__.py | 11 ++++ .../components/counter/services.yaml | 6 ++ tests/components/counter/test_init.py | 57 ++++++++++++++++++- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/counter/__init__.py b/homeassistant/components/counter/__init__.py index 79877d63f14..aca3461b4f7 100644 --- a/homeassistant/components/counter/__init__.py +++ b/homeassistant/components/counter/__init__.py @@ -16,6 +16,7 @@ ATTR_INITIAL = "initial" ATTR_STEP = "step" ATTR_MINIMUM = "minimum" ATTR_MAXIMUM = "maximum" +VALUE = "value" CONF_INITIAL = "initial" CONF_RESTORE = "restore" @@ -37,6 +38,8 @@ SERVICE_SCHEMA_CONFIGURE = ENTITY_SERVICE_SCHEMA.extend( vol.Optional(ATTR_MINIMUM): vol.Any(None, vol.Coerce(int)), vol.Optional(ATTR_MAXIMUM): vol.Any(None, vol.Coerce(int)), vol.Optional(ATTR_STEP): cv.positive_int, + vol.Optional(ATTR_INITIAL): cv.positive_int, + vol.Optional(VALUE): cv.positive_int, } ) @@ -171,6 +174,10 @@ class Counter(RestoreEntity): state = await self.async_get_last_state() if state is not None: self._state = self.compute_next_state(int(state.state)) + self._initial = state.attributes.get(ATTR_INITIAL) + self._max = state.attributes.get(ATTR_MAXIMUM) + self._min = state.attributes.get(ATTR_MINIMUM) + self._step = state.attributes.get(ATTR_STEP) async def async_decrement(self): """Decrement the counter.""" @@ -195,6 +202,10 @@ class Counter(RestoreEntity): self._max = kwargs[CONF_MAXIMUM] if CONF_STEP in kwargs: self._step = kwargs[CONF_STEP] + if CONF_INITIAL in kwargs: + self._initial = kwargs[CONF_INITIAL] + if VALUE in kwargs: + self._state = kwargs[VALUE] self._state = self.compute_next_state(self._state) await self.async_update_ha_state() diff --git a/homeassistant/components/counter/services.yaml b/homeassistant/components/counter/services.yaml index fc3f0ad36cb..449ae6841ff 100644 --- a/homeassistant/components/counter/services.yaml +++ b/homeassistant/components/counter/services.yaml @@ -33,3 +33,9 @@ configure: step: description: New value for step example: 2 + initial: + description: New value for initial + example: 6 + value: + description: New state value + example: 3 diff --git a/tests/components/counter/test_init.py b/tests/components/counter/test_init.py index 664f4d014b7..8ce90e164b6 100644 --- a/tests/components/counter/test_init.py +++ b/tests/components/counter/test_init.py @@ -174,14 +174,22 @@ def test_initial_state_overrules_restore_state(hass): @asyncio.coroutine def test_restore_state_overrules_initial_state(hass): """Ensure states are restored on startup.""" + + attr = {"initial": 6, "minimum": 1, "maximum": 8, "step": 2} + mock_restore_cache( - hass, (State("counter.test1", "11"), State("counter.test2", "-22")) + hass, + ( + State("counter.test1", "11"), + State("counter.test2", "-22"), + State("counter.test3", "5", attr), + ), ) hass.state = CoreState.starting yield from async_setup_component( - hass, DOMAIN, {DOMAIN: {"test1": {}, "test2": {CONF_INITIAL: 10}}} + hass, DOMAIN, {DOMAIN: {"test1": {}, "test2": {CONF_INITIAL: 10}, "test3": {}}} ) state = hass.states.get("counter.test1") @@ -192,6 +200,14 @@ def test_restore_state_overrules_initial_state(hass): assert state assert int(state.state) == -22 + state = hass.states.get("counter.test3") + assert state + assert int(state.state) == 5 + assert state.attributes.get("initial") == 6 + assert state.attributes.get("minimum") == 1 + assert state.attributes.get("maximum") == 8 + assert state.attributes.get("step") == 2 + @asyncio.coroutine def test_no_initial_state_and_no_restore_state(hass): @@ -379,11 +395,45 @@ async def test_configure(hass, hass_admin_user): assert state.state == "5" assert 3 == state.attributes.get("step") + # update value + await hass.services.async_call( + "counter", + "configure", + {"entity_id": state.entity_id, "value": 6}, + True, + Context(user_id=hass_admin_user.id), + ) + + state = hass.states.get("counter.test") + assert state is not None + assert state.state == "6" + + # update initial + await hass.services.async_call( + "counter", + "configure", + {"entity_id": state.entity_id, "initial": 5}, + True, + Context(user_id=hass_admin_user.id), + ) + + state = hass.states.get("counter.test") + assert state is not None + assert state.state == "6" + assert 5 == state.attributes.get("initial") + # update all await hass.services.async_call( "counter", "configure", - {"entity_id": state.entity_id, "step": 5, "minimum": 0, "maximum": 9}, + { + "entity_id": state.entity_id, + "step": 5, + "minimum": 0, + "maximum": 9, + "value": 5, + "initial": 6, + }, True, Context(user_id=hass_admin_user.id), ) @@ -394,3 +444,4 @@ async def test_configure(hass, hass_admin_user): assert 5 == state.attributes.get("step") assert 0 == state.attributes.get("minimum") assert 9 == state.attributes.get("maximum") + assert 6 == state.attributes.get("initial") From fc8920646b4b860bd9e9230add3d47dd192a1318 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 21 Oct 2019 18:29:04 -0500 Subject: [PATCH 548/639] Fix Plex test timeouts (#28077) * Ensure mocked calls are inside patch * Avoid filesytem I/O --- tests/components/plex/test_config_flow.py | 75 +++++++++++------------ 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 2a2178da9d5..c0d14f1efdc 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -46,14 +46,14 @@ async def test_bad_credentials(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value="BAD TOKEN" ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -103,17 +103,17 @@ async def test_import_file_from_discovery(hass): async def test_discovery(hass): """Test starting a flow from discovery.""" - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": "discovery"}, - data={ - CONF_HOST: MOCK_SERVERS[0][CONF_HOST], - CONF_PORT: MOCK_SERVERS[0][CONF_PORT], - }, - ) - assert result["type"] == "abort" - assert result["reason"] == "discovery_no_file" + with patch("homeassistant.components.plex.config_flow.load_json", return_value={}): + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": "discovery"}, + data={ + CONF_HOST: MOCK_SERVERS[0][CONF_HOST], + CONF_PORT: MOCK_SERVERS[0][CONF_PORT], + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "discovery_no_file" async def test_discovery_while_in_progress(hass): @@ -192,12 +192,12 @@ async def test_unknown_exception(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", side_effect=Exception), asynctest.patch( "plexauth.PlexAuth.initiate_auth" ), asynctest.patch("plexauth.PlexAuth.token", return_value="MOCK_TOKEN"): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -217,14 +217,13 @@ async def test_no_servers_found(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0) ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -248,14 +247,14 @@ async def test_single_available_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( "plexapi.server.PlexServer", return_value=mock_plex_server ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -287,9 +286,6 @@ async def test_multiple_servers_with_selection(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), patch( @@ -299,6 +295,9 @@ async def test_multiple_servers_with_selection(hass): ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -349,9 +348,6 @@ async def test_adding_last_unconfigured_server(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), patch( @@ -361,6 +357,9 @@ async def test_adding_last_unconfigured_server(hass): ), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -440,14 +439,14 @@ async def test_all_available_servers_configured(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with patch( "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) ), asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -495,12 +494,12 @@ async def test_external_timed_out(hass): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=None ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "external_done" @@ -520,12 +519,12 @@ async def test_callback_view(hass, aiohttp_client): assert result["type"] == "form" assert result["step_id"] == "start_website_auth" - result = await hass.config_entries.flow.async_configure(result["flow_id"]) - assert result["type"] == "external" - with asynctest.patch("plexauth.PlexAuth.initiate_auth"), asynctest.patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + assert result["type"] == "external" + client = await aiohttp_client(hass.http.app) forward_url = f'{config_flow.AUTH_CALLBACK_PATH}?flow_id={result["flow_id"]}' From fe7c45b3636e80eed69433d21465b89bacb24601 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 21 Oct 2019 19:30:56 -0400 Subject: [PATCH 549/639] Move remaining of ZHA imports to top level. (#28071) * Move ZHA import to top level. * ZHA tests: move imports to top level. --- homeassistant/components/zha/api.py | 2 +- .../components/zha/core/channels/__init__.py | 14 +- homeassistant/components/zha/core/device.py | 13 +- .../components/zha/core/discovery.py | 7 +- homeassistant/components/zha/core/gateway.py | 6 +- homeassistant/components/zha/core/helpers.py | 28 ++-- .../components/zha/core/registries.py | 136 ++++++++---------- tests/components/zha/common.py | 41 +++--- tests/components/zha/conftest.py | 9 +- tests/components/zha/test_api.py | 20 ++- tests/components/zha/test_binary_sensor.py | 22 +-- tests/components/zha/test_config_flow.py | 2 + tests/components/zha/test_device_action.py | 26 ++-- tests/components/zha/test_device_tracker.py | 27 ++-- tests/components/zha/test_device_trigger.py | 18 +-- tests/components/zha/test_fan.py | 19 ++- tests/components/zha/test_light.py | 51 ++++--- tests/components/zha/test_lock.py | 26 ++-- tests/components/zha/test_sensor.py | 45 +++--- tests/components/zha/test_switch.py | 21 +-- 20 files changed, 261 insertions(+), 272 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index ece644f8168..6f24db442dd 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -5,6 +5,7 @@ import logging import voluptuous as vol from zigpy.types.named import EUI64 +import zigpy.zdo.types as zdo_types from homeassistant.components import websocket_api from homeassistant.core import callback @@ -514,7 +515,6 @@ async def websocket_unbind_devices(hass, connection, msg): async def async_binding_operation(zha_gateway, source_ieee, target_ieee, operation): """Create or remove a direct zigbee binding between 2 devices.""" - from zigpy.zdo import types as zdo_types source_device = zha_gateway.get_device(source_ieee) target_device = zha_gateway.get_device(target_ieee) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 37b0bec207b..66a31ff8f21 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -11,6 +11,8 @@ from functools import wraps import logging from random import uniform +import zigpy.exceptions + from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -48,8 +50,6 @@ def decorate_command(channel, command): @wraps(command) async def wrapper(*args, **kwds): - from zigpy.exceptions import DeliveryError - try: result = await command(*args, **kwds) channel.debug( @@ -61,7 +61,7 @@ def decorate_command(channel, command): ) return result - except (DeliveryError, Timeout) as ex: + except (zigpy.exceptions.DeliveryError, Timeout) as ex: channel.debug("command failed: %s exception: %s", command.__name__, str(ex)) return ex @@ -143,12 +143,10 @@ class ZigbeeChannel(LogMixin): This also swallows DeliveryError exceptions that are thrown when devices are unreachable. """ - from zigpy.exceptions import DeliveryError - try: res = await self.cluster.bind() self.debug("bound '%s' cluster: %s", self.cluster.ep_attribute, res[0]) - except (DeliveryError, Timeout) as ex: + except (zigpy.exceptions.DeliveryError, Timeout) as ex: self.debug( "Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex) ) @@ -167,8 +165,6 @@ class ZigbeeChannel(LogMixin): This also swallows DeliveryError exceptions that are thrown when devices are unreachable. """ - from zigpy.exceptions import DeliveryError - attr_name = self.cluster.attributes.get(attr, [attr])[0] kwargs = {} @@ -189,7 +185,7 @@ class ZigbeeChannel(LogMixin): reportable_change, res, ) - except (DeliveryError, Timeout) as ex: + except (zigpy.exceptions.DeliveryError, Timeout) as ex: self.debug( "failed to set reporting for '%s' attr on '%s' cluster: %s", attr_name, diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index f4a3a2c3d48..b3be8037ff6 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -10,6 +10,10 @@ from enum import Enum import logging import time +import zigpy.exceptions +import zigpy.quirks +from zigpy.profiles import zha, zll + from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -87,9 +91,7 @@ class ZHADevice(LogMixin): self._unsub = async_dispatcher_connect( self.hass, self._available_signal, self.async_initialize ) - from zigpy.quirks import CustomDevice - - self.quirk_applied = isinstance(self._zigpy_device, CustomDevice) + self.quirk_applied = isinstance(self._zigpy_device, zigpy.quirks.CustomDevice) self.quirk_class = "{}.{}".format( self._zigpy_device.__class__.__module__, self._zigpy_device.__class__.__name__, @@ -394,7 +396,6 @@ class ZHADevice(LogMixin): @callback def async_get_std_clusters(self): """Get ZHA and ZLL clusters for this device.""" - from zigpy.profiles import zha, zll return { ep_id: { @@ -448,8 +449,6 @@ class ZHADevice(LogMixin): if cluster is None: return None - from zigpy.exceptions import DeliveryError - try: response = await cluster.write_attributes( {attribute: value}, manufacturer=manufacturer @@ -463,7 +462,7 @@ class ZHADevice(LogMixin): response, ) return response - except DeliveryError as exc: + except zigpy.exceptions.DeliveryError as exc: self.debug( "failed to set attribute: %s %s %s %s %s", f"{ATTR_VALUE}: {value}", diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 622adead803..e23862a7d3e 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -7,6 +7,9 @@ https://home-assistant.io/integrations/zha/ import logging +import zigpy.profiles +from zigpy.zcl.clusters.general import OnOff, PowerConfiguration + from homeassistant import const as ha_const from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.sensor import DOMAIN as SENSOR @@ -52,8 +55,6 @@ def async_process_endpoint( is_new_join, ): """Process an endpoint on a zigpy device.""" - import zigpy.profiles - if endpoint_id == 0: # ZDO _async_create_cluster_channel( endpoint, zha_device, is_new_join, channel_class=ZDOChannel @@ -179,8 +180,6 @@ def _async_handle_single_cluster_matches( hass, endpoint, zha_device, profile_clusters, device_key, is_new_join ): """Dispatch single cluster matches to HA components.""" - from zigpy.zcl.clusters.general import OnOff, PowerConfiguration - cluster_matches = [] cluster_match_results = [] matched_power_configuration = False diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index a64e8cf7fd9..77702c8f3de 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -108,9 +108,9 @@ class ZHAGateway: baudrate = self._config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE) radio_type = self._config_entry.data.get(CONF_RADIO_TYPE) - radio_details = RADIO_TYPES[radio_type][ZHA_GW_RADIO]() - radio = radio_details[ZHA_GW_RADIO] - self.radio_description = RADIO_TYPES[radio_type][ZHA_GW_RADIO_DESCRIPTION] + radio_details = RADIO_TYPES[radio_type] + radio = radio_details[ZHA_GW_RADIO]() + self.radio_description = radio_details[ZHA_GW_RADIO_DESCRIPTION] await radio.connect(usb_path, baudrate) if CONF_DATABASE in self._config: diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index 14103a5ea38..d3f06090dae 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -8,7 +8,15 @@ import asyncio import collections import logging -from zigpy.types.named import EUI64 +import bellows.ezsp +import bellows.zigbee.application +import zigpy.types +import zigpy_deconz.api +import zigpy_deconz.zigbee.application +import zigpy_xbee.api +import zigpy_xbee.zigbee.application +import zigpy_zigate.api +import zigpy_zigate.zigbee.application from homeassistant.core import callback @@ -51,25 +59,17 @@ async def safe_read( async def check_zigpy_connection(usb_path, radio_type, database_path): """Test zigpy radio connection.""" if radio_type == RadioType.ezsp.name: - import bellows.ezsp - from bellows.zigbee.application import ControllerApplication - radio = bellows.ezsp.EZSP() + ControllerApplication = bellows.zigbee.application.ControllerApplication elif radio_type == RadioType.xbee.name: - import zigpy_xbee.api - from zigpy_xbee.zigbee.application import ControllerApplication - radio = zigpy_xbee.api.XBee() + ControllerApplication = zigpy_xbee.zigbee.application.ControllerApplication elif radio_type == RadioType.deconz.name: - import zigpy_deconz.api - from zigpy_deconz.zigbee.application import ControllerApplication - radio = zigpy_deconz.api.Deconz() + ControllerApplication = zigpy_deconz.zigbee.application.ControllerApplication elif radio_type == RadioType.zigate.name: - import zigpy_zigate.api - from zigpy_zigate.zigbee.application import ControllerApplication - radio = zigpy_zigate.api.ZiGate() + ControllerApplication = zigpy_zigate.zigbee.application.ControllerApplication try: await radio.connect(usb_path, DEFAULT_BAUDRATE) controller = ControllerApplication(radio, database_path) @@ -138,7 +138,7 @@ async def async_get_zha_device(hass, device_id): registry_device = device_registry.async_get(device_id) zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] ieee_address = list(list(registry_device.identifiers)[0])[1] - ieee = EUI64.convert(ieee_address) + ieee = zigpy.types.EUI64.convert(ieee_address) return zha_gateway.devices[ieee] diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 43ddc888d2f..571e77d4fae 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -6,6 +6,18 @@ https://home-assistant.io/integrations/zha/ """ import collections +import bellows.ezsp +import bellows.zigbee.application +import zigpy.profiles.zha +import zigpy.profiles.zll +import zigpy.zcl as zcl +import zigpy_deconz.api +import zigpy_deconz.zigbee.application +import zigpy_xbee.api +import zigpy_xbee.zigbee.application +import zigpy_zigate.api +import zigpy_zigate.zigbee.application + from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER from homeassistant.components.fan import DOMAIN as FAN @@ -14,6 +26,8 @@ from homeassistant.components.lock import DOMAIN as LOCK from homeassistant.components.sensor import DOMAIN as SENSOR from homeassistant.components.switch import DOMAIN as SWITCH +# importing channels updates registries +from . import channels # noqa pylint: disable=wrong-import-position,unused-import from .const import ( CONTROLLER, SENSOR_ACCELERATION, @@ -63,9 +77,6 @@ COMPONENT_CLUSTERS = { ZIGBEE_CHANNEL_REGISTRY = DictRegistry() -# importing channels updates registries -from . import channels # noqa pylint: disable=wrong-import-position,unused-import - def establish_device_mappings(): """Establish mappings between ZCL objects and HA ZHA objects. @@ -73,56 +84,27 @@ def establish_device_mappings(): These cannot be module level, as importing bellows must be done in a in a function. """ - from zigpy import zcl - from zigpy.profiles import zha, zll - - def get_ezsp_radio(): - import bellows.ezsp - from bellows.zigbee.application import ControllerApplication - - return {ZHA_GW_RADIO: bellows.ezsp.EZSP(), CONTROLLER: ControllerApplication} - RADIO_TYPES[RadioType.ezsp.name] = { - ZHA_GW_RADIO: get_ezsp_radio, + ZHA_GW_RADIO: bellows.ezsp.EZSP, + CONTROLLER: bellows.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "EZSP", } - def get_deconz_radio(): - import zigpy_deconz.api - from zigpy_deconz.zigbee.application import ControllerApplication - - return { - ZHA_GW_RADIO: zigpy_deconz.api.Deconz(), - CONTROLLER: ControllerApplication, - } - RADIO_TYPES[RadioType.deconz.name] = { - ZHA_GW_RADIO: get_deconz_radio, + ZHA_GW_RADIO: zigpy_deconz.api.Deconz, + CONTROLLER: zigpy_deconz.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "Deconz", } - def get_xbee_radio(): - import zigpy_xbee.api - from zigpy_xbee.zigbee.application import ControllerApplication - - return {ZHA_GW_RADIO: zigpy_xbee.api.XBee(), CONTROLLER: ControllerApplication} - RADIO_TYPES[RadioType.xbee.name] = { - ZHA_GW_RADIO: get_xbee_radio, + ZHA_GW_RADIO: zigpy_xbee.api.XBee, + CONTROLLER: zigpy_xbee.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "XBee", } - def get_zigate_radio(): - import zigpy_zigate.api - from zigpy_zigate.zigbee.application import ControllerApplication - - return { - ZHA_GW_RADIO: zigpy_zigate.api.ZiGate(), - CONTROLLER: ControllerApplication, - } - RADIO_TYPES[RadioType.zigate.name] = { - ZHA_GW_RADIO: get_zigate_radio, + ZHA_GW_RADIO: zigpy_zigate.api.ZiGate, + CONTROLLER: zigpy_zigate.zigbee.application.ControllerApplication, ZHA_GW_RADIO_DESCRIPTION: "ZiGate", } @@ -137,33 +119,33 @@ def establish_device_mappings(): } ) - DEVICE_CLASS[zha.PROFILE_ID].update( + DEVICE_CLASS[zigpy.profiles.zha.PROFILE_ID].update( { SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: DEVICE_TRACKER, - zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, - zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zha.DeviceType.DIMMABLE_BALLAST: LIGHT, - zha.DeviceType.DIMMABLE_LIGHT: LIGHT, - zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, - zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, - zha.DeviceType.ON_OFF_BALLAST: SWITCH, - zha.DeviceType.ON_OFF_LIGHT: LIGHT, - zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, - zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, - zha.DeviceType.SMART_PLUG: SWITCH, + zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: LIGHT, + zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH: SWITCH, + zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: SWITCH, + zigpy.profiles.zha.DeviceType.SMART_PLUG: SWITCH, } ) - DEVICE_CLASS[zll.PROFILE_ID].update( + DEVICE_CLASS[zigpy.profiles.zll.PROFILE_ID].update( { - zll.DeviceType.COLOR_LIGHT: LIGHT, - zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, - zll.DeviceType.DIMMABLE_LIGHT: LIGHT, - zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, - zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, - zll.DeviceType.ON_OFF_LIGHT: LIGHT, - zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, + zigpy.profiles.zll.DeviceType.COLOR_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: LIGHT, + zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: LIGHT, + zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: SWITCH, } ) @@ -207,19 +189,21 @@ def establish_device_mappings(): } ) - zhap = zha.PROFILE_ID - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_DIMMER_SWITCH) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.DIMMER_SWITCH) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.NON_COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.REMOTE_CONTROL) - REMOTE_DEVICE_TYPES[zhap].append(zha.DeviceType.SCENE_SELECTOR) + zha = zigpy.profiles.zha + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_DIMMER_SWITCH) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.COLOR_SCENE_CONTROLLER) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.DIMMER_SWITCH) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.NON_COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append( + zha.DeviceType.NON_COLOR_SCENE_CONTROLLER + ) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.REMOTE_CONTROL) + REMOTE_DEVICE_TYPES[zha.PROFILE_ID].append(zha.DeviceType.SCENE_SELECTOR) - zllp = zll.PROFILE_ID - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_CONTROLLER) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.COLOR_SCENE_CONTROLLER) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROL_BRIDGE) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.CONTROLLER) - REMOTE_DEVICE_TYPES[zllp].append(zll.DeviceType.SCENE_CONTROLLER) + zll = zigpy.profiles.zll + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_CONTROLLER) + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.COLOR_SCENE_CONTROLLER) + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROL_BRIDGE) + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.CONTROLLER) + REMOTE_DEVICE_TYPES[zll.PROFILE_ID].append(zll.DeviceType.SCENE_CONTROLLER) diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 9a559aae9b6..5f9172749b0 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -3,8 +3,12 @@ import time from unittest.mock import Mock, patch from asynctest import CoroutineMock -from zigpy.types.named import EUI64 +import zigpy.profiles.zha +import zigpy.types +import zigpy.zcl +import zigpy.zcl.clusters.general import zigpy.zcl.foundation as zcl_f +import zigpy.zdo.types from homeassistant.components.zha.core.const import ( DATA_ZHA, @@ -22,7 +26,7 @@ class FakeApplication: def __init__(self): """Init fake application.""" - self.ieee = EUI64.convert("00:15:8d:00:02:32:4f:32") + self.ieee = zigpy.types.EUI64.convert("00:15:8d:00:02:32:4f:32") self.nwk = 0x087D @@ -34,8 +38,6 @@ class FakeEndpoint: def __init__(self, manufacturer, model): """Init fake endpoint.""" - from zigpy.profiles.zha import PROFILE_ID - self.device = None self.endpoint_id = 1 self.in_clusters = {} @@ -44,14 +46,12 @@ class FakeEndpoint: self.status = 1 self.manufacturer = manufacturer self.model = model - self.profile_id = PROFILE_ID + self.profile_id = zigpy.profiles.zha.PROFILE_ID self.device_type = None def add_input_cluster(self, cluster_id): """Add an input cluster.""" - from zigpy.zcl import Cluster - - cluster = Cluster.from_id(self, cluster_id, is_server=True) + cluster = zigpy.zcl.Cluster.from_id(self, cluster_id, is_server=True) patch_cluster(cluster) self.in_clusters[cluster_id] = cluster if hasattr(cluster, "ep_attribute"): @@ -59,9 +59,7 @@ class FakeEndpoint: def add_output_cluster(self, cluster_id): """Add an output cluster.""" - from zigpy.zcl import Cluster - - cluster = Cluster.from_id(self, cluster_id, is_server=False) + cluster = zigpy.zcl.Cluster.from_id(self, cluster_id, is_server=False) patch_cluster(cluster) self.out_clusters[cluster_id] = cluster @@ -83,7 +81,7 @@ class FakeDevice: def __init__(self, ieee, manufacturer, model): """Init fake device.""" self._application = APPLICATION - self.ieee = EUI64.convert(ieee) + self.ieee = zigpy.types.EUI64.convert(ieee) self.nwk = 0xB79C self.zdo = Mock() self.endpoints = {0: self.zdo} @@ -94,9 +92,7 @@ class FakeDevice: self.initializing = False self.manufacturer = manufacturer self.model = model - from zigpy.zdo.types import NodeDescriptor - - self.node_desc = NodeDescriptor() + self.node_desc = zigpy.zdo.types.NodeDescriptor() def make_device( @@ -150,11 +146,9 @@ async def async_init_zigpy_device( def make_attribute(attrid, value, status=0): """Make an attribute.""" - from zigpy.zcl.foundation import Attribute, TypeValue - - attr = Attribute() + attr = zcl_f.Attribute() attr.attrid = attrid - attr.value = TypeValue() + attr.value = zcl_f.TypeValue() attr.value.value = value return attr @@ -202,21 +196,18 @@ async def async_test_device_join( simulate pairing a new device to the network so that code pathways that only trigger during device joins can be tested. """ - from zigpy.zcl.foundation import Status - from zigpy.zcl.clusters.general import Basic - # create zigpy device mocking out the zigbee network operations with patch( "zigpy.zcl.Cluster.configure_reporting", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): with patch( "zigpy.zcl.Cluster.bind", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): zigpy_device = await async_init_zigpy_device( hass, - [cluster_id, Basic.cluster_id], + [cluster_id, zigpy.zcl.clusters.general.Basic.cluster_id], [], device_type, zha_gateway, diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index b836c55df17..e34ad208744 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,13 +1,16 @@ """Test configuration for the ZHA component.""" from unittest.mock import patch + import pytest + from homeassistant import config_entries -from homeassistant.components.zha.core.const import DOMAIN, DATA_ZHA, COMPONENTS -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.components.zha.core.const import COMPONENTS, DATA_ZHA, DOMAIN from homeassistant.components.zha.core.gateway import ZHAGateway from homeassistant.components.zha.core.registries import establish_device_mappings -from .common import async_setup_entry from homeassistant.components.zha.core.store import async_get_registry +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg + +from .common import async_setup_entry @pytest.fixture(name="config_entry") diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index ae8e460b613..3fea9dfe088 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,33 +1,39 @@ """Test ZHA API.""" import pytest +import zigpy.zcl.clusters.general as general + from homeassistant.components.switch import DOMAIN -from homeassistant.components.zha.api import async_load_api, TYPE, ID +from homeassistant.components.websocket_api import const +from homeassistant.components.zha.api import ID, TYPE, async_load_api from homeassistant.components.zha.core.const import ( ATTR_CLUSTER_ID, ATTR_CLUSTER_TYPE, - CLUSTER_TYPE_IN, + ATTR_ENDPOINT_ID, ATTR_IEEE, + ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, ATTR_QUIRK_APPLIED, - ATTR_MANUFACTURER, - ATTR_ENDPOINT_ID, + CLUSTER_TYPE_IN, ) -from homeassistant.components.websocket_api import const + from .common import async_init_zigpy_device @pytest.fixture async def zha_client(hass, config_entry, zha_gateway, hass_ws_client): """Test zha switch platform.""" - from zigpy.zcl.clusters.general import OnOff, Basic # load the ZHA API async_load_api(hass) # create zigpy device await async_init_zigpy_device( - hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [general.OnOff.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) # load up switch domain diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 4fca5505d29..89dc1ae25a6 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,5 +1,8 @@ """Test zha binary sensor.""" -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.measurement as measurement +import zigpy.zcl.clusters.security as security +import zigpy.zcl.foundation as zcl_f from homeassistant.components.binary_sensor import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -16,18 +19,19 @@ from .common import ( async def test_binary_sensor(hass, config_entry, zha_gateway): """Test zha binary_sensor platform.""" - from zigpy.zcl.clusters.security import IasZone - from zigpy.zcl.clusters.measurement import OccupancySensing - from zigpy.zcl.clusters.general import Basic # create zigpy devices zigpy_device_zone = await async_init_zigpy_device( - hass, [IasZone.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [security.IasZone.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) zigpy_device_occupancy = await async_init_zigpy_device( hass, - [OccupancySensing.cluster_id, Basic.cluster_id], + [measurement.OccupancySensing.cluster_id, general.Basic.cluster_id], [], None, zha_gateway, @@ -71,14 +75,16 @@ async def test_binary_sensor(hass, config_entry, zha_gateway): await async_test_iaszone_on_off(hass, zone_cluster, zone_entity_id) # test new sensor join - await async_test_device_join(hass, zha_gateway, OccupancySensing.cluster_id, DOMAIN) + await async_test_device_join( + hass, zha_gateway, measurement.OccupancySensing.cluster_id, DOMAIN + ) async def async_test_binary_sensor_on_off(hass, cluster, entity_id): """Test getting on and off messages for binary sensors.""" # binary sensor on attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 25b0910931a..5e6bf51afd6 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,7 +1,9 @@ """Tests for ZHA config flow.""" from asynctest import patch + from homeassistant.components.zha import config_flow from homeassistant.components.zha.core.const import DOMAIN + from tests.common import MockConfigEntry diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 91049a9bfa8..62884fe72ae 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -2,6 +2,9 @@ from unittest.mock import patch import pytest +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.security as security +import zigpy.zcl.foundation as zcl_f import homeassistant.components.automation as automation from homeassistant.components.device_automation import ( @@ -29,13 +32,15 @@ def calls(hass): async def test_get_actions(hass, config_entry, zha_gateway): """Test we get the expected actions from a zha device.""" - from zigpy.zcl.clusters.general import Basic - from zigpy.zcl.clusters.security import IasZone, IasWd # create zigpy device zigpy_device = await async_init_zigpy_device( hass, - [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], + [ + general.Basic.cluster_id, + security.IasZone.cluster_id, + security.IasWd.cluster_id, + ], [], None, zha_gateway, @@ -64,15 +69,15 @@ async def test_get_actions(hass, config_entry, zha_gateway): async def test_action(hass, config_entry, zha_gateway, calls): """Test for executing a zha device action.""" - from zigpy.zcl.clusters.general import Basic, OnOff - from zigpy.zcl.clusters.security import IasZone, IasWd - from zigpy.zcl.foundation import Status - # create zigpy device zigpy_device = await async_init_zigpy_device( hass, - [Basic.cluster_id, IasZone.cluster_id, IasWd.cluster_id], - [OnOff.cluster_id], + [ + general.Basic.cluster_id, + security.IasZone.cluster_id, + security.IasWd.cluster_id, + ], + [general.OnOff.cluster_id], None, zha_gateway, ) @@ -96,7 +101,8 @@ async def test_action(hass, config_entry, zha_gateway, calls): await async_enable_traffic(hass, zha_gateway, [zha_device]) with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), ): assert await async_setup_component( hass, diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 0c27c1514f1..446920eb2f9 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -2,7 +2,8 @@ from datetime import timedelta import time -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER from homeassistant.components.zha.core.registries import ( @@ -25,26 +26,18 @@ from tests.common import async_fire_time_changed async def test_device_tracker(hass, config_entry, zha_gateway): """Test zha device tracker platform.""" - from zigpy.zcl.clusters.general import ( - Basic, - PowerConfiguration, - BinaryInput, - Identify, - Ota, - PollControl, - ) # create zigpy device zigpy_device = await async_init_zigpy_device( hass, [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - PollControl.cluster_id, - BinaryInput.cluster_id, + general.Basic.cluster_id, + general.PowerConfiguration.cluster_id, + general.Identify.cluster_id, + general.PollControl.cluster_id, + general.BinaryInput.cluster_id, ], - [Identify.cluster_id, Ota.cluster_id], + [general.Identify.cluster_id, general.Ota.cluster_id], SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, zha_gateway, ) @@ -73,7 +66,7 @@ async def test_device_tracker(hass, config_entry, zha_gateway): # turn state flip attr = make_attribute(0x0020, 23) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) attr = make_attribute(0x0021, 200) @@ -96,7 +89,7 @@ async def test_device_tracker(hass, config_entry, zha_gateway): await async_test_device_join( hass, zha_gateway, - PowerConfiguration.cluster_id, + general.PowerConfiguration.cluster_id, DOMAIN, SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, ) diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 8df1a072801..75e8538c5bf 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,5 +1,6 @@ """ZHA device automation trigger tests.""" import pytest +import zigpy.zcl.clusters.general as general import homeassistant.components.automation as automation from homeassistant.components.switch import DOMAIN @@ -9,7 +10,7 @@ from homeassistant.setup import async_setup_component from .common import async_enable_traffic, async_init_zigpy_device -from tests.common import async_mock_service, async_get_device_automations +from tests.common import async_get_device_automations, async_mock_service ON = 1 OFF = 0 @@ -43,11 +44,10 @@ def calls(hass): async def test_triggers(hass, config_entry, zha_gateway): """Test zha device triggers.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) zigpy_device.device_automation_triggers = { @@ -112,11 +112,10 @@ async def test_triggers(hass, config_entry, zha_gateway): async def test_no_triggers(hass, config_entry, zha_gateway): """Test zha device with no triggers.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) @@ -135,11 +134,10 @@ async def test_no_triggers(hass, config_entry, zha_gateway): async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): """Test for remote triggers firing.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) zigpy_device.device_automation_triggers = { @@ -197,11 +195,10 @@ async def test_if_fires_on_event(hass, config_entry, zha_gateway, calls): async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, caplog): """Test for exception on event triggers firing.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN) @@ -244,11 +241,10 @@ async def test_exception_no_triggers(hass, config_entry, zha_gateway, calls, cap async def test_exception_bad_trigger(hass, config_entry, zha_gateway, calls, caplog): """Test for exception on event triggers firing.""" - from zigpy.zcl.clusters.general import OnOff, Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Basic.cluster_id], [OnOff.cluster_id], None, zha_gateway + hass, [general.Basic.cluster_id], [general.OnOff.cluster_id], None, zha_gateway ) zigpy_device.device_automation_triggers = { diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 1704ab2196b..a196ba50ba7 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,7 +1,9 @@ """Test zha fan.""" from unittest.mock import call, patch -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.hvac as hvac +import zigpy.zcl.foundation as zcl_f from homeassistant.components import fan from homeassistant.components.fan import ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED @@ -28,13 +30,10 @@ from tests.common import mock_coro async def test_fan(hass, config_entry, zha_gateway): """Test zha fan platform.""" - from zigpy.zcl.clusters.hvac import Fan - from zigpy.zcl.clusters.general import Basic - from zigpy.zcl.foundation import Status # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [Fan.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, [hvac.Fan.cluster_id, general.Basic.cluster_id], [], None, zha_gateway ) # load up fan domain @@ -56,7 +55,7 @@ async def test_fan(hass, config_entry, zha_gateway): # turn on at fan attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -70,7 +69,7 @@ async def test_fan(hass, config_entry, zha_gateway): # turn on from HA with patch( "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): # turn on via UI await async_turn_on(hass, entity_id) @@ -80,7 +79,7 @@ async def test_fan(hass, config_entry, zha_gateway): # turn off from HA with patch( "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): # turn off via UI await async_turn_off(hass, entity_id) @@ -90,7 +89,7 @@ async def test_fan(hass, config_entry, zha_gateway): # change speed from HA with patch( "zigpy.zcl.Cluster.write_attributes", - return_value=mock_coro([Status.SUCCESS, Status.SUCCESS]), + return_value=mock_coro([zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS]), ): # turn on via UI await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH) @@ -98,7 +97,7 @@ async def test_fan(hass, config_entry, zha_gateway): assert cluster.write_attributes.call_args == call({"fan_mode": 3}) # test adding new fan to the network and HA - await async_test_device_join(hass, zha_gateway, Fan.cluster_id, DOMAIN) + await async_test_device_join(hass, zha_gateway, hvac.Fan.cluster_id, DOMAIN) async def async_turn_on(hass, entity_id, speed=None): diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 567f61ad1e1..f0d9d4913e6 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -2,7 +2,10 @@ import asyncio from unittest.mock import MagicMock, call, patch, sentinel -from zigpy.zcl.foundation import Command +import zigpy.profiles.zha +import zigpy.types +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f from homeassistant.components.light import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -24,24 +27,25 @@ OFF = 0 async def test_light(hass, config_entry, zha_gateway, monkeypatch): """Test zha light platform.""" - from zigpy.zcl.clusters.general import OnOff, LevelControl, Basic - from zigpy.zcl.foundation import Status - from zigpy.profiles.zha import DeviceType # create zigpy devices zigpy_device_on_off = await async_init_zigpy_device( hass, - [OnOff.cluster_id, Basic.cluster_id], + [general.OnOff.cluster_id, general.Basic.cluster_id], [], - DeviceType.ON_OFF_LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, zha_gateway, ) zigpy_device_level = await async_init_zigpy_device( hass, - [OnOff.cluster_id, LevelControl.cluster_id, Basic.cluster_id], + [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + general.Basic.cluster_id, + ], [], - DeviceType.ON_OFF_LIGHT, + zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, zha_gateway, ieee="00:0d:6f:11:0a:90:69:e7", manufacturer="FakeLevelManufacturer", @@ -64,12 +68,12 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): level_device_level_cluster = zigpy_device_level.endpoints.get(1).level on_off_mock = MagicMock( side_effect=asyncio.coroutine( - MagicMock(return_value=[sentinel.data, Status.SUCCESS]) + MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]) ) ) level_mock = MagicMock( side_effect=asyncio.coroutine( - MagicMock(return_value=[sentinel.data, Status.SUCCESS]) + MagicMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]) ) ) monkeypatch.setattr(level_device_on_off_cluster, "request", on_off_mock) @@ -118,7 +122,11 @@ async def test_light(hass, config_entry, zha_gateway, monkeypatch): # test adding a new light to the network and HA await async_test_device_join( - hass, zha_gateway, OnOff.cluster_id, DOMAIN, device_type=DeviceType.ON_OFF_LIGHT + hass, + zha_gateway, + general.OnOff.cluster_id, + DOMAIN, + device_type=zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, ) @@ -126,7 +134,7 @@ async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -142,7 +150,7 @@ async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -150,10 +158,9 @@ async def async_test_on_from_light(hass, cluster, entity_id): async def async_test_on_off_from_hass(hass, cluster, entity_id): """Test on off functionality from hass.""" - from zigpy.zcl.foundation import Status - with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), ): # turn on via UI await hass.services.async_call( @@ -169,10 +176,9 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): async def async_test_off_from_hass(hass, cluster, entity_id): """Test turning off the light from homeassistant.""" - from zigpy.zcl.foundation import Status - with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x01, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), ): # turn off via UI await hass.services.async_call( @@ -188,7 +194,6 @@ async def async_test_level_on_off_from_hass( hass, on_off_cluster, level_cluster, entity_id ): """Test on off functionality from hass.""" - from zigpy import types # turn on via UI await hass.services.async_call( @@ -213,7 +218,7 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_args == call( False, 4, - (types.uint8_t, types.uint16_t), + (zigpy.types.uint8_t, zigpy.types.uint16_t), 254, 100.0, expect_reply=True, @@ -233,7 +238,7 @@ async def async_test_level_on_off_from_hass( assert level_cluster.request.call_args == call( False, 4, - (types.uint8_t, types.uint16_t), + (zigpy.types.uint8_t, zigpy.types.uint16_t), 10, 0, expect_reply=True, @@ -248,7 +253,7 @@ async def async_test_level_on_off_from_hass( async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): """Test dimmer functionality from the light.""" attr = make_attribute(0, level) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == expected_state diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index c7cc5bdd2a9..118526a1d85 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -1,7 +1,9 @@ """Test zha lock.""" from unittest.mock import patch -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.closures as closures +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f from homeassistant.components.lock import DOMAIN from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED @@ -22,12 +24,14 @@ UNLOCK_DOOR = 1 async def test_lock(hass, config_entry, zha_gateway): """Test zha lock platform.""" - from zigpy.zcl.clusters.closures import DoorLock - from zigpy.zcl.clusters.general import Basic # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [DoorLock.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [closures.DoorLock.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) # load up lock domain @@ -49,7 +53,7 @@ async def test_lock(hass, config_entry, zha_gateway): # set state to locked attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_LOCKED @@ -69,9 +73,9 @@ async def test_lock(hass, config_entry, zha_gateway): async def async_lock(hass, cluster, entity_id): """Test lock functionality from hass.""" - from zigpy.zcl.foundation import Status - - with patch("zigpy.zcl.Cluster.request", return_value=mock_coro([Status.SUCCESS])): + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS]) + ): # lock via UI await hass.services.async_call( DOMAIN, "lock", {"entity_id": entity_id}, blocking=True @@ -83,9 +87,9 @@ async def async_lock(hass, cluster, entity_id): async def async_unlock(hass, cluster, entity_id): """Test lock functionality from hass.""" - from zigpy.zcl.foundation import Status - - with patch("zigpy.zcl.Cluster.request", return_value=mock_coro([Status.SUCCESS])): + with patch( + "zigpy.zcl.Cluster.request", return_value=mock_coro([zcl_f.Status.SUCCESS]) + ): # lock via UI await hass.services.async_call( DOMAIN, "unlock", {"entity_id": entity_id}, blocking=True diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 37d412e6a25..dec551f8d62 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,5 +1,9 @@ """Test zha sensor.""" -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.homeautomation as homeautomation +import zigpy.zcl.clusters.measurement as measurement +import zigpy.zcl.clusters.smartenergy as smartenergy +import zigpy.zcl.foundation as zcl_f from homeassistant.components.sensor import DOMAIN from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN @@ -16,23 +20,15 @@ from .common import ( async def test_sensor(hass, config_entry, zha_gateway): """Test zha sensor platform.""" - from zigpy.zcl.clusters.measurement import ( - RelativeHumidity, - TemperatureMeasurement, - PressureMeasurement, - IlluminanceMeasurement, - ) - from zigpy.zcl.clusters.smartenergy import Metering - from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement # list of cluster ids to create devices and sensor entities for cluster_ids = [ - RelativeHumidity.cluster_id, - TemperatureMeasurement.cluster_id, - PressureMeasurement.cluster_id, - IlluminanceMeasurement.cluster_id, - Metering.cluster_id, - ElectricalMeasurement.cluster_id, + measurement.RelativeHumidity.cluster_id, + measurement.TemperatureMeasurement.cluster_id, + measurement.PressureMeasurement.cluster_id, + measurement.IlluminanceMeasurement.cluster_id, + smartenergy.Metering.cluster_id, + homeautomation.ElectricalMeasurement.cluster_id, ] # devices that were created from cluster_ids list above @@ -63,33 +59,33 @@ async def test_sensor(hass, config_entry, zha_gateway): assert hass.states.get(entity_id).state == STATE_UNKNOWN # get the humidity device info and test the associated sensor logic - device_info = zigpy_device_infos[RelativeHumidity.cluster_id] + device_info = zigpy_device_infos[measurement.RelativeHumidity.cluster_id] await async_test_humidity(hass, device_info) # get the temperature device info and test the associated sensor logic - device_info = zigpy_device_infos[TemperatureMeasurement.cluster_id] + device_info = zigpy_device_infos[measurement.TemperatureMeasurement.cluster_id] await async_test_temperature(hass, device_info) # get the pressure device info and test the associated sensor logic - device_info = zigpy_device_infos[PressureMeasurement.cluster_id] + device_info = zigpy_device_infos[measurement.PressureMeasurement.cluster_id] await async_test_pressure(hass, device_info) # get the illuminance device info and test the associated sensor logic - device_info = zigpy_device_infos[IlluminanceMeasurement.cluster_id] + device_info = zigpy_device_infos[measurement.IlluminanceMeasurement.cluster_id] await async_test_illuminance(hass, device_info) # get the metering device info and test the associated sensor logic - device_info = zigpy_device_infos[Metering.cluster_id] + device_info = zigpy_device_infos[smartenergy.Metering.cluster_id] await async_test_metering(hass, device_info) # get the electrical_measurement device info and test the associated # sensor logic - device_info = zigpy_device_infos[ElectricalMeasurement.cluster_id] + device_info = zigpy_device_infos[homeautomation.ElectricalMeasurement.cluster_id] await async_test_electrical_measurement(hass, device_info) # test joining a new temperature sensor to the network await async_test_device_join( - hass, zha_gateway, TemperatureMeasurement.cluster_id, DOMAIN + hass, zha_gateway, measurement.TemperatureMeasurement.cluster_id, DOMAIN ) @@ -102,7 +98,6 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): A dict containing relevant device info for testing is returned. It contains the entity id, zigpy device, and the zigbee cluster for the sensor. """ - from zigpy.zcl.clusters.general import Basic device_infos = {} counter = 0 @@ -111,7 +106,7 @@ async def async_build_devices(hass, zha_gateway, config_entry, cluster_ids): device_infos[cluster_id] = {"zigpy_device": None} device_infos[cluster_id]["zigpy_device"] = await async_init_zigpy_device( hass, - [cluster_id, Basic.cluster_id], + [cluster_id, general.Basic.cluster_id], [], None, zha_gateway, @@ -181,7 +176,7 @@ async def send_attribute_report(hass, cluster, attrid, value): device is paired to the zigbee network. """ attr = make_attribute(attrid, value) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 9c2b2da9c95..bf4ff3ed628 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -1,7 +1,8 @@ """Test zha switch.""" from unittest.mock import call, patch -from zigpy.zcl.foundation import Command +import zigpy.zcl.clusters.general as general +import zigpy.zcl.foundation as zcl_f from homeassistant.components.switch import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -23,12 +24,14 @@ OFF = 0 async def test_switch(hass, config_entry, zha_gateway): """Test zha switch platform.""" - from zigpy.zcl.clusters.general import OnOff, Basic - from zigpy.zcl.foundation import Status # create zigpy device zigpy_device = await async_init_zigpy_device( - hass, [OnOff.cluster_id, Basic.cluster_id], [], None, zha_gateway + hass, + [general.OnOff.cluster_id, general.Basic.cluster_id], + [], + None, + zha_gateway, ) # load up switch domain @@ -50,7 +53,7 @@ async def test_switch(hass, config_entry, zha_gateway): # turn on at switch attr = make_attribute(0, 1) - hdr = make_zcl_header(Command.Report_Attributes) + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) cluster.handle_message(hdr, [[attr]]) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -63,7 +66,8 @@ async def test_switch(hass, config_entry, zha_gateway): # turn on from HA with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x00, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x00, zcl_f.Status.SUCCESS]), ): # turn on via UI await hass.services.async_call( @@ -76,7 +80,8 @@ async def test_switch(hass, config_entry, zha_gateway): # turn off from HA with patch( - "zigpy.zcl.Cluster.request", return_value=mock_coro([0x01, Status.SUCCESS]) + "zigpy.zcl.Cluster.request", + return_value=mock_coro([0x01, zcl_f.Status.SUCCESS]), ): # turn off via UI await hass.services.async_call( @@ -88,4 +93,4 @@ async def test_switch(hass, config_entry, zha_gateway): ) # test joining a new switch to the network and HA - await async_test_device_join(hass, zha_gateway, OnOff.cluster_id, DOMAIN) + await async_test_device_join(hass, zha_gateway, general.OnOff.cluster_id, DOMAIN) From 2cc039dbc4c0db88f863808204d25459c7a66246 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 22 Oct 2019 00:32:10 +0000 Subject: [PATCH 550/639] [ci skip] Translation update --- .../components/adguard/.translations/lb.json | 2 + .../components/adguard/.translations/nl.json | 2 + .../components/adguard/.translations/sl.json | 2 + .../components/airly/.translations/nl.json | 22 ++++++++++ .../alarm_control_panel/.translations/nl.json | 7 ++++ .../components/axis/.translations/nl.json | 1 + .../binary_sensor/.translations/nl.json | 36 +++++++++++++++++ .../binary_sensor/.translations/ru.json | 4 +- .../cert_expiry/.translations/nl.json | 2 +- .../components/ecobee/.translations/nl.json | 14 ++++++- .../components/glances/.translations/ca.json | 37 +++++++++++++++++ .../components/glances/.translations/da.json | 37 +++++++++++++++++ .../components/glances/.translations/en.json | 40 +++++++++---------- .../components/glances/.translations/fi.json | 13 ++++++ .../components/glances/.translations/fr.json | 32 +++++++++++++++ .../components/glances/.translations/ko.json | 37 +++++++++++++++++ .../components/glances/.translations/lb.json | 37 +++++++++++++++++ .../components/glances/.translations/nl.json | 37 +++++++++++++++++ .../components/glances/.translations/no.json | 31 ++++++++++++++ .../components/glances/.translations/ru.json | 37 +++++++++++++++++ .../components/glances/.translations/sl.json | 37 +++++++++++++++++ .../components/glances/.translations/th.json | 11 +++++ .../components/izone/.translations/nl.json | 15 +++++++ .../components/lock/.translations/nl.json | 5 +++ .../components/neato/.translations/nl.json | 7 ++++ .../opentherm_gw/.translations/nl.json | 19 +++++++++ .../components/plex/.translations/nl.json | 33 +++++++++++++-- .../components/plex/.translations/sl.json | 2 +- .../components/sensor/.translations/ca.json | 2 + .../components/sensor/.translations/nl.json | 13 +++++- .../components/soma/.translations/nl.json | 20 +++++++++- .../transmission/.translations/nl.json | 24 +++++++++++ .../components/zha/.translations/ko.json | 4 +- .../components/zha/.translations/nl.json | 13 ++++++ 34 files changed, 600 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/airly/.translations/nl.json create mode 100644 homeassistant/components/alarm_control_panel/.translations/nl.json create mode 100644 homeassistant/components/glances/.translations/ca.json create mode 100644 homeassistant/components/glances/.translations/da.json create mode 100644 homeassistant/components/glances/.translations/fi.json create mode 100644 homeassistant/components/glances/.translations/fr.json create mode 100644 homeassistant/components/glances/.translations/ko.json create mode 100644 homeassistant/components/glances/.translations/lb.json create mode 100644 homeassistant/components/glances/.translations/nl.json create mode 100644 homeassistant/components/glances/.translations/no.json create mode 100644 homeassistant/components/glances/.translations/ru.json create mode 100644 homeassistant/components/glances/.translations/sl.json create mode 100644 homeassistant/components/glances/.translations/th.json create mode 100644 homeassistant/components/izone/.translations/nl.json diff --git a/homeassistant/components/adguard/.translations/lb.json b/homeassistant/components/adguard/.translations/lb.json index cc3ecf5db87..e449f668fd9 100644 --- a/homeassistant/components/adguard/.translations/lb.json +++ b/homeassistant/components/adguard/.translations/lb.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "D\u00ebs Integratioun ben\u00e9idegt AdgGuard Home {minimal_version} oder m\u00e9i, dir hutt {current_version}. Aktualis\u00e9iert w.e.g. \u00e4ren Hass.io AdGuard Home Add-on.", + "adguard_home_outdated": "D\u00ebs Integratioun ben\u00e9idegt AdgGuard Home {minimal_version} oder m\u00e9i, dir hutt {current_version}.", "existing_instance_updated": "D\u00e9i bestehend Konfiguratioun ass ge\u00e4nnert.", "single_instance_allowed": "N\u00ebmmen eng eenzeg Konfiguratioun vun AdGuard Home ass erlaabt." }, diff --git a/homeassistant/components/adguard/.translations/nl.json b/homeassistant/components/adguard/.translations/nl.json index 3ef86c30a3f..bd0dcc5fa43 100644 --- a/homeassistant/components/adguard/.translations/nl.json +++ b/homeassistant/components/adguard/.translations/nl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Deze integratie vereist AdGuard Home {minimal_version} of hoger, u heeft {current_version}. Update uw Hass.io AdGuard Home-add-on.", + "adguard_home_outdated": "Deze integratie vereist AdGuard Home {minimal_version} of hoger, u heeft {current_version}.", "existing_instance_updated": "Bestaande configuratie bijgewerkt.", "single_instance_allowed": "Slechts \u00e9\u00e9n configuratie van AdGuard Home is toegestaan." }, diff --git a/homeassistant/components/adguard/.translations/sl.json b/homeassistant/components/adguard/.translations/sl.json index f1ca796363d..974524c932d 100644 --- a/homeassistant/components/adguard/.translations/sl.json +++ b/homeassistant/components/adguard/.translations/sl.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Za to integracijo je potrebna AdGuard Home {minimal_version} ali vi\u0161ja, vi imate {current_version}. Prosimo posodobite va\u0161 hass.io AdGuard Home dodatek.", + "adguard_home_outdated": "Za to integracijo je potrebna AdGuard Home {minimal_version} ali vi\u0161ja, vi imate {current_version}.", "existing_instance_updated": "Posodobljena obstoje\u010da konfiguracija.", "single_instance_allowed": "Dovoljena je samo ena konfiguracija AdGuard Home." }, diff --git a/homeassistant/components/airly/.translations/nl.json b/homeassistant/components/airly/.translations/nl.json new file mode 100644 index 00000000000..232d5d54d85 --- /dev/null +++ b/homeassistant/components/airly/.translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "auth": "API-sleutel is niet correct.", + "name_exists": "Naam bestaat al.", + "wrong_location": "Geen Airly meetstations in dit gebied." + }, + "step": { + "user": { + "data": { + "api_key": "Airly API-sleutel", + "latitude": "Breedtegraad", + "longitude": "Lengtegraad", + "name": "Naam van de integratie" + }, + "description": "Airly-integratie van luchtkwaliteit instellen. Ga naar https://developer.airly.eu/register om de API-sleutel te genereren", + "title": "Airly" + } + }, + "title": "Airly" + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/nl.json b/homeassistant/components/alarm_control_panel/.translations/nl.json new file mode 100644 index 00000000000..9f4e6a4a51c --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/nl.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "disarm": "Uitschakelen {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/nl.json b/homeassistant/components/axis/.translations/nl.json index 83395283404..10fc8c02d66 100644 --- a/homeassistant/components/axis/.translations/nl.json +++ b/homeassistant/components/axis/.translations/nl.json @@ -12,6 +12,7 @@ "device_unavailable": "Apparaat is niet beschikbaar", "faulty_credentials": "Ongeldige gebruikersreferenties" }, + "flow_title": "Axis apparaat: {naam} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/.translations/nl.json b/homeassistant/components/binary_sensor/.translations/nl.json index 92cadf79aa8..508a06b38a2 100644 --- a/homeassistant/components/binary_sensor/.translations/nl.json +++ b/homeassistant/components/binary_sensor/.translations/nl.json @@ -6,6 +6,11 @@ "is_connected": "{entity_name} is verbonden", "is_gas": "{entity_name} detecteert gas", "is_hot": "{entity_name} is hot", + "is_light": "{entity_name} detecteert licht", + "is_locked": "{entity_name} is vergrendeld", + "is_moist": "{entity_name} is vochtig", + "is_motion": "{entity_name} detecteert beweging", + "is_moving": "{entity_name} is in beweging", "is_no_gas": "{entity_name} detecteert geen gas", "is_no_light": "{entity_name} detecteert geen licht", "is_no_motion": "{entity_name} detecteert geen beweging", @@ -17,10 +22,21 @@ "is_not_cold": "{entity_name} is niet koud", "is_not_connected": "{entity_name} is niet verbonden", "is_not_hot": "{entity_name} is niet heet", + "is_not_locked": "{entity_name} is ontgrendeld", + "is_not_moist": "{entity_name} is droog", + "is_not_moving": "{entity_name} beweegt niet", "is_not_occupied": "{entity_name} is niet bezet", + "is_not_open": "{entity_name} is gesloten", + "is_not_plugged_in": "{entity_name} is niet aangesloten", + "is_not_powered": "{entity_name} is niet van stroom voorzien...", "is_not_present": "{entity_name} is niet aanwezig", "is_not_unsafe": "{entity_name} is veilig", "is_occupied": "{entity_name} bezet is", + "is_off": "{entity_name} is uitgeschakeld", + "is_on": "{entity_name} is ingeschakeld", + "is_open": "{entity_name} is open", + "is_plugged_in": "{entity_name} is aangesloten", + "is_powered": "{entity_name} is van stroom voorzien....", "is_present": "{entity_name} is aanwezig", "is_problem": "{entity_name} detecteert een probleem", "is_smoke": "{entity_name} detecteert rook", @@ -31,27 +47,47 @@ "trigger_type": { "bat_low": "{entity_name} batterij bijna leeg", "closed": "{entity_name} gesloten", + "cold": "{entity_name} werd koud", "connected": "{entity_name} verbonden", + "gas": "{entity_name} begon gas te detecteren", + "hot": "{entity_name} werd heet", "light": "{entity_name} begon licht te detecteren", "locked": "{entity_name} vergrendeld", "moist": "{entity_name} werd vochtig", + "moist\u00a7": "{entity_name} werd vochtig", "motion": "{entity_name} begon beweging te detecteren", + "moving": "{entity_name} begon te bewegen", + "no_gas": "{entity_name} is gestopt met het detecteren van gas", + "no_light": "{entity_name} gestopt met het detecteren van licht", + "no_motion": "{entity_name} gestopt met het detecteren van beweging", + "no_problem": "{entity_name} gestopt met het detecteren van het probleem", + "no_smoke": "{entity_name} gestopt met het detecteren van rook", + "no_sound": "{entity_name} gestopt met het detecteren van geluid", + "no_vibration": "{entity_name} gestopt met het detecteren van trillingen", "not_bat_low": "{entity_name} batterij normaal", + "not_cold": "{entity_name} werd niet koud", "not_connected": "{entity_name} verbroken", + "not_hot": "{entity_name} werd niet warm", "not_locked": "{entity_name} ontgrendeld", + "not_moist": "{entity_name} werd droog", + "not_moving": "{entity_name} gestopt met bewegen", "not_occupied": "{entity_name} werd niet bezet", "not_opened": "{entity_name} gesloten", "not_plugged_in": "{entity_name} niet verbonden", "not_powered": "{entity_name} niet ingeschakeld", + "not_present": "{entity_name} is niet aanwezig", + "not_unsafe": "{entity_name} werd veilig", "occupied": "{entity_name} werd bezet", "opened": "{entity_name} geopend", "plugged_in": "{entity_name} aangesloten", "powered": "{entity_name} heeft vermogen", + "present": "{entity_name} aanwezig", "problem": "{entity_name} begonnen met het detecteren van een probleem", "smoke": "{entity_name} begon rook te detecteren", "sound": "{entity_name} begon geluid te detecteren", "turned_off": "{entity_name} uitgeschakeld", "turned_on": "{entity_name} ingeschakeld", + "unsafe": "{entity_name} werd onveilig", "vibration": "{entity_name} begon trillingen te detecteren" } } diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json index a7ecced3f11..cce765c8d84 100644 --- a/homeassistant/components/binary_sensor/.translations/ru.json +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -74,13 +74,13 @@ "not_occupied": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "not_plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u044d\u043d\u0435\u0440\u0433\u0438\u0438", + "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", "not_present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "not_unsafe": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u043e\u0441\u0442\u044c", "occupied": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "opened": "{entity_name} \u043e\u0442\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "plugged_in": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435", - "powered": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u044d\u043d\u0435\u0440\u0433\u0438\u0438", + "powered": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u0435 \u043f\u0438\u0442\u0430\u043d\u0438\u044f", "present": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043f\u0440\u0438\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0435", "problem": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", "smoke": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u044b\u043c", diff --git a/homeassistant/components/cert_expiry/.translations/nl.json b/homeassistant/components/cert_expiry/.translations/nl.json index d2fe3c76e85..40316f008d5 100644 --- a/homeassistant/components/cert_expiry/.translations/nl.json +++ b/homeassistant/components/cert_expiry/.translations/nl.json @@ -5,7 +5,7 @@ }, "error": { "certificate_fetch_failed": "Kan certificaat niet ophalen van deze combinatie van host en poort", - "connection_timeout": "Timeout bij verbinding maken met deze host", + "connection_timeout": "Time-out bij verbinding maken met deze host", "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd", "resolve_failed": "Deze host kon niet gevonden worden" }, diff --git a/homeassistant/components/ecobee/.translations/nl.json b/homeassistant/components/ecobee/.translations/nl.json index b2e3ce9cdd7..56bb3ace26f 100644 --- a/homeassistant/components/ecobee/.translations/nl.json +++ b/homeassistant/components/ecobee/.translations/nl.json @@ -4,12 +4,22 @@ "one_instance_only": "Deze integratie ondersteunt momenteel slechts \u00e9\u00e9n ecobee-instantie." }, "error": { + "pin_request_failed": "Fout bij het aanvragen van pincode bij ecobee; Controleer of de API-sleutel correct is.", "token_request_failed": "Fout bij het aanvragen van tokens bij ecobee; probeer het opnieuw." }, "step": { "authorize": { - "description": "Autoriseer deze app op https://www.ecobee.com/consumerportal/index.html met pincode: \n\n {pin} \n \nDruk vervolgens op Submit." + "description": "Autoriseer deze app op https://www.ecobee.com/consumerportal/index.html met pincode: \n\n {pin} \n \nDruk vervolgens op Submit.", + "title": "Autoriseer app op ecobee.com" + }, + "user": { + "data": { + "api_key": "API-sleutel" + }, + "description": "Voer de API-sleutel in die u van ecobee.com hebt gekregen.", + "title": "ecobee API-sleutel" } - } + }, + "title": "ecobee" } } \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/ca.json b/homeassistant/components/glances/.translations/ca.json new file mode 100644 index 00000000000..edff236623e --- /dev/null +++ b/homeassistant/components/glances/.translations/ca.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat." + }, + "error": { + "cannot_connect": "No s'ha pogut connectar amb l'amfitri\u00f3", + "wrong_version": "Versi\u00f3 no compatible (2 o 3 necess\u00e0ria)" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "ssl": "Utilitza SSL/TLS per connectar-te al sistema Glances", + "username": "Nom d'usuari", + "verify_ssl": "Verifica la certificaci\u00f3 del sistema", + "version": "Versi\u00f3 de l'API de Glances (2 o 3)" + }, + "title": "Configuraci\u00f3 de Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" + }, + "description": "Opcions de configuraci\u00f3 per a Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/da.json b/homeassistant/components/glances/.translations/da.json new file mode 100644 index 00000000000..7779c6e40a0 --- /dev/null +++ b/homeassistant/components/glances/.translations/da.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "V\u00e6rten er allerede konfigureret." + }, + "error": { + "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", + "wrong_version": "Version underst\u00f8ttes ikke (kun 2 eller 3)" + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rt", + "name": "Navn", + "password": "Adgangskode", + "port": "Port", + "ssl": "Brug SSL/TLS til at oprette forbindelse til Glances-systemet", + "username": "Brugernavn", + "verify_ssl": "Bekr\u00e6ft certificering af systemet", + "version": "Glances API version (2 eller 3)" + }, + "title": "Ops\u00e6tning af Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Opdateringsfrekvens" + }, + "description": "Konfigurationsindstillinger for Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/en.json b/homeassistant/components/glances/.translations/en.json index 1bd7275daef..ef1a8fb5e31 100644 --- a/homeassistant/components/glances/.translations/en.json +++ b/homeassistant/components/glances/.translations/en.json @@ -1,36 +1,36 @@ { "config": { - "title": "Glances", - "step": { - "user": { - "title": "Setup Glances", - "data": { - "name": "Name", - "host": "Host", - "username": "Username", - "password": "Password", - "port": "Port", - "version": "Glances API Version (2 or 3)", - "ssl": "Use SSL/TLS to connect to the Glances system", - "verify_ssl": "Verify the certification of the system" - } - } + "abort": { + "already_configured": "Host is already configured." }, "error": { "cannot_connect": "Unable to connect to host", "wrong_version": "Version not supported (2 or 3 only)" }, - "abort": { - "already_configured": "Host is already configured." - } + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port", + "ssl": "Use SSL/TLS to connect to the Glances system", + "username": "Username", + "verify_ssl": "Verify the certification of the system", + "version": "Glances API Version (2 or 3)" + }, + "title": "Setup Glances" + } + }, + "title": "Glances" }, "options": { "step": { "init": { - "description": "Configure options for Glances", "data": { "scan_interval": "Update frequency" - } + }, + "description": "Configure options for Glances" } } } diff --git a/homeassistant/components/glances/.translations/fi.json b/homeassistant/components/glances/.translations/fi.json new file mode 100644 index 00000000000..43ccf405d14 --- /dev/null +++ b/homeassistant/components/glances/.translations/fi.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Nimi", + "password": "Salasana", + "port": "portti" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/fr.json b/homeassistant/components/glances/.translations/fr.json new file mode 100644 index 00000000000..d7b3dc8a448 --- /dev/null +++ b/homeassistant/components/glances/.translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9." + }, + "error": { + "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te", + "wrong_version": "Version non prise en charge (2 ou 3 uniquement)" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "name": "Nom", + "password": "Mot de passe", + "port": "Port", + "username": "Nom d'utilisateur", + "verify_ssl": "V\u00e9rifier la certification du syst\u00e8me" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/ko.json b/homeassistant/components/glances/.translations/ko.json new file mode 100644 index 00000000000..ad19b589d5d --- /dev/null +++ b/homeassistant/components/glances/.translations/ko.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "wrong_version": "\ud574\ub2f9 \ubc84\uc804\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4 (2 \ub610\ub294 3\ub9cc \uc9c0\uc6d0)" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec Glances \uc2dc\uc2a4\ud15c\uc5d0 \uc5f0\uacb0", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984", + "verify_ssl": "\uc2dc\uc2a4\ud15c \uc778\uc99d \ud655\uc778", + "version": "Glances API \ubc84\uc804 (2 \ub610\ub294 3)" + }, + "title": "Glances \uc124\uce58" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" + }, + "description": "Glances \uc635\uc158 \uad6c\uc131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/lb.json b/homeassistant/components/glances/.translations/lb.json new file mode 100644 index 00000000000..06723a4bd12 --- /dev/null +++ b/homeassistant/components/glances/.translations/lb.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Kann sech net mam Server verbannen.", + "wrong_version": "Versioun net \u00ebnnerst\u00ebtzt (n\u00ebmmen 2 oder 3)" + }, + "step": { + "user": { + "data": { + "host": "Apparat", + "name": "Numm", + "password": "Passwuert", + "port": "Port", + "ssl": "Benotzt SSL/TLS fir sech mam Usiichte System ze verbannen", + "username": "Benotzernumm", + "verify_ssl": "Zertifikatioun vum System iwwerpr\u00e9iwen", + "version": "API Versioun vun den Usiichten (2 oder 3)" + }, + "title": "Usiichten konfigur\u00e9ieren" + } + }, + "title": "Usiichten" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervalle vun de Mise \u00e0 jour" + }, + "description": "Optioune konfigur\u00e9ieren fir d'Usiichten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/nl.json b/homeassistant/components/glances/.translations/nl.json new file mode 100644 index 00000000000..7de81bfee98 --- /dev/null +++ b/homeassistant/components/glances/.translations/nl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Host is al geconfigureerd." + }, + "error": { + "cannot_connect": "Kan geen verbinding maken met host", + "wrong_version": "Versie niet ondersteund (alleen 2 of 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort", + "ssl": "Gebruik SSL / TLS om verbinding te maken met het Glances-systeem", + "username": "Gebruikersnaam", + "verify_ssl": "Controleer de certificering van het systeem", + "version": "Glances API-versie (2 of 3)" + }, + "title": "Glances instellen" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequentie" + }, + "description": "Configureer opties voor Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/no.json b/homeassistant/components/glances/.translations/no.json new file mode 100644 index 00000000000..7d742cad262 --- /dev/null +++ b/homeassistant/components/glances/.translations/no.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Verten er allerede konfigurert." + }, + "error": { + "cannot_connect": "Kan ikke koble til vert", + "wrong_version": "Versjonen st\u00f8ttes ikke (bare 2 eller 3)" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdater frekvens" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/ru.json b/homeassistant/components/glances/.translations/ru.json new file mode 100644 index 00000000000..597a914a88d --- /dev/null +++ b/homeassistant/components/glances/.translations/ru.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", + "wrong_version": "\u041f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442\u0441\u044f \u0442\u043e\u043b\u044c\u043a\u043e \u0432\u0435\u0440\u0441\u0438\u0438 2 \u0438 3." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL / TLS \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "username": "\u041b\u043e\u0433\u0438\u043d", + "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044e \u0441\u0438\u0441\u0442\u0435\u043c\u044b", + "version": "\u0412\u0435\u0440\u0441\u0438\u044f API Glances (2 \u0438\u043b\u0438 3)" + }, + "title": "Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" + }, + "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/sl.json b/homeassistant/components/glances/.translations/sl.json new file mode 100644 index 00000000000..b1d0fda94b5 --- /dev/null +++ b/homeassistant/components/glances/.translations/sl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Gostitelj je \u017ee konfiguriran." + }, + "error": { + "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem", + "wrong_version": "Razli\u010dica ni podprta (samo 2 ali 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Ime", + "password": "Geslo", + "port": "Vrata", + "ssl": "Za povezavo s sistemom Glances uporabite SSL/TLS", + "username": "Uporabni\u0161ko ime", + "verify_ssl": "Preverite veljavnost potrdila sistema", + "version": "Glances API Version (2 ali 3)" + }, + "title": "Nastavite Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Pogostost posodabljanja" + }, + "description": "Konfiguracija mo\u017enosti za Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/th.json b/homeassistant/components/glances/.translations/th.json new file mode 100644 index 00000000000..718c857c490 --- /dev/null +++ b/homeassistant/components/glances/.translations/th.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "\u0e0a\u0e37\u0e48\u0e2d\u0e1c\u0e39\u0e49\u0e43\u0e0a\u0e49" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/.translations/nl.json b/homeassistant/components/izone/.translations/nl.json new file mode 100644 index 00000000000..979441f7288 --- /dev/null +++ b/homeassistant/components/izone/.translations/nl.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "Geen iZone-apparaten gevonden op het netwerk.", + "single_instance_allowed": "Er is slechts \u00e9\u00e9n configuratie van iZone nodig." + }, + "step": { + "confirm": { + "description": "Wilt u iZone instellen?", + "title": "iZone" + } + }, + "title": "iZone" + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/.translations/nl.json b/homeassistant/components/lock/.translations/nl.json index 6a39f9cbf58..099b7308334 100644 --- a/homeassistant/components/lock/.translations/nl.json +++ b/homeassistant/components/lock/.translations/nl.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Vergrendel {entity_name}", + "open": "Open {entity_name}", + "unlock": "Ontgrendel {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} is vergrendeld", "is_unlocked": "{entity_name} is ontgrendeld" diff --git a/homeassistant/components/neato/.translations/nl.json b/homeassistant/components/neato/.translations/nl.json index a90009cb7be..4846f0180f1 100644 --- a/homeassistant/components/neato/.translations/nl.json +++ b/homeassistant/components/neato/.translations/nl.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Al geconfigureerd", "invalid_credentials": "Ongeldige gebruikersgegevens" }, "create_entry": { @@ -12,6 +13,12 @@ }, "step": { "user": { + "data": { + "password": "Wachtwoord", + "username": "Gebruikersnaam", + "vendor": "Leverancier" + }, + "description": "Zie [Neato-documentatie] ({docs_url}).", "title": "Neato-account info" } }, diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json index 81f4aa028f1..dbed3326b4a 100644 --- a/homeassistant/components/opentherm_gw/.translations/nl.json +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -1,15 +1,34 @@ { "config": { + "error": { + "already_configured": "Gateway al geconfigureerd", + "id_exists": "Gateway id bestaat al", + "serial_error": "Fout bij het verbinden met het apparaat", + "timeout": "Er is een time-out opgetreden voor de verbindingspoging" + }, "step": { "init": { "data": { "device": "Pad of URL", + "floor_temperature": "Vloertemperatuur", "id": "ID", + "name": "Naam", "precision": "Klimaattemperatuur precisie" }, "title": "OpenTherm Gateway" } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Vloertemperatuur", + "precision": "Precisie" + }, + "description": "Opties voor de OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/nl.json b/homeassistant/components/plex/.translations/nl.json index 413f4ad3207..c971ebb4762 100644 --- a/homeassistant/components/plex/.translations/nl.json +++ b/homeassistant/components/plex/.translations/nl.json @@ -1,6 +1,10 @@ { "config": { "abort": { + "all_configured": "Alle gekoppelde servers zijn al geconfigureerd", + "already_configured": "Deze Plex-server is al geconfigureerd", + "already_in_progress": "Plex wordt geconfigureerd", + "discovery_no_file": "Geen legacy configuratiebestand gevonden", "invalid_import": "Ge\u00efmporteerde configuratie is ongeldig", "token_request_timeout": "Time-out verkrijgen van token", "unknown": "Mislukt om onbekende reden" @@ -16,19 +20,42 @@ "data": { "host": "Host", "port": "Poort", - "ssl": "Gebruik SSL" + "ssl": "Gebruik SSL", + "token": "Token (indien nodig)", + "verify_ssl": "Controleer SSL-certificaat" }, "title": "Plex server" }, + "select_server": { + "data": { + "server": "Server" + }, + "description": "Meerdere servers beschikbaar, selecteer er een:", + "title": "Selecteer Plex server" + }, "start_website_auth": { "description": "Ga verder met autoriseren bij plex.tv.", "title": "Verbind de Plex server" }, "user": { "data": { - "manual_setup": "Handmatig setup" + "manual_setup": "Handmatig setup", + "token": "Plex token" }, - "description": "Ga verder met autoriseren bij plex.tv of configureer een server." + "description": "Ga verder met autoriseren bij plex.tv of configureer een server.", + "title": "Verbind de Plex server" + } + }, + "title": "Plex" + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "show_all_controls": "Toon alle bedieningselementen", + "use_episode_art": "Gebruik aflevering kunst" + }, + "description": "Opties voor Plex-mediaspelers" } } } diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 9be270a017c..7426e7f95ed 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -42,7 +42,7 @@ "manual_setup": "Ro\u010dna nastavitev", "token": "Plex \u017eeton" }, - "description": "Vnesite \u017eeton Plex za samodejno nastavitev ali ro\u010dno konfigurirajte stre\u017enik.", + "description": "Nadaljujte z avtorizacijo na plex.tv ali ro\u010dno konfigurirajte stre\u017enik.", "title": "Pove\u017eite stre\u017enik Plex" } }, diff --git a/homeassistant/components/sensor/.translations/ca.json b/homeassistant/components/sensor/.translations/ca.json index 59db5a62f86..94d95e7ddf8 100644 --- a/homeassistant/components/sensor/.translations/ca.json +++ b/homeassistant/components/sensor/.translations/ca.json @@ -4,6 +4,7 @@ "is_battery_level": "Nivell de bateria de {entity_name}", "is_humidity": "Humitat de {entity_name}", "is_illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "is_power": "Pot\u00e8ncia de {entity_name}", "is_pressure": "Pressi\u00f3 de {entity_name}", "is_signal_strength": "For\u00e7a del senyal de {entity_name}", "is_temperature": "Temperatura de {entity_name}", @@ -14,6 +15,7 @@ "battery_level": "Nivell de bateria de {entity_name}", "humidity": "Humitat de {entity_name}", "illuminance": "Il\u00b7luminaci\u00f3 de {entity_name}", + "power": "Pot\u00e8ncia de {entity_name}", "pressure": "Pressi\u00f3 de {entity_name}", "signal_strength": "For\u00e7a del senyal de {entity_name}", "temperature": "Temperatura de {entity_name}", diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json index aca2306d90e..f9cd8475a4c 100644 --- a/homeassistant/components/sensor/.translations/nl.json +++ b/homeassistant/components/sensor/.translations/nl.json @@ -1,7 +1,15 @@ { "device_automation": { "condition_type": { - "is_power": "{entity_name}\nvermogen" + "is_battery_level": "{entity_name} batterijniveau", + "is_humidity": "{entity_name} vochtigheidsgraad", + "is_illuminance": "{entity_name} verlichtingssterkte", + "is_power": "{entity_name}\nvermogen", + "is_pressure": "{entity_name} druk", + "is_signal_strength": "{entity_name} signaalsterkte", + "is_temperature": "{entity_name} temperatuur", + "is_timestamp": "{entity_name} tijdstip", + "is_value": "{entity_name} waarde" }, "trigger_type": { "battery_level": "{entity_name} batterijniveau", @@ -11,7 +19,8 @@ "pressure": "{entity_name} druk", "signal_strength": "{entity_name} signaalsterkte", "temperature": "{entity_name} temperatuur", - "timestamp": "{entity_name} tijdstip" + "timestamp": "{entity_name} tijdstip", + "value": "{entity_name} waarde" } } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/nl.json b/homeassistant/components/soma/.translations/nl.json index 0bf2836c5a1..c1188b0ac63 100644 --- a/homeassistant/components/soma/.translations/nl.json +++ b/homeassistant/components/soma/.translations/nl.json @@ -1,7 +1,23 @@ { "config": { "abort": { - "already_setup": "U kunt slechts \u00e9\u00e9n Soma-account configureren." - } + "already_setup": "U kunt slechts \u00e9\u00e9n Soma-account configureren.", + "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", + "missing_configuration": "De Soma-component is niet geconfigureerd. Gelieve de documentatie te volgen." + }, + "create_entry": { + "default": "Succesvol geverifieerd met Soma." + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Poort" + }, + "description": "Voer de verbindingsinstellingen van uw SOMA Connect in.", + "title": "SOMA Connect" + } + }, + "title": "Soma" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/nl.json b/homeassistant/components/transmission/.translations/nl.json index 6d9d130f85c..fdf3db99ed0 100644 --- a/homeassistant/components/transmission/.translations/nl.json +++ b/homeassistant/components/transmission/.translations/nl.json @@ -1,16 +1,40 @@ { "config": { + "abort": { + "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." + }, "error": { "cannot_connect": "Kan geen verbinding maken met host", "wrong_credentials": "verkeerde gebruikersnaam of wachtwoord" }, "step": { + "options": { + "data": { + "scan_interval": "Update frequentie" + }, + "title": "Configureer opties" + }, "user": { "data": { + "host": "Host", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort", "username": "Gebruikersnaam" }, "title": "Verzendclient instellen" } + }, + "title": "Transmission" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update frequentie" + }, + "description": "Configureer opties voor Transmission" + } } } } \ No newline at end of file diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json index f2277414a3e..3a62f5d7ebe 100644 --- a/homeassistant/components/zha/.translations/ko.json +++ b/homeassistant/components/zha/.translations/ko.json @@ -47,11 +47,11 @@ "turn_on": "\ucf1c\uae30" }, "trigger_type": { - "device_dropped": "\uc7a5\uce58\ub97c \ub5a8\uc5b4\ub728\ub9bc", + "device_dropped": "\uae30\uae30\ub97c \ub5a8\uad7c", "device_flipped": "\"{subtype}\" \uae30\uae30\ub97c \ub4a4\uc9d1\uc74c", "device_knocked": "\"{subtype}\" \uae30\uae30\ub97c \ub450\ub4dc\ub9bc", "device_rotated": "\"{subtype}\" \uae30\uae30\ub97c \ud68c\uc804", - "device_shaken": "\uae30\uae30 \ud754\ub4e6", + "device_shaken": "\uae30\uae30\ub97c \ud754\ub4e6", "device_slid": "\"{subtype}\" \uae30\uae30\ub97c \uc2ac\ub77c\uc774\ub4dc", "device_tilted": "\uae30\uae30\ub97c \uae30\uc6b8\uc784", "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc744 \ub450 \ubc88 \ub204\ub984", diff --git a/homeassistant/components/zha/.translations/nl.json b/homeassistant/components/zha/.translations/nl.json index bfb47c9d7fc..fc7ae970503 100644 --- a/homeassistant/components/zha/.translations/nl.json +++ b/homeassistant/components/zha/.translations/nl.json @@ -23,9 +23,22 @@ "warn": "Waarschuwen" }, "trigger_subtype": { + "both_buttons": "Beide knoppen", + "button_1": "Eerste knop", + "button_2": "Tweede knop", + "button_3": "Derde knop", + "button_4": "Vierde knop", + "button_5": "Vijfde knop", + "button_6": "Zesde knop", "close": "Sluiten", "dim_down": "Dim omlaag", "dim_up": "Dim omhoog", + "face_1": "met gezicht 1 geactiveerd", + "face_2": "met gezicht 2 geactiveerd", + "face_3": "met gezicht 3 geactiveerd", + "face_4": "met gezicht 4 geactiveerd", + "face_5": "met gezicht 5 geactiveerd", + "face_6": "met gezicht 6 geactiveerd", "face_any": "Met elk/opgegeven gezicht (en) geactiveerd", "left": "Links", "open": "Open", From 1111e150f447096db5f6958486d584bba1769a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:31:41 +0000 Subject: [PATCH 551/639] Move imports in shodan component (#28098) --- homeassistant/components/shodan/sensor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shodan/sensor.py b/homeassistant/components/shodan/sensor.py index 7b1360b0b01..d2a6a28fbe4 100644 --- a/homeassistant/components/shodan/sensor.py +++ b/homeassistant/components/shodan/sensor.py @@ -1,12 +1,13 @@ """Sensor for displaying the number of result on Shodan.io.""" -import logging from datetime import timedelta +import logging +import shodan import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -32,8 +33,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Shodan sensor.""" - import shodan - api_key = config.get(CONF_API_KEY) name = config.get(CONF_NAME) query = config.get(CONF_QUERY) From d9cb9601aa00f7aa7b3f10e87a5e8b610f2ce4d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:31:58 +0000 Subject: [PATCH 552/639] Move imports in skybeacon component (#28099) --- homeassistant/components/skybeacon/sensor.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/skybeacon/sensor.py b/homeassistant/components/skybeacon/sensor.py index 1c098409610..cbf394edf47 100644 --- a/homeassistant/components/skybeacon/sensor.py +++ b/homeassistant/components/skybeacon/sensor.py @@ -3,6 +3,9 @@ import logging import threading from uuid import UUID +from pygatt import BLEAddressType +from pygatt.backends import Characteristic, GATTToolBackend +from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -132,13 +135,8 @@ class Monitor(threading.Thread): def run(self): """Thread that keeps connection alive.""" - # pylint: disable=import-error - import pygatt - from pygatt.backends import Characteristic - from pygatt.exceptions import BLEError, NotConnectedError, NotificationTimeout - cached_char = Characteristic(BLE_TEMP_UUID, BLE_TEMP_HANDLE) - adapter = pygatt.backends.GATTToolBackend() + adapter = GATTToolBackend() while True: try: _LOGGER.debug("Connecting to %s", self.name) @@ -147,7 +145,7 @@ class Monitor(threading.Thread): # Seems only one connection can be initiated at a time with CONNECT_LOCK: device = adapter.connect( - self.mac, CONNECT_TIMEOUT, pygatt.BLEAddressType.random + self.mac, CONNECT_TIMEOUT, BLEAddressType.random ) if SKIP_HANDLE_LOOKUP: # HACK: inject handle mapping collected offline From 953f31dd55006a01afb72963c2143ac68e8cc73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:32:37 +0000 Subject: [PATCH 553/639] Move imports in shiftr component (#28097) --- homeassistant/components/shiftr/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shiftr/__init__.py b/homeassistant/components/shiftr/__init__.py index 8e698d283cf..1c3cddac256 100644 --- a/homeassistant/components/shiftr/__init__.py +++ b/homeassistant/components/shiftr/__init__.py @@ -1,16 +1,17 @@ """Support for Shiftr.io.""" import logging +import paho.mqtt.client as mqtt import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_PASSWORD, CONF_USERNAME, - EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_STOP, + EVENT_STATE_CHANGED, ) from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -33,8 +34,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Initialize the Shiftr.io MQTT consumer.""" - import paho.mqtt.client as mqtt - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) From f440259edc02af7ca82a09a0e9e991485b4d5ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:32:53 +0000 Subject: [PATCH 554/639] Move imports in seven_segments component (#28096) --- .../seven_segments/image_processing.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/seven_segments/image_processing.py b/homeassistant/components/seven_segments/image_processing.py index 4b96cc50ecc..315b5c39fec 100644 --- a/homeassistant/components/seven_segments/image_processing.py +++ b/homeassistant/components/seven_segments/image_processing.py @@ -1,19 +1,21 @@ """Optical character recognition processing of seven segments displays.""" -import logging import io +import logging import os +import subprocess +from PIL import Image import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.core import split_entity_id from homeassistant.components.image_processing import ( - PLATFORM_SCHEMA, - ImageProcessingEntity, - CONF_SOURCE, CONF_ENTITY_ID, CONF_NAME, + CONF_SOURCE, + PLATFORM_SCHEMA, + ImageProcessingEntity, ) +from homeassistant.core import split_entity_id +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -120,9 +122,6 @@ class ImageProcessingSsocr(ImageProcessingEntity): def process_image(self, image): """Process the image.""" - from PIL import Image - import subprocess - stream = io.BytesIO(image) img = Image.open(stream) img.save(self.filepath, "png") From 828bf1b400092e2f578072fcd2fff573f6f4c62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:33:02 +0000 Subject: [PATCH 555/639] Move imports in sesame component (#28095) --- homeassistant/components/sesame/lock.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sesame/lock.py b/homeassistant/components/sesame/lock.py index f8698ac6bd8..fa12ff7a1b2 100644 --- a/homeassistant/components/sesame/lock.py +++ b/homeassistant/components/sesame/lock.py @@ -1,15 +1,17 @@ """Support for Sesame, by CANDY HOUSE.""" from typing import Callable + +import pysesame2 import voluptuous as vol -import homeassistant.helpers.config_validation as cv -from homeassistant.components.lock import LockDevice, PLATFORM_SCHEMA +from homeassistant.components.lock import PLATFORM_SCHEMA, LockDevice from homeassistant.const import ( ATTR_BATTERY_LEVEL, CONF_API_KEY, STATE_LOCKED, STATE_UNLOCKED, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType ATTR_DEVICE_ID = "device_id" @@ -22,8 +24,6 @@ def setup_platform( hass, config: ConfigType, add_entities: Callable[[list], None], discovery_info=None ): """Set up the Sesame platform.""" - import pysesame2 - api_key = config.get(CONF_API_KEY) add_entities( From 4a3d6208ae54cec17c169dad8d6d39f65165f9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:33:23 +0000 Subject: [PATCH 556/639] Move imports in rpi_pfio component (#28094) --- homeassistant/components/rpi_pfio/__init__.py | 10 ++-------- homeassistant/components/rpi_pfio/binary_sensor.py | 2 +- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/rpi_pfio/__init__.py b/homeassistant/components/rpi_pfio/__init__.py index d51785daf9c..72be34e0f45 100644 --- a/homeassistant/components/rpi_pfio/__init__.py +++ b/homeassistant/components/rpi_pfio/__init__.py @@ -1,6 +1,8 @@ """Support for controlling the PiFace Digital I/O module on a RPi.""" import logging +import pifacedigitalio as PFIO + from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) @@ -12,8 +14,6 @@ DATA_PFIO_LISTENER = "pfio_listener" def setup(hass, config): """Set up the Raspberry PI PFIO component.""" - import pifacedigitalio as PFIO - pifacedigital = PFIO.PiFaceDigital() hass.data[DATA_PFIO_LISTENER] = PFIO.InputEventListener(chip=pifacedigital) @@ -33,22 +33,16 @@ def setup(hass, config): def write_output(port, value): """Write a value to a PFIO.""" - import pifacedigitalio as PFIO - PFIO.digital_write(port, value) def read_input(port): """Read a value from a PFIO.""" - import pifacedigitalio as PFIO - return PFIO.digital_read(port) def edge_detect(hass, port, event_callback, settle): """Add detection for RISING and FALLING events.""" - import pifacedigitalio as PFIO - hass.data[DATA_PFIO_LISTENER].register( port, PFIO.IODIR_BOTH, event_callback, settle_time=settle ) diff --git a/homeassistant/components/rpi_pfio/binary_sensor.py b/homeassistant/components/rpi_pfio/binary_sensor.py index 44da251732b..89d44a0e8db 100644 --- a/homeassistant/components/rpi_pfio/binary_sensor.py +++ b/homeassistant/components/rpi_pfio/binary_sensor.py @@ -3,8 +3,8 @@ import logging import voluptuous as vol -from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.components import rpi_pfio +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv From d9b890a4024d04bbab65e3feef3c1f435f0213f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:34:35 +0000 Subject: [PATCH 557/639] Move imports in repetier component (#28093) --- homeassistant/components/repetier/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/repetier/__init__.py b/homeassistant/components/repetier/__init__.py index 6f72a6b7ddc..12975baca91 100644 --- a/homeassistant/components/repetier/__init__.py +++ b/homeassistant/components/repetier/__init__.py @@ -1,7 +1,8 @@ """Support for Repetier-Server sensors.""" -import logging from datetime import timedelta +import logging +import pyrepetier import voluptuous as vol from homeassistant.const import ( @@ -160,8 +161,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the Repetier Server component.""" - import pyrepetier - hass.data[REPETIER_API] = {} for repetier in config[DOMAIN]: From 3e8f2bf2fce86054bc4bae53e3e723383753d97b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:34:51 +0000 Subject: [PATCH 558/639] Move imports in remember_the_milk component (#28092) --- .../components/remember_the_milk/__init__.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index c92a246da14..fdfbdfd5cdc 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -3,6 +3,7 @@ import json import logging import os +from rtmapi import Rtm, RtmRequestFailedException import voluptuous as vol from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME, CONF_TOKEN, STATE_OK @@ -102,8 +103,6 @@ def _create_instance( def _register_new_account( hass, account_name, api_key, shared_secret, stored_rtm_config, component ): - from rtmapi import Rtm - request_id = None configurator = hass.components.configurator api = Rtm(api_key, shared_secret, "write", None) @@ -240,14 +239,12 @@ class RememberTheMilk(Entity): def __init__(self, name, api_key, shared_secret, token, rtm_config): """Create new instance of Remember The Milk component.""" - import rtmapi - self._name = name self._api_key = api_key self._shared_secret = shared_secret self._token = token self._rtm_config = rtm_config - self._rtm_api = rtmapi.Rtm(api_key, shared_secret, "delete", token) + self._rtm_api = Rtm(api_key, shared_secret, "delete", token) self._token_valid = None self._check_token() _LOGGER.debug("Instance created for account %s", self._name) @@ -277,8 +274,6 @@ class RememberTheMilk(Entity): e.g. "my task #some_tag ^today" will add tag "some_tag" and set the due date to today. """ - import rtmapi - try: task_name = call.data.get(CONF_NAME) hass_id = call.data.get(CONF_ID) @@ -316,7 +311,7 @@ class RememberTheMilk(Entity): self.name, task_name, ) - except rtmapi.RtmRequestFailedException as rtm_exception: + except RtmRequestFailedException as rtm_exception: _LOGGER.error( "Error creating new Remember The Milk task for " "account %s: %s", self._name, @@ -327,8 +322,6 @@ class RememberTheMilk(Entity): def complete_task(self, call): """Complete a task that was previously created by this component.""" - import rtmapi - hass_id = call.data.get(CONF_ID) rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id) if rtm_id is None: @@ -352,7 +345,7 @@ class RememberTheMilk(Entity): _LOGGER.debug( "Completed task with id %s in account %s", hass_id, self._name ) - except rtmapi.RtmRequestFailedException as rtm_exception: + except RtmRequestFailedException as rtm_exception: _LOGGER.error( "Error creating new Remember The Milk task for " "account %s: %s", self._name, From 40fbfe7a93c712d0ab0be97dc896982ef61e989a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:35:05 +0000 Subject: [PATCH 559/639] Move imports in rejseplanen component (#28091) --- homeassistant/components/rejseplanen/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rejseplanen/sensor.py b/homeassistant/components/rejseplanen/sensor.py index 61cb319fd11..b7d36010714 100644 --- a/homeassistant/components/rejseplanen/sensor.py +++ b/homeassistant/components/rejseplanen/sensor.py @@ -7,17 +7,18 @@ https://help.rejseplanen.dk/hc/en-us/articles/214174465-Rejseplanen-s-API For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.rejseplanen/ """ +from datetime import datetime, timedelta import logging -from datetime import timedelta, datetime from operator import itemgetter +import rjpl import voluptuous as vol -import homeassistant.helpers.config_validation as cv -import homeassistant.util.dt as dt_util from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_NAME, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -166,8 +167,6 @@ class PublicTransportData: def update(self): """Get the latest data from rejseplanen.""" - import rjpl - self.info = [] def intersection(lst1, lst2): From 0193207b5c662b0f29c914e6ced2b88f3b5c84d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:35:25 +0000 Subject: [PATCH 560/639] Move imports in recollect_waste component (#28089) --- homeassistant/components/recollect_waste/sensor.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/recollect_waste/sensor.py b/homeassistant/components/recollect_waste/sensor.py index 118b6fb3709..17496f3d361 100644 --- a/homeassistant/components/recollect_waste/sensor.py +++ b/homeassistant/components/recollect_waste/sensor.py @@ -1,11 +1,12 @@ """Support for Recollect Waste curbside collection pickup.""" import logging +import recollect_waste import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -29,9 +30,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Recollect Waste platform.""" - import recollect_waste - - # pylint: disable=no-member client = recollect_waste.RecollectWasteClient( config[CONF_PLACE_ID], config[CONF_SERVICE_ID] ) @@ -40,7 +38,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # with given place_id and service_id. try: client.get_next_pickup() - # pylint: disable=no-member except recollect_waste.RecollectWasteException as ex: _LOGGER.error("Recollect Waste platform error. %s", ex) return @@ -85,8 +82,6 @@ class RecollectWasteSensor(Entity): def update(self): """Update device state.""" - import recollect_waste - try: pickup_event = self.client.get_next_pickup() self._state = pickup_event.event_date @@ -96,6 +91,5 @@ class RecollectWasteSensor(Entity): ATTR_AREA_NAME: pickup_event.area_name, } ) - # pylint: disable=no-member except recollect_waste.RecollectWasteException as ex: _LOGGER.error("Recollect Waste platform error. %s", ex) From 2d36e9c08e0b994239e6e888f8954769e9735c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:35:43 +0000 Subject: [PATCH 561/639] Move imports in prometheus component (#28086) --- .../components/prometheus/__init__.py | 51 +++++++++---------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 82db5f6725f..8eeb9325bc0 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -3,24 +3,25 @@ import logging import string from aiohttp import web +import prometheus_client import voluptuous as vol from homeassistant import core as hacore from homeassistant.components.climate.const import ATTR_CURRENT_TEMPERATURE from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_TEMPERATURE, ATTR_UNIT_OF_MEASUREMENT, - ATTR_DEVICE_CLASS, CONTENT_TYPE_TEXT_PLAIN, EVENT_STATE_CHANGED, - TEMP_FAHRENHEIT, TEMP_CELSIUS, + TEMP_FAHRENHEIT, ) from homeassistant.helpers import entityfilter, state as state_helper import homeassistant.helpers.config_validation as cv -from homeassistant.util.temperature import fahrenheit_to_celsius from homeassistant.helpers.entity_values import EntityValues +from homeassistant.util.temperature import fahrenheit_to_celsius _LOGGER = logging.getLogger(__name__) @@ -64,8 +65,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Activate Prometheus component.""" - import prometheus_client - hass.http.register_view(PrometheusView(prometheus_client)) conf = config[DOMAIN] @@ -99,7 +98,7 @@ class PrometheusMetrics: def __init__( self, - prometheus_client, + prometheus_cli, entity_filter, namespace, climate_units, @@ -108,7 +107,7 @@ class PrometheusMetrics: default_metric, ): """Initialize Prometheus Metrics.""" - self.prometheus_client = prometheus_client + self.prometheus_cli = prometheus_cli self._component_config = component_config self._override_metric = override_metric self._default_metric = default_metric @@ -147,9 +146,7 @@ class PrometheusMetrics: getattr(self, handler)(state) metric = self._metric( - "state_change", - self.prometheus_client.Counter, - "The number of state changes", + "state_change", self.prometheus_cli.Counter, "The number of state changes" ) metric.labels(**self._labels(state)).inc() @@ -199,7 +196,7 @@ class PrometheusMetrics: if "battery_level" in state.attributes: metric = self._metric( "battery_level_percent", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "Battery level as a percentage of its capacity", ) try: @@ -211,7 +208,7 @@ class PrometheusMetrics: def _handle_binary_sensor(self, state): metric = self._metric( "binary_sensor_state", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "State of the binary sensor (0/1)", ) value = self.state_as_number(state) @@ -220,7 +217,7 @@ class PrometheusMetrics: def _handle_input_boolean(self, state): metric = self._metric( "input_boolean_state", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "State of the input boolean (0/1)", ) value = self.state_as_number(state) @@ -229,7 +226,7 @@ class PrometheusMetrics: def _handle_device_tracker(self, state): metric = self._metric( "device_tracker_state", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "State of the device tracker (0/1)", ) value = self.state_as_number(state) @@ -237,14 +234,14 @@ class PrometheusMetrics: def _handle_person(self, state): metric = self._metric( - "person_state", self.prometheus_client.Gauge, "State of the person (0/1)" + "person_state", self.prometheus_cli.Gauge, "State of the person (0/1)" ) value = self.state_as_number(state) metric.labels(**self._labels(state)).set(value) def _handle_light(self, state): metric = self._metric( - "light_state", self.prometheus_client.Gauge, "Load level of a light (0..1)" + "light_state", self.prometheus_cli.Gauge, "Load level of a light (0..1)" ) try: @@ -259,7 +256,7 @@ class PrometheusMetrics: def _handle_lock(self, state): metric = self._metric( - "lock_state", self.prometheus_client.Gauge, "State of the lock (0/1)" + "lock_state", self.prometheus_cli.Gauge, "State of the lock (0/1)" ) value = self.state_as_number(state) metric.labels(**self._labels(state)).set(value) @@ -271,7 +268,7 @@ class PrometheusMetrics: temp = fahrenheit_to_celsius(temp) metric = self._metric( "temperature_c", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "Temperature in degrees Celsius", ) metric.labels(**self._labels(state)).set(temp) @@ -282,15 +279,13 @@ class PrometheusMetrics: current_temp = fahrenheit_to_celsius(current_temp) metric = self._metric( "current_temperature_c", - self.prometheus_client.Gauge, + self.prometheus_cli.Gauge, "Current Temperature in degrees Celsius", ) metric.labels(**self._labels(state)).set(current_temp) metric = self._metric( - "climate_state", - self.prometheus_client.Gauge, - "State of the thermostat (0/1)", + "climate_state", self.prometheus_cli.Gauge, "State of the thermostat (0/1)" ) try: value = self.state_as_number(state) @@ -308,7 +303,7 @@ class PrometheusMetrics: if metric is not None: _metric = self._metric( - metric, self.prometheus_client.Gauge, f"Sensor data measured in {unit}" + metric, self.prometheus_cli.Gauge, f"Sensor data measured in {unit}" ) try: @@ -368,7 +363,7 @@ class PrometheusMetrics: def _handle_switch(self, state): metric = self._metric( - "switch_state", self.prometheus_client.Gauge, "State of the switch (0/1)" + "switch_state", self.prometheus_cli.Gauge, "State of the switch (0/1)" ) try: @@ -383,7 +378,7 @@ class PrometheusMetrics: def _handle_automation(self, state): metric = self._metric( "automation_triggered_count", - self.prometheus_client.Counter, + self.prometheus_cli.Counter, "Count of times an automation has been triggered", ) @@ -396,15 +391,15 @@ class PrometheusView(HomeAssistantView): url = API_ENDPOINT name = "api:prometheus" - def __init__(self, prometheus_client): + def __init__(self, prometheus_cli): """Initialize Prometheus view.""" - self.prometheus_client = prometheus_client + self.prometheus_cli = prometheus_cli async def get(self, request): """Handle request for Prometheus metrics.""" _LOGGER.debug("Received Prometheus metrics request") return web.Response( - body=self.prometheus_client.generate_latest(), + body=self.prometheus_cli.generate_latest(), content_type=CONTENT_TYPE_TEXT_PLAIN, ) From 1313ec4ec8b9de74685c83ecef3a0c97b075ae79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:36:35 +0000 Subject: [PATCH 562/639] Move imports in proliphix component (#28085) --- homeassistant/components/proliphix/climate.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index 666f8c28241..44e31e24fb0 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -1,7 +1,8 @@ """Support for Proliphix NT10e Thermostats.""" +import proliphix import voluptuous as vol -from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA +from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_HEAT, @@ -9,12 +10,12 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ( + ATTR_TEMPERATURE, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, PRECISION_TENTHS, TEMP_FAHRENHEIT, - ATTR_TEMPERATURE, ) import homeassistant.helpers.config_validation as cv @@ -35,8 +36,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): password = config.get(CONF_PASSWORD) host = config.get(CONF_HOST) - import proliphix - pdp = proliphix.PDP(host, username, password) add_entities([ProliphixThermostat(pdp)], True) From 87cc6610872ac560ed221c622bb7659931d63d4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:36:46 +0000 Subject: [PATCH 563/639] Move imports in pocketcasts component (#28084) --- homeassistant/components/pocketcasts/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/pocketcasts/sensor.py b/homeassistant/components/pocketcasts/sensor.py index 815e2688009..05a8f96bda7 100644 --- a/homeassistant/components/pocketcasts/sensor.py +++ b/homeassistant/components/pocketcasts/sensor.py @@ -1,13 +1,13 @@ """Support for Pocket Casts.""" +from datetime import timedelta import logging -from datetime import timedelta - +import pocketcasts import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -25,8 +25,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the pocketcasts platform for sensors.""" - import pocketcasts - username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) From a8c6b04906d77509e2917978728c6cdf2fcbba03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:37:09 +0000 Subject: [PATCH 564/639] Move imports in opencv component (#28042) * Move imports in opencv component * Fix pylint --- .../components/opencv/image_processing.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/opencv/image_processing.py b/homeassistant/components/opencv/image_processing.py index 3c72af4f368..4a1b830a324 100644 --- a/homeassistant/components/opencv/image_processing.py +++ b/homeassistant/components/opencv/image_processing.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging +import numpy import requests import voluptuous as vol @@ -15,6 +16,15 @@ from homeassistant.components.image_processing import ( from homeassistant.core import split_entity_id import homeassistant.helpers.config_validation as cv +try: + # Verify that the OpenCV python package is pre-installed + import cv2 + + CV2_IMPORTED = True +except ImportError: + CV2_IMPORTED = False + + _LOGGER = logging.getLogger(__name__) ATTR_MATCHES = "matches" @@ -86,11 +96,7 @@ def _get_default_classifier(dest_path): def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the OpenCV image processing platform.""" - try: - # Verify that the OpenCV python package is pre-installed - # pylint: disable=unused-import,unused-variable - import cv2 # noqa - except ImportError: + if not CV2_IMPORTED: _LOGGER.error( "No OpenCV library found! Install or compile for your system " "following instructions here: http://opencv.org/releases.html" @@ -154,9 +160,6 @@ class OpenCVImageProcessor(ImageProcessingEntity): def process_image(self, image): """Process the image.""" - import cv2 # pylint: disable=import-error - import numpy - cv_image = cv2.imdecode(numpy.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) for name, classifier in self._classifiers.items(): From 1a48c347a415b06a53d0b799e57cbdaf680b1134 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:37:31 +0000 Subject: [PATCH 565/639] Move imports in mitemp_bt component (#28026) * Move imports in mitemp_bt component * Fix pylint --- homeassistant/components/mitemp_bt/sensor.py | 31 +++++++++----------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index adeba48dbc8..b536149680d 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -1,21 +1,30 @@ """Support for Xiaomi Mi Temp BLE environmental sensor.""" import logging +import btlewrap +from btlewrap.base import BluetoothBackendException +from mitemp_bt import mitemp_bt_poller import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_FORCE_UPDATE, + CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, - CONF_MAC, + DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, - DEVICE_CLASS_BATTERY, ) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +try: + import bluepy.btle # noqa: F401 pylint: disable=unused-import + + BACKEND = btlewrap.BluepyBackend +except ImportError: + BACKEND = btlewrap.GatttoolBackend _LOGGER = logging.getLogger(__name__) @@ -60,17 +69,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MiTempBt sensor.""" - from mitemp_bt import mitemp_bt_poller - - try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - from btlewrap import BluepyBackend - - backend = BluepyBackend - except ImportError: - from btlewrap import GatttoolBackend - - backend = GatttoolBackend + backend = BACKEND _LOGGER.debug("MiTempBt is using %s backend.", backend.__name__) cache = config.get(CONF_CACHE) @@ -152,8 +151,6 @@ class MiTempBtSensor(Entity): This uses a rolling median over 3 values to filter out outliers. """ - from btlewrap.base import BluetoothBackendException - try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) From 5d317dc096f147fc8930bd2ce77823678c763b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:37:48 +0000 Subject: [PATCH 566/639] Move imports in miflora component (#28025) * Move imports in miflora component * Fix pylint --- homeassistant/components/miflora/sensor.py | 31 +++++++++++----------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 28020a80175..a08c4ce5eac 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -1,20 +1,31 @@ """Support for Xiaomi Mi Flora BLE plant sensor.""" from datetime import timedelta import logging + +import btlewrap +from btlewrap import BluetoothBackendException +from miflora import miflora_poller import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.helpers.entity import Entity -import homeassistant.helpers.config_validation as cv from homeassistant.const import ( CONF_FORCE_UPDATE, + CONF_MAC, CONF_MONITORED_CONDITIONS, CONF_NAME, - CONF_MAC, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, ) from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity + +try: + import bluepy.btle # noqa: F401 pylint: disable=unused-import + + BACKEND = btlewrap.BluepyBackend +except ImportError: + BACKEND = btlewrap.GatttoolBackend _LOGGER = logging.getLogger(__name__) @@ -53,17 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the MiFlora sensor.""" - from miflora import miflora_poller - - try: - import bluepy.btle # noqa: F401 pylint: disable=unused-import - from btlewrap import BluepyBackend - - backend = BluepyBackend - except ImportError: - from btlewrap import GatttoolBackend - - backend = GatttoolBackend + backend = BACKEND _LOGGER.debug("Miflora is using %s backend.", backend.__name__) cache = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL).total_seconds() @@ -152,8 +153,6 @@ class MiFloraSensor(Entity): This uses a rolling median over 3 values to filter out outliers. """ - from btlewrap import BluetoothBackendException - try: _LOGGER.debug("Polling data for %s", self.name) data = self.poller.parameter_value(self.parameter) From 6c18bbcf044800d5cbd5813ad8fb2e311eef0deb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:38:01 +0000 Subject: [PATCH 567/639] Move imports in lastfm component (#28010) * Move imports in lastfm component * Fix pylint --- homeassistant/components/lastfm/sensor.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 736792aefd8..68d727626cf 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -2,10 +2,12 @@ import logging import re +import pylast as lastfm +from pylast import WSError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import CONF_API_KEY, ATTR_ATTRIBUTION +from homeassistant.const import ATTR_ATTRIBUTION, CONF_API_KEY import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity @@ -30,9 +32,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Last.fm sensor platform.""" - import pylast as lastfm - from pylast import WSError - api_key = config[CONF_API_KEY] users = config.get(CONF_USERS) @@ -53,11 +52,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class LastfmSensor(Entity): """A class for the Last.fm account.""" - def __init__(self, user, lastfm): + def __init__(self, user, lastfm_api): """Initialize the sensor.""" - self._user = lastfm.get_user(user) + self._user = lastfm_api.get_user(user) self._name = user - self._lastfm = lastfm + self._lastfm = lastfm_api self._state = "Not Scrobbling" self._playcount = None self._lastplayed = None From ff17bb4a56d8b0de56c155c89281ed2440d1ab59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:38:21 +0000 Subject: [PATCH 568/639] Move imports in knx component (#28008) * Move imports in knx component * Fix pylint --- homeassistant/components/knx/__init__.py | 28 ++++--------------- homeassistant/components/knx/binary_sensor.py | 4 +-- homeassistant/components/knx/climate.py | 22 ++++++--------- homeassistant/components/knx/cover.py | 5 ++-- homeassistant/components/knx/light.py | 5 ++-- homeassistant/components/knx/notify.py | 5 ++-- homeassistant/components/knx/scene.py | 5 ++-- homeassistant/components/knx/sensor.py | 5 ++-- homeassistant/components/knx/switch.py | 5 ++-- 9 files changed, 29 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 5d0c4be3d07..00d5d18f013 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -2,6 +2,11 @@ import logging import voluptuous as vol +from xknx import XKNX +from xknx.devices import ActionCallback, DateTime, DateTimeBroadcastType, ExposeSensor +from xknx.exceptions import XKNXException +from xknx.io import DEFAULT_MCAST_PORT, ConnectionConfig, ConnectionType +from xknx.knx import AddressFilter, DPTArray, DPTBinary, GroupAddress, Telegram from homeassistant.const import ( CONF_ENTITY_ID, @@ -90,13 +95,10 @@ SERVICE_KNX_SEND_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the KNX component.""" - from xknx.exceptions import XKNXException - try: hass.data[DATA_KNX] = KNXModule(hass, config) hass.data[DATA_KNX].async_create_exposures() await hass.data[DATA_KNX].start() - except XKNXException as ex: _LOGGER.warning("Can't connect to KNX interface: %s", ex) hass.components.persistent_notification.async_create( @@ -157,8 +159,6 @@ class KNXModule: def init_xknx(self): """Initialize of KNX object.""" - from xknx import XKNX - self.xknx = XKNX( config=self.config_file(), loop=self.hass.loop, @@ -198,8 +198,6 @@ class KNXModule: def connection_config_routing(self): """Return the connection_config if routing is configured.""" - from xknx.io import ConnectionConfig, ConnectionType - local_ip = self.config[DOMAIN][CONF_KNX_ROUTING].get(CONF_KNX_LOCAL_IP) return ConnectionConfig( connection_type=ConnectionType.ROUTING, local_ip=local_ip @@ -207,8 +205,6 @@ class KNXModule: def connection_config_tunneling(self): """Return the connection_config if tunneling is configured.""" - from xknx.io import ConnectionConfig, ConnectionType, DEFAULT_MCAST_PORT - gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_HOST) gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_PORT) local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_KNX_LOCAL_IP) @@ -224,8 +220,6 @@ class KNXModule: def connection_config_auto(self): """Return the connection_config if auto is configured.""" # pylint: disable=no-self-use - from xknx.io import ConnectionConfig - return ConnectionConfig() def register_callbacks(self): @@ -234,8 +228,6 @@ class KNXModule: CONF_KNX_FIRE_EVENT in self.config[DOMAIN] and self.config[DOMAIN][CONF_KNX_FIRE_EVENT] ): - from xknx.knx import AddressFilter - address_filters = list( map(AddressFilter, self.config[DOMAIN][CONF_KNX_FIRE_EVENT_FILTER]) ) @@ -274,8 +266,6 @@ class KNXModule: async def service_send_to_knx_bus(self, call): """Service for sending an arbitrary KNX message to the KNX bus.""" - from xknx.knx import Telegram, GroupAddress, DPTBinary, DPTArray - attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD) attr_address = call.data.get(SERVICE_KNX_ATTR_ADDRESS) @@ -304,9 +294,7 @@ class KNXAutomation: script_name = "{} turn ON script".format(device.get_name()) self.script = Script(hass, action, script_name) - import xknx - - self.action = xknx.devices.ActionCallback( + self.action = ActionCallback( hass.data[DATA_KNX].xknx, self.script.async_run, hook=hook, counter=counter ) device.actions.append(self.action) @@ -325,8 +313,6 @@ class KNXExposeTime: @callback def async_register(self): """Register listener.""" - from xknx.devices import DateTime, DateTimeBroadcastType - broadcast_type_string = self.type.upper() broadcast_type = DateTimeBroadcastType[broadcast_type_string] self.device = DateTime( @@ -350,8 +336,6 @@ class KNXExposeSensor: @callback def async_register(self): """Register listener.""" - from xknx.devices import ExposeSensor - self.device = ExposeSensor( self.xknx, name=self.entity_id, diff --git a/homeassistant/components/knx/binary_sensor.py b/homeassistant/components/knx/binary_sensor.py index fbe9c4e421e..94a171d9c2a 100644 --- a/homeassistant/components/knx/binary_sensor.py +++ b/homeassistant/components/knx/binary_sensor.py @@ -1,5 +1,6 @@ """Support for KNX/IP binary sensors.""" import voluptuous as vol +from xknx.devices import BinarySensor from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME @@ -70,9 +71,8 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): def async_add_entities_config(hass, config, async_add_entities): """Set up binary senor for KNX platform configured within platform.""" name = config[CONF_NAME] - import xknx - binary_sensor = xknx.devices.BinarySensor( + binary_sensor = BinarySensor( hass.data[DATA_KNX].xknx, name=name, group_address_state=config[CONF_STATE_ADDRESS], diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 014cd8d9ba1..819fb1794c3 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -1,20 +1,22 @@ """Support for KNX/IP climate devices.""" -from typing import Optional, List +from typing import List, Optional import voluptuous as vol +from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode +from xknx.knx import HVACOperationMode from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, + HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_OFF, - HVAC_MODE_COOL, - HVAC_MODE_AUTO, - PRESET_ECO, - PRESET_SLEEP, PRESET_AWAY, PRESET_COMFORT, + PRESET_ECO, + PRESET_SLEEP, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -135,9 +137,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up climate for KNX platform configured within platform.""" - import xknx - - climate_mode = xknx.devices.ClimateMode( + climate_mode = XknxClimateMode( hass.data[DATA_KNX].xknx, name=config[CONF_NAME] + " Mode", group_address_operation_mode=config.get(CONF_OPERATION_MODE_ADDRESS), @@ -165,7 +165,7 @@ def async_add_entities_config(hass, config, async_add_entities): ) hass.data[DATA_KNX].xknx.devices.add(climate_mode) - climate = xknx.devices.Climate( + climate = XknxClimate( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_temperature=config[CONF_TEMPERATURE_ADDRESS], @@ -302,8 +302,6 @@ class KNXClimate(ClimateDevice): elif self.device.supports_on_off and hvac_mode == HVAC_MODE_HEAT: await self.device.turn_on() elif self.device.mode.supports_operation_mode: - from xknx.knx import HVACOperationMode - knx_operation_mode = HVACOperationMode(OPERATION_MODES_INV.get(hvac_mode)) await self.device.mode.set_operation_mode(knx_operation_mode) await self.async_update_ha_state() @@ -337,8 +335,6 @@ class KNXClimate(ClimateDevice): This method must be run in the event loop and returns a coroutine. """ if self.device.mode.supports_operation_mode: - from xknx.knx import HVACOperationMode - knx_operation_mode = HVACOperationMode(PRESET_MODES_INV.get(preset_mode)) await self.device.mode.set_operation_mode(knx_operation_mode) await self.async_update_ha_state() diff --git a/homeassistant/components/knx/cover.py b/homeassistant/components/knx/cover.py index 9af7c11678a..976d1286c9f 100644 --- a/homeassistant/components/knx/cover.py +++ b/homeassistant/components/knx/cover.py @@ -1,5 +1,6 @@ """Support for KNX/IP covers.""" import voluptuous as vol +from xknx.devices import Cover as XknxCover from homeassistant.components.cover import ( ATTR_POSITION, @@ -74,9 +75,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up cover for KNX platform configured within platform.""" - import xknx - - cover = xknx.devices.Cover( + cover = XknxCover( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_long=config.get(CONF_MOVE_LONG_ADDRESS), diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 71a82c6df2a..81bf4ad3c83 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -2,6 +2,7 @@ from enum import Enum import voluptuous as vol +from xknx.devices import Light as XknxLight from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -98,8 +99,6 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up light for KNX platform configured within platform.""" - import xknx - group_address_tunable_white = None group_address_tunable_white_state = None group_address_color_temp = None @@ -111,7 +110,7 @@ def async_add_entities_config(hass, config, async_add_entities): group_address_tunable_white = config.get(CONF_COLOR_TEMP_ADDRESS) group_address_tunable_white_state = config.get(CONF_COLOR_TEMP_STATE_ADDRESS) - light = xknx.devices.Light( + light = XknxLight( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_switch=config[CONF_ADDRESS], diff --git a/homeassistant/components/knx/notify.py b/homeassistant/components/knx/notify.py index b83edb89eb1..64d513b8624 100644 --- a/homeassistant/components/knx/notify.py +++ b/homeassistant/components/knx/notify.py @@ -1,5 +1,6 @@ """Support for KNX/IP notification services.""" import voluptuous as vol +from xknx.devices import Notification as XknxNotification from homeassistant.components.notify import PLATFORM_SCHEMA, BaseNotificationService from homeassistant.const import CONF_ADDRESS, CONF_NAME @@ -42,9 +43,7 @@ def async_get_service_discovery(hass, discovery_info): @callback def async_get_service_config(hass, config): """Set up notification for KNX platform configured within platform.""" - import xknx - - notification = xknx.devices.Notification( + notification = XknxNotification( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address=config[CONF_ADDRESS], diff --git a/homeassistant/components/knx/scene.py b/homeassistant/components/knx/scene.py index d635384092f..c8c6ac2bcfb 100644 --- a/homeassistant/components/knx/scene.py +++ b/homeassistant/components/knx/scene.py @@ -1,5 +1,6 @@ """Support for KNX scenes.""" import voluptuous as vol +from xknx.devices import Scene as XknxScene from homeassistant.components.scene import CONF_PLATFORM, Scene from homeassistant.const import CONF_ADDRESS, CONF_NAME @@ -42,9 +43,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up scene for KNX platform configured within platform.""" - import xknx - - scene = xknx.devices.Scene( + scene = XknxScene( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address=config[CONF_ADDRESS], diff --git a/homeassistant/components/knx/sensor.py b/homeassistant/components/knx/sensor.py index 9a19ba91b7a..a0a0f6ea18d 100644 --- a/homeassistant/components/knx/sensor.py +++ b/homeassistant/components/knx/sensor.py @@ -1,5 +1,6 @@ """Support for KNX/IP sensors.""" import voluptuous as vol +from xknx.devices import Sensor as XknxSensor from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_NAME, CONF_TYPE @@ -44,9 +45,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up sensor for KNX platform configured within platform.""" - import xknx - - sensor = xknx.devices.Sensor( + sensor = XknxSensor( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address_state=config[CONF_STATE_ADDRESS], diff --git a/homeassistant/components/knx/switch.py b/homeassistant/components/knx/switch.py index 72a5b5dcdd7..e9a0df5c983 100644 --- a/homeassistant/components/knx/switch.py +++ b/homeassistant/components/knx/switch.py @@ -1,5 +1,6 @@ """Support for KNX/IP switches.""" import voluptuous as vol +from xknx.devices import Switch as XknxSwitch from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_ADDRESS, CONF_NAME @@ -41,9 +42,7 @@ def async_add_entities_discovery(hass, discovery_info, async_add_entities): @callback def async_add_entities_config(hass, config, async_add_entities): """Set up switch for KNX platform configured within platform.""" - import xknx - - switch = xknx.devices.Switch( + switch = XknxSwitch( hass.data[DATA_KNX].xknx, name=config[CONF_NAME], group_address=config[CONF_ADDRESS], From 3692c7496e1a3eaa7e67a8639fbe454b4c6dcc88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:38:33 +0000 Subject: [PATCH 569/639] Move imports in gtfs component (#27999) * Move imports in gtfs component * Fix pylint --- homeassistant/components/gtfs/sensor.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 086545f0c76..07b450dd33e 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -5,6 +5,8 @@ import os import threading from typing import Any, Callable, Optional +import pygtfs +from sqlalchemy.sql import text import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -129,8 +131,6 @@ def get_next_departure( tomorrow = now + datetime.timedelta(days=1) tomorrow_date = tomorrow.strftime(dt_util.DATE_STR_FORMAT) - from sqlalchemy.sql import text - # Fetch all departures for yesterday, today and optionally tomorrow, # up to an overkill maximum in case of a departure every minute for those # days. @@ -353,8 +353,6 @@ def setup_platform( _LOGGER.error("The given GTFS data file/folder was not found") return - import pygtfs - (gtfs_root, _) = os.path.splitext(data) sqlite_file = f"{gtfs_root}.sqlite?check_same_thread=False" @@ -375,7 +373,7 @@ class GTFSDepartureSensor(Entity): def __init__( self, - pygtfs: Any, + gtfs: Any, name: Optional[Any], origin: Any, destination: Any, @@ -383,7 +381,7 @@ class GTFSDepartureSensor(Entity): include_tomorrow: bool, ) -> None: """Initialize the sensor.""" - self._pygtfs = pygtfs + self._pygtfs = gtfs self.origin = origin self.destination = destination self._include_tomorrow = include_tomorrow From 4a54b130cb83d68cce260d5baa5f245dbe3307b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:39:36 +0000 Subject: [PATCH 570/639] Move imports in ptvsd component (#28087) --- homeassistant/components/ptvsd/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ptvsd/__init__.py b/homeassistant/components/ptvsd/__init__.py index 869987bfe4b..f12c8004774 100644 --- a/homeassistant/components/ptvsd/__init__.py +++ b/homeassistant/components/ptvsd/__init__.py @@ -4,10 +4,11 @@ Enable ptvsd debugger to attach to HA. Attach ptvsd debugger by default to port 5678. """ +from asyncio import Event import logging from threading import Thread -from asyncio import Event +import ptvsd import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT @@ -36,8 +37,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up ptvsd debugger.""" - import ptvsd - conf = config[DOMAIN] host = conf[CONF_HOST] port = conf[CONF_PORT] From 77d55a3b155ee82c0f70bca229fe24b04bd9cf9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 05:40:49 +0000 Subject: [PATCH 571/639] Move imports in isy994 component (#28004) --- homeassistant/components/isy994/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index 324dcb019b3..96796e37a6a 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -3,6 +3,8 @@ from collections import namedtuple import logging from urllib.parse import urlparse +import PyISY +from PyISY.Nodes import Group import voluptuous as vol from homeassistant.const import ( @@ -312,8 +314,6 @@ def _categorize_nodes( # Don't import this node as a device at all continue - from PyISY.Nodes import Group - if isinstance(node, Group): hass.data[ISY994_NODES][SCENE_DOMAIN].append(node) continue @@ -419,8 +419,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.error("isy994 host value in configuration is invalid") return False - import PyISY - # Connect to ISY controller. isy = PyISY.ISY( host.hostname, From acee87bef6edfa83d949a87b553bf92a9592277b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 22 Oct 2019 09:00:58 +0200 Subject: [PATCH 572/639] Support to use Whatsapp numbers (fixes ##28065) (#28078) --- homeassistant/components/twilio/__init__.py | 9 ++++----- homeassistant/components/twilio/config_flow.py | 1 - homeassistant/components/twilio/manifest.json | 2 +- homeassistant/components/twilio_call/notify.py | 5 ++--- homeassistant/components/twilio_sms/notify.py | 8 ++++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 7 files changed, 13 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/twilio/__init__.py b/homeassistant/components/twilio/__init__.py index ea5629e7cab..15c6697b2f7 100644 --- a/homeassistant/components/twilio/__init__.py +++ b/homeassistant/components/twilio/__init__.py @@ -1,9 +1,12 @@ """Support for Twilio.""" +from twilio.rest import Client +from twilio.twiml import TwiML import voluptuous as vol -import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.helpers import config_entry_flow +import homeassistant.helpers.config_validation as cv + from .const import DOMAIN CONF_ACCOUNT_SID = "account_sid" @@ -28,8 +31,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Twilio component.""" - from twilio.rest import Client - if DOMAIN not in config: return True @@ -42,8 +43,6 @@ async def async_setup(hass, config): async def handle_webhook(hass, webhook_id, request): """Handle incoming webhook from Twilio for inbound messages and calls.""" - from twilio.twiml import TwiML - data = dict(await request.post()) data["webhook_id"] = webhook_id hass.bus.async_fire(RECEIVED_DATA, dict(data)) diff --git a/homeassistant/components/twilio/config_flow.py b/homeassistant/components/twilio/config_flow.py index dad8e0bf496..1539c1ffadc 100644 --- a/homeassistant/components/twilio/config_flow.py +++ b/homeassistant/components/twilio/config_flow.py @@ -3,7 +3,6 @@ from homeassistant.helpers import config_entry_flow from .const import DOMAIN - config_entry_flow.register_webhook_flow( DOMAIN, "Twilio Webhook", diff --git a/homeassistant/components/twilio/manifest.json b/homeassistant/components/twilio/manifest.json index 23fac51a347..8f4ed125fb6 100644 --- a/homeassistant/components/twilio/manifest.json +++ b/homeassistant/components/twilio/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/twilio", "requirements": [ - "twilio==6.19.1" + "twilio==6.32.0" ], "dependencies": [ "webhook" diff --git a/homeassistant/components/twilio_call/notify.py b/homeassistant/components/twilio_call/notify.py index 0672a3d3b9e..82705091814 100644 --- a/homeassistant/components/twilio_call/notify.py +++ b/homeassistant/components/twilio_call/notify.py @@ -4,14 +4,13 @@ import urllib import voluptuous as vol -from homeassistant.components.twilio import DATA_TWILIO -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, ) +from homeassistant.components.twilio import DATA_TWILIO +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/twilio_sms/notify.py b/homeassistant/components/twilio_sms/notify.py index bd873f13468..da5e0e754b9 100644 --- a/homeassistant/components/twilio_sms/notify.py +++ b/homeassistant/components/twilio_sms/notify.py @@ -3,15 +3,14 @@ import logging import voluptuous as vol -from homeassistant.components.twilio import DATA_TWILIO -import homeassistant.helpers.config_validation as cv - from homeassistant.components.notify import ( + ATTR_DATA, ATTR_TARGET, PLATFORM_SCHEMA, BaseNotificationService, - ATTR_DATA, ) +from homeassistant.components.twilio import DATA_TWILIO +import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -26,6 +25,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( r"^\+?[1-9]\d{1,14}$|" r"^(?=.{1,11}$)[a-zA-Z0-9\s]*" r"[a-zA-Z][a-zA-Z0-9\s]*$" + r"^(?:[a-zA-Z]+)\:?\+?[1-9]\d{1,14}$|" ), ) } diff --git a/requirements_all.txt b/requirements_all.txt index 9a7ff3bb14b..149ad6234eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1909,7 +1909,7 @@ tuyaha==0.0.4 twentemilieu==0.1.0 # homeassistant.components.twilio -twilio==6.19.1 +twilio==6.32.0 # homeassistant.components.upcloud upcloud-api==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 047ed96bf10..71cbac4de0f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -596,7 +596,7 @@ transmissionrpc==0.11 twentemilieu==0.1.0 # homeassistant.components.twilio -twilio==6.19.1 +twilio==6.32.0 # homeassistant.components.uvc uvcclient==0.11.0 From 09b4f65515af4c6111f9a725050acad75f6e6445 Mon Sep 17 00:00:00 2001 From: Mark Coombes Date: Tue, 22 Oct 2019 11:22:42 -0400 Subject: [PATCH 573/639] Add modelnumber for ecobee4 (#28107) --- homeassistant/components/ecobee/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/ecobee/const.py b/homeassistant/components/ecobee/const.py index a6141d874f1..5022cb71903 100644 --- a/homeassistant/components/ecobee/const.py +++ b/homeassistant/components/ecobee/const.py @@ -19,6 +19,7 @@ ECOBEE_MODEL_TO_NAME = { "corSmart": "Carrier/Bryant Cor", "nikeSmart": "ecobee3 lite Smart", "nikeEms": "ecobee3 lite EMS", + "apolloSmart": "ecobee4 Smart", } ECOBEE_PLATFORMS = ["binary_sensor", "climate", "sensor", "weather"] From 37bf577284daea88ff84196ae86ee8c03464e7f2 Mon Sep 17 00:00:00 2001 From: Pascal Roeleven Date: Tue, 22 Oct 2019 17:23:39 +0200 Subject: [PATCH 574/639] Add support for more Orange Pi devices (#28109) * Bump OPi.GPIO to 0.4.0 * Move imports to top-level --- .../components/orangepi_gpio/__init__.py | 26 ++---------- .../components/orangepi_gpio/const.py | 40 ++++++++++++++++++- .../components/orangepi_gpio/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 44 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/orangepi_gpio/__init__.py b/homeassistant/components/orangepi_gpio/__init__.py index 7b686399d0f..71d8d65d8b8 100644 --- a/homeassistant/components/orangepi_gpio/__init__.py +++ b/homeassistant/components/orangepi_gpio/__init__.py @@ -6,6 +6,8 @@ from OPi import GPIO from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from .const import PIN_MODES + _LOGGER = logging.getLogger(__name__) DOMAIN = "orangepi_gpio" @@ -23,33 +25,13 @@ async def async_setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, cleanup_gpio) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, prepare_gpio) - return True def setup_mode(mode): """Set GPIO pin mode.""" - _LOGGER.debug("Setting GPIO pin mode as %s", mode) - if mode == "pc": - import orangepi.pc - - GPIO.setmode(orangepi.pc.BOARD) - elif mode == "zeroplus": - import orangepi.zeroplus - - GPIO.setmode(orangepi.zeroplus.BOARD) - elif mode == "zeroplus2": - import orangepi.zeroplus - - GPIO.setmode(orangepi.zeroplus2.BOARD) - elif mode == "duo": - import nanopi.duo - - GPIO.setmode(nanopi.duo.BOARD) - elif mode == "neocore2": - import nanopi.neocore2 - - GPIO.setmode(nanopi.neocore2.BOARD) + _LOGGER.debug("Setting GPIO pin mode as %s", PIN_MODES[mode]) + GPIO.setmode(PIN_MODES[mode]) def setup_input(port): diff --git a/homeassistant/components/orangepi_gpio/const.py b/homeassistant/components/orangepi_gpio/const.py index 928d75a1f98..47ddf5b7085 100644 --- a/homeassistant/components/orangepi_gpio/const.py +++ b/homeassistant/components/orangepi_gpio/const.py @@ -1,5 +1,23 @@ """Constants for Orange Pi GPIO.""" +from nanopi import duo, neocore2 +from orangepi import ( + lite, + lite2, + one, + oneplus, + pc, + pc2, + pcplus, + pi3, + plus2e, + prime, + r1, + winplus, + zero, + zeroplus, + zeroplus2, +) import voluptuous as vol from homeassistant.helpers import config_validation as cv @@ -8,12 +26,30 @@ CONF_INVERT_LOGIC = "invert_logic" CONF_PIN_MODE = "pin_mode" CONF_PORTS = "ports" DEFAULT_INVERT_LOGIC = False -PIN_MODES = ["pc", "zeroplus", "zeroplus2", "deo", "neocore2"] +PIN_MODES = { + "lite": lite.BOARD, + "lite2": lite2.BOARD, + "one": one.BOARD, + "oneplus": oneplus.BOARD, + "pc": pc.BOARD, + "pc2": pc2.BOARD, + "pcplus": pcplus.BOARD, + "pi3": pi3.BOARD, + "plus2e": plus2e.BOARD, + "prime": prime.BOARD, + "r1": r1.BOARD, + "winplus": winplus.BOARD, + "zero": zero.BOARD, + "zeroplus": zeroplus.BOARD, + "zeroplus2": zeroplus2.BOARD, + "duo": duo.BOARD, + "neocore2": neocore2.BOARD, +} _SENSORS_SCHEMA = vol.Schema({cv.positive_int: cv.string}) PORT_SCHEMA = { vol.Required(CONF_PORTS): _SENSORS_SCHEMA, - vol.Required(CONF_PIN_MODE): vol.In(PIN_MODES), + vol.Required(CONF_PIN_MODE): vol.In(PIN_MODES.keys()), vol.Optional(CONF_INVERT_LOGIC, default=DEFAULT_INVERT_LOGIC): cv.boolean, } diff --git a/homeassistant/components/orangepi_gpio/manifest.json b/homeassistant/components/orangepi_gpio/manifest.json index 51bca8fbbbe..52c8f8f509f 100644 --- a/homeassistant/components/orangepi_gpio/manifest.json +++ b/homeassistant/components/orangepi_gpio/manifest.json @@ -3,7 +3,7 @@ "name": "Orangepi GPIO", "documentation": "https://www.home-assistant.io/integrations/orangepi_gpio", "requirements": [ - "OPi.GPIO==0.3.6" + "OPi.GPIO==0.4.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 149ad6234eb..884155f3b4c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -41,7 +41,7 @@ HAP-python==2.6.0 Mastodon.py==1.5.0 # homeassistant.components.orangepi_gpio -OPi.GPIO==0.3.6 +OPi.GPIO==0.4.0 # homeassistant.components.essent PyEssent==0.13 From c2c9213e9b35d856a85543c5f41f996b1d61a5fc Mon Sep 17 00:00:00 2001 From: Santobert Date: Tue, 22 Oct 2019 17:28:06 +0200 Subject: [PATCH 575/639] Add improved scene support to the counter integration (#28103) * Add improved scene support to the counter integration * Remove comment --- .../components/counter/reproduce_state.py | 71 +++++++++++++++++++ .../counter/test_reproduce_state.py | 71 +++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 homeassistant/components/counter/reproduce_state.py create mode 100644 tests/components/counter/test_reproduce_state.py diff --git a/homeassistant/components/counter/reproduce_state.py b/homeassistant/components/counter/reproduce_state.py new file mode 100644 index 00000000000..ac5045d68e7 --- /dev/null +++ b/homeassistant/components/counter/reproduce_state.py @@ -0,0 +1,71 @@ +"""Reproduce an Counter state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_INITIAL, + ATTR_MAXIMUM, + ATTR_MINIMUM, + ATTR_STEP, + VALUE, + DOMAIN, + SERVICE_CONFIGURE, +) + +_LOGGER = logging.getLogger(__name__) + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if not state.state.isdigit(): + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if ( + cur_state.state == state.state + and cur_state.attributes.get(ATTR_INITIAL) == state.attributes.get(ATTR_INITIAL) + and cur_state.attributes.get(ATTR_MAXIMUM) == state.attributes.get(ATTR_MAXIMUM) + and cur_state.attributes.get(ATTR_MINIMUM) == state.attributes.get(ATTR_MINIMUM) + and cur_state.attributes.get(ATTR_STEP) == state.attributes.get(ATTR_STEP) + ): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id, VALUE: state.state} + service = SERVICE_CONFIGURE + if ATTR_INITIAL in state.attributes: + service_data[ATTR_INITIAL] = state.attributes[ATTR_INITIAL] + if ATTR_MAXIMUM in state.attributes: + service_data[ATTR_MAXIMUM] = state.attributes[ATTR_MAXIMUM] + if ATTR_MINIMUM in state.attributes: + service_data[ATTR_MINIMUM] = state.attributes[ATTR_MINIMUM] + if ATTR_STEP in state.attributes: + service_data[ATTR_STEP] = state.attributes[ATTR_STEP] + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Counter states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/counter/test_reproduce_state.py b/tests/components/counter/test_reproduce_state.py new file mode 100644 index 00000000000..aa2c5ddbd9a --- /dev/null +++ b/tests/components/counter/test_reproduce_state.py @@ -0,0 +1,71 @@ +"""Test reproduce state for Counter.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Counter states.""" + hass.states.async_set("counter.entity", "5", {}) + hass.states.async_set( + "counter.entity_attr", + "8", + {"initial": 12, "minimum": 5, "maximum": 15, "step": 3}, + ) + + configure_calls = async_mock_service(hass, "counter", "configure") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("counter.entity", "5"), + State( + "counter.entity_attr", + "8", + {"initial": 12, "minimum": 5, "maximum": 15, "step": 3}, + ), + ], + blocking=True, + ) + + assert len(configure_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("counter.entity", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(configure_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("counter.entity", "2"), + State( + "counter.entity_attr", + "7", + {"initial": 10, "minimum": 3, "maximum": 21, "step": 5}, + ), + # Should not raise + State("counter.non_existing", "6"), + ], + blocking=True, + ) + + valid_calls = [ + {"entity_id": "counter.entity", "value": "2"}, + { + "entity_id": "counter.entity_attr", + "value": "7", + "initial": 10, + "minimum": 3, + "maximum": 21, + "step": 5, + }, + ] + assert len(configure_calls) == 2 + for call in configure_calls: + assert call.domain == "counter" + assert call.data in valid_calls + valid_calls.remove(call.data) From 0226b76e0ae40ac8596f2ae1470d60b62faec8a3 Mon Sep 17 00:00:00 2001 From: bastshoes Date: Tue, 22 Oct 2019 18:39:26 +0300 Subject: [PATCH 576/639] Add support SQL VACUUM for PostgeSQL (#28106) * Add support SQL VACUUM for PostgeSQL VACUUM PostgreSQL DB if repack is true * Update tests --- homeassistant/components/recorder/purge.py | 4 ++-- tests/components/recorder/test_purge.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 2ac0b38c694..089476245fe 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -34,8 +34,8 @@ def purge_old_data(instance, purge_days, repack): _LOGGER.debug("Deleted %s events", deleted_rows) # Execute sqlite vacuum command to free up space on disk - if repack and instance.engine.driver == "pysqlite": - _LOGGER.debug("Vacuuming SQLite to free space") + if repack and instance.engine.driver in ("pysqlite", "postgresql"): + _LOGGER.debug("Vacuuming SQL DB to free space") instance.engine.execute("VACUUM") except SQLAlchemyError as err: diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 1c676e203d2..7e06dcd1e5e 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -174,5 +174,5 @@ class TestRecorderPurge(unittest.TestCase): self.hass.data[DATA_INSTANCE].block_till_done() assert ( mock_logger.debug.mock_calls[3][1][0] - == "Vacuuming SQLite to free space" + == "Vacuuming SQL DB to free space" ) From 04dbe5bc841e1a429873efbd850c35b823ef26ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Tue, 22 Oct 2019 16:50:49 +0000 Subject: [PATCH 577/639] Move imports in dsmr component (#27974) * Move imports in dsmr component * Review * Fix tests --- homeassistant/components/dsmr/sensor.py | 11 ++++------- tests/components/dsmr/test_sensor.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/dsmr/sensor.py b/homeassistant/components/dsmr/sensor.py index 82a81118dbd..253e8409f1b 100644 --- a/homeassistant/components/dsmr/sensor.py +++ b/homeassistant/components/dsmr/sensor.py @@ -4,6 +4,9 @@ from datetime import timedelta from functools import partial import logging +from dsmr_parser import obis_references as obis_ref +from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader +import serial import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA @@ -52,10 +55,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Suppress logging logging.getLogger("dsmr_parser").setLevel(logging.ERROR) - from dsmr_parser import obis_references as obis_ref - from dsmr_parser.clients.protocol import create_dsmr_reader, create_tcp_dsmr_reader - import serial - dsmr_version = config[CONF_DSMR_VERSION] # Define list of name,obis mappings to generate entities @@ -212,11 +211,9 @@ class DSMREntity(Entity): @property def state(self): """Return the state of sensor, if available, translate if needed.""" - from dsmr_parser import obis_references as obis - value = self.get_dsmr_object_attr("value") - if self._obis == obis.ELECTRICITY_ACTIVE_TARIFF: + if self._obis == obis_ref.ELECTRICITY_ACTIVE_TARIFF: return self.translate_tariff(value) try: diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 57dfa183feb..195345dd489 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -11,9 +11,11 @@ from decimal import Decimal from unittest.mock import Mock import asynctest +import pytest + from homeassistant.bootstrap import async_setup_component from homeassistant.components.dsmr.sensor import DerivativeDSMREntity -import pytest + from tests.common import assert_setup_component @@ -34,10 +36,11 @@ def mock_connection_factory(monkeypatch): # apply the mock to both connection factories monkeypatch.setattr( - "dsmr_parser.clients.protocol.create_dsmr_reader", connection_factory + "homeassistant.components.dsmr.sensor.create_dsmr_reader", connection_factory ) monkeypatch.setattr( - "dsmr_parser.clients.protocol.create_tcp_dsmr_reader", connection_factory + "homeassistant.components.dsmr.sensor.create_tcp_dsmr_reader", + connection_factory, ) return connection_factory, transport, protocol @@ -158,7 +161,8 @@ def test_connection_errors_retry(hass, monkeypatch, mock_connection_factory): ) monkeypatch.setattr( - "dsmr_parser.clients.protocol.create_dsmr_reader", first_fail_connection_factory + "homeassistant.components.dsmr.sensor.create_dsmr_reader", + first_fail_connection_factory, ) yield from async_setup_component(hass, "sensor", {"sensor": config}) From 4700d647b01bcc535927713fc5fe5054c2345b7b Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 22 Oct 2019 20:40:07 +0200 Subject: [PATCH 578/639] Minor tweaks for sensor device automations (#27829) * Minor tweaks for sensor device automations * Change unit_of_measurement to suffix in extra_fields * Address review comment --- .../components/sensor/device_trigger.py | 12 ++----- homeassistant/components/sensor/strings.json | 36 +++++++++---------- .../components/sensor/test_device_trigger.py | 4 +-- 3 files changed, 22 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 7eabc457161..b462124165a 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -11,7 +11,6 @@ from homeassistant.const import ( CONF_ENTITY_ID, CONF_FOR, CONF_TYPE, - CONF_UNIT_OF_MEASUREMENT, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, @@ -73,11 +72,6 @@ TRIGGER_SCHEMA = vol.All( ), vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), - vol.Optional(CONF_FOR): vol.Any( - vol.All(cv.time_period, cv.positive_timedelta), - cv.template, - cv.template_complex, - ), vol.Optional(CONF_FOR): cv.positive_time_period_dict, } ), @@ -159,12 +153,10 @@ async def async_get_trigger_capabilities(hass, config): "extra_fields": vol.Schema( { vol.Optional( - CONF_ABOVE, - description={CONF_UNIT_OF_MEASUREMENT: unit_of_measurement}, + CONF_ABOVE, description={"suffix": unit_of_measurement} ): vol.Coerce(float), vol.Optional( - CONF_BELOW, - description={CONF_UNIT_OF_MEASUREMENT: unit_of_measurement}, + CONF_BELOW, description={"suffix": unit_of_measurement} ): vol.Coerce(float), vol.Optional(CONF_FOR): cv.positive_time_period_dict, } diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 7df239facde..a05f57f4584 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} battery level", - "is_humidity": "{entity_name} humidity", - "is_illuminance": "{entity_name} illuminance", - "is_power": "{entity_name} power", - "is_pressure": "{entity_name} pressure", - "is_signal_strength": "{entity_name} signal strength", - "is_temperature": "{entity_name} temperature", - "is_timestamp": "{entity_name} timestamp", - "is_value": "{entity_name} value" + "is_battery_level": "Current {entity_name} battery level", + "is_humidity": "Current {entity_name} humidity", + "is_illuminance": "Current {entity_name} illuminance", + "is_power": "Current {entity_name} power", + "is_pressure": "Current {entity_name} pressure", + "is_signal_strength": "Current {entity_name} signal strength", + "is_temperature": "Current {entity_name} temperature", + "is_timestamp": "Current {entity_name} timestamp", + "is_value": "Current {entity_name} value" }, "trigger_type": { - "battery_level": "{entity_name} battery level", - "humidity": "{entity_name} humidity", - "illuminance": "{entity_name} illuminance", - "power": "{entity_name} power", - "pressure": "{entity_name} pressure", - "signal_strength": "{entity_name} signal strength", - "temperature": "{entity_name} temperature", - "timestamp": "{entity_name} timestamp", - "value": "{entity_name} value" + "battery_level": "{entity_name} battery level changes", + "humidity": "{entity_name} humidity changes", + "illuminance": "{entity_name} illuminance changes", + "power": "{entity_name} power changes", + "pressure": "{entity_name} pressure changes", + "signal_strength": "{entity_name} signal strength changes", + "temperature": "{entity_name} temperature changes", + "timestamp": "{entity_name} timestamp changes", + "value": "{entity_name} value changes" } } } diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 1bc7e5e1ee5..a21839fcebc 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -101,13 +101,13 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): expected_capabilities = { "extra_fields": [ { - "description": {"unit_of_measurement": "%"}, + "description": {"suffix": "%"}, "name": "above", "optional": True, "type": "float", }, { - "description": {"unit_of_measurement": "%"}, + "description": {"suffix": "%"}, "name": "below", "optional": True, "type": "float", From adb15286b489fc83ff307e75e2de753b1dfc7dc2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 23 Oct 2019 01:13:34 +0200 Subject: [PATCH 579/639] Fix test coverage, reverting top level import ptvsd (#28118) --- homeassistant/components/ptvsd/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/ptvsd/__init__.py b/homeassistant/components/ptvsd/__init__.py index f12c8004774..55cef1405d9 100644 --- a/homeassistant/components/ptvsd/__init__.py +++ b/homeassistant/components/ptvsd/__init__.py @@ -8,7 +8,6 @@ from asyncio import Event import logging from threading import Thread -import ptvsd import voluptuous as vol from homeassistant.const import CONF_HOST, CONF_PORT @@ -37,6 +36,12 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up ptvsd debugger.""" + + # This is a local import, since importing this at the top, will cause + # ptvsd to hook into `sys.settrace`. So does `coverage` to generate + # coverage, resulting in a battle and incomplete code test coverage. + import ptvsd # pylint: disable=import-outside-toplevel + conf = config[DOMAIN] host = conf[CONF_HOST] port = conf[CONF_PORT] From dc3aa43f739e321eb1cb6f912489f28365bf6826 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 23 Oct 2019 00:32:15 +0000 Subject: [PATCH 580/639] [ci skip] Translation update --- .../components/abode/.translations/es.json | 17 +++++++++ .../components/adguard/.translations/es.json | 2 + .../alarm_control_panel/.translations/es.json | 4 +- .../components/axis/.translations/es.json | 1 + .../binary_sensor/.translations/fr.json | 4 +- .../components/deconz/.translations/es.json | 1 + .../components/glances/.translations/de.json | 31 ++++++++++++++++ .../components/glances/.translations/es.json | 37 +++++++++++++++++++ .../components/glances/.translations/no.json | 14 +++++-- .../glances/.translations/zh-Hant.json | 37 +++++++++++++++++++ .../components/light/.translations/fr.json | 2 +- .../components/lock/.translations/es.json | 5 +++ .../opentherm_gw/.translations/de.json | 10 +++++ .../opentherm_gw/.translations/es.json | 11 ++++++ .../components/sensor/.translations/en.json | 36 +++++++++--------- .../components/sensor/.translations/fr.json | 16 ++++---- .../components/soma/.translations/es.json | 4 +- 17 files changed, 196 insertions(+), 36 deletions(-) create mode 100644 homeassistant/components/glances/.translations/de.json create mode 100644 homeassistant/components/glances/.translations/es.json create mode 100644 homeassistant/components/glances/.translations/zh-Hant.json diff --git a/homeassistant/components/abode/.translations/es.json b/homeassistant/components/abode/.translations/es.json index e0c1b6d6a7d..908e8f0fbc3 100644 --- a/homeassistant/components/abode/.translations/es.json +++ b/homeassistant/components/abode/.translations/es.json @@ -1,5 +1,22 @@ { "config": { + "abort": { + "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de Abode." + }, + "error": { + "connection_error": "No se puede conectar a Abode.", + "identifier_exists": "Cuenta ya registrada.", + "invalid_credentials": "Credenciales inv\u00e1lidas." + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "title": "Rellene la informaci\u00f3n de acceso Abode" + } + }, "title": "Abode" } } \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/es.json b/homeassistant/components/adguard/.translations/es.json index 5886d8e5c5b..c6946ab6120 100644 --- a/homeassistant/components/adguard/.translations/es.json +++ b/homeassistant/components/adguard/.translations/es.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, usted tiene {current_version}. Por favor, actualice su complemento Hass.io AdGuard Home.", + "adguard_home_outdated": "Esta integraci\u00f3n requiere AdGuard Home {minimal_version} o superior, usted tiene {current_version}.", "existing_instance_updated": "Se ha actualizado la configuraci\u00f3n existente.", "single_instance_allowed": "S\u00f3lo se permite una \u00fanica configuraci\u00f3n de AdGuard Home." }, diff --git a/homeassistant/components/alarm_control_panel/.translations/es.json b/homeassistant/components/alarm_control_panel/.translations/es.json index a704080a2b4..273efeeaba5 100644 --- a/homeassistant/components/alarm_control_panel/.translations/es.json +++ b/homeassistant/components/alarm_control_panel/.translations/es.json @@ -2,8 +2,8 @@ "device_automation": { "action_type": { "arm_away": "Armar {entity_name} exterior", - "arm_home": "Armar {entity_name} casa", - "arm_night": "Armar {entity_name}", + "arm_home": "Armar {entity_name} modo casa", + "arm_night": "Armar {entity_name} por la noche", "disarm": "Desarmar {entity_name}", "trigger": "Lanzar {entity_name}" } diff --git a/homeassistant/components/axis/.translations/es.json b/homeassistant/components/axis/.translations/es.json index d29481a3be9..3f7db674fdf 100644 --- a/homeassistant/components/axis/.translations/es.json +++ b/homeassistant/components/axis/.translations/es.json @@ -12,6 +12,7 @@ "device_unavailable": "El dispositivo no est\u00e1 disponible", "faulty_credentials": "Credenciales de usuario incorrectas" }, + "flow_title": "Dispositivo Axis: {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json index 9a04ea17747..4d9bcefbe66 100644 --- a/homeassistant/components/binary_sensor/.translations/fr.json +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -9,7 +9,7 @@ "is_light": "{entity_name} d\u00e9tecte de la lumi\u00e8re", "is_locked": "{entity_name} est verrouill\u00e9", "is_moist": "{entity_name} est humide", - "is_motion": "{entity_name} d\u00e9tecte un mouvement", + "is_motion": "{entity_name} d\u00e9tecte du mouvement", "is_moving": "{entity_name} se d\u00e9place", "is_no_gas": "{entity_name} ne d\u00e9tecte pas de gaz", "is_no_light": "{entity_name} ne d\u00e9tecte pas de lumi\u00e8re", @@ -85,7 +85,7 @@ "problem": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter un probl\u00e8me", "smoke": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter la fum\u00e9e", "sound": "{entity_name} commenc\u00e9 \u00e0 d\u00e9tecter le son", - "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} activ\u00e9", "unsafe": "{entity_name} est devenu dangereux", "vibration": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter les vibrations" diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 04a08d185b3..d4f8de9f282 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -11,6 +11,7 @@ "error": { "no_key": "No se pudo obtener una clave API" }, + "flow_title": "pasarela deCONZ Zigbee ({host})", "step": { "hassio_confirm": { "data": { diff --git a/homeassistant/components/glances/.translations/de.json b/homeassistant/components/glances/.translations/de.json new file mode 100644 index 00000000000..04fed0fdc49 --- /dev/null +++ b/homeassistant/components/glances/.translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Host ist bereits konfiguriert." + }, + "error": { + "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "wrong_version": "Version nicht unterst\u00fctzt (nur 2 oder 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Aktualisierungsfrequenz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/es.json b/homeassistant/components/glances/.translations/es.json new file mode 100644 index 00000000000..1b6b0335192 --- /dev/null +++ b/homeassistant/components/glances/.translations/es.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se puede conectar al host", + "wrong_version": "Versi\u00f3n no soportada (s\u00f3lo 2 o 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "ssl": "Utilice SSL/TLS para conectarse al sistema Glances", + "username": "Nombre de usuario", + "verify_ssl": "Verificar la certificaci\u00f3n del sistema", + "version": "Versi\u00f3n API Glances (2 o 3)" + }, + "title": "Configurar Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Frecuencia de actualizaci\u00f3n" + }, + "description": "Configurar opciones para Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/no.json b/homeassistant/components/glances/.translations/no.json index 7d742cad262..7cf28cc34d0 100644 --- a/homeassistant/components/glances/.translations/no.json +++ b/homeassistant/components/glances/.translations/no.json @@ -14,17 +14,23 @@ "name": "Navn", "password": "Passord", "port": "Port", - "username": "Brukernavn" - } + "ssl": "Bruk SSL / TLS for \u00e5 koble til Glances-systemet", + "username": "Brukernavn", + "verify_ssl": "Bekreft sertifiseringen av systemet", + "version": "Glances API-versjon (2 eller 3)" + }, + "title": "Oppsett av Glances" } - } + }, + "title": "Glances" }, "options": { "step": { "init": { "data": { "scan_interval": "Oppdater frekvens" - } + }, + "description": "Konfigurasjonsalternativer for Glances" } } } diff --git a/homeassistant/components/glances/.translations/zh-Hant.json b/homeassistant/components/glances/.translations/zh-Hant.json new file mode 100644 index 00000000000..12ba7670355 --- /dev/null +++ b/homeassistant/components/glances/.translations/zh-Hant.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "error": { + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef", + "wrong_version": "\u7248\u672c\u4e0d\u652f\u63f4\uff08\u50c5 2 \u6216 3\uff09" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u4f7f\u7528 SSL/TLS \u9023\u7dda\u81f3 Glances \u7cfb\u7d71", + "username": "\u4f7f\u7528\u8005\u540d\u7a31", + "verify_ssl": "\u9a57\u8b49\u7cfb\u7d71\u8a8d\u8b49", + "version": "Glances API \u7248\u672c\uff082 \u6216 3\uff09" + }, + "title": "\u8a2d\u5b9a Glances" + } + }, + "title": "Glances" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u983b\u7387" + }, + "description": "Glances \u8a2d\u5b9a\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/.translations/fr.json b/homeassistant/components/light/.translations/fr.json index fd30e931718..4a1dc82bbd6 100644 --- a/homeassistant/components/light/.translations/fr.json +++ b/homeassistant/components/light/.translations/fr.json @@ -10,7 +10,7 @@ "is_on": "{entity_name} est allum\u00e9" }, "trigger_type": { - "turned_off": "{entity_name} d\u00e9sactiv\u00e9", + "turned_off": "{entity_name} est d\u00e9sactiv\u00e9", "turned_on": "{entity_name} activ\u00e9" } } diff --git a/homeassistant/components/lock/.translations/es.json b/homeassistant/components/lock/.translations/es.json index 5c23c270f61..c6ef789e9cb 100644 --- a/homeassistant/components/lock/.translations/es.json +++ b/homeassistant/components/lock/.translations/es.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Bloquear {entity_name}", + "open": "Abrir {entity_name}", + "unlock": "Desbloquear {entity_name}" + }, "condition_type": { "is_locked": "{entity_name} est\u00e1 bloqueado", "is_unlocked": "{entity_name} est\u00e1 desbloqueado" diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json index 0957e233116..3b18aa71b6c 100644 --- a/homeassistant/components/opentherm_gw/.translations/de.json +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -19,5 +19,15 @@ } }, "title": "OpenTherm Gateway" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Boden-Temperatur", + "precision": "Genauigkeit" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/es.json b/homeassistant/components/opentherm_gw/.translations/es.json index 8ad9d89b07a..bb8a8b20f36 100644 --- a/homeassistant/components/opentherm_gw/.translations/es.json +++ b/homeassistant/components/opentherm_gw/.translations/es.json @@ -19,5 +19,16 @@ } }, "title": "Gateway OpenTherm" + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura del suelo", + "precision": "Precisi\u00f3n" + }, + "description": "Opciones para OpenTherm Gateway" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/en.json b/homeassistant/components/sensor/.translations/en.json index 7bbbe660feb..07411b885b8 100644 --- a/homeassistant/components/sensor/.translations/en.json +++ b/homeassistant/components/sensor/.translations/en.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} battery level", - "is_humidity": "{entity_name} humidity", - "is_illuminance": "{entity_name} illuminance", - "is_power": "{entity_name} power", - "is_pressure": "{entity_name} pressure", - "is_signal_strength": "{entity_name} signal strength", - "is_temperature": "{entity_name} temperature", - "is_timestamp": "{entity_name} timestamp", - "is_value": "{entity_name} value" + "is_battery_level": "Current {entity_name} battery level", + "is_humidity": "Current {entity_name} humidity", + "is_illuminance": "Current {entity_name} illuminance", + "is_power": "Current {entity_name} power", + "is_pressure": "Current {entity_name} pressure", + "is_signal_strength": "Current {entity_name} signal strength", + "is_temperature": "Current {entity_name} temperature", + "is_timestamp": "Current {entity_name} timestamp", + "is_value": "Current {entity_name} value" }, "trigger_type": { - "battery_level": "{entity_name} battery level", - "humidity": "{entity_name} humidity", - "illuminance": "{entity_name} illuminance", - "power": "{entity_name} power", - "pressure": "{entity_name} pressure", - "signal_strength": "{entity_name} signal strength", - "temperature": "{entity_name} temperature", - "timestamp": "{entity_name} timestamp", - "value": "{entity_name} value" + "battery_level": "{entity_name} battery level changes", + "humidity": "{entity_name} humidity changes", + "illuminance": "{entity_name} illuminance changes", + "power": "{entity_name} power changes", + "pressure": "{entity_name} pressure changes", + "signal_strength": "{entity_name} signal strength changes", + "temperature": "{entity_name} temperature changes", + "timestamp": "{entity_name} timestamp changes", + "value": "{entity_name} value changes" } } } \ No newline at end of file diff --git a/homeassistant/components/sensor/.translations/fr.json b/homeassistant/components/sensor/.translations/fr.json index 676a5aa413f..56725a59e21 100644 --- a/homeassistant/components/sensor/.translations/fr.json +++ b/homeassistant/components/sensor/.translations/fr.json @@ -1,24 +1,24 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} niveau batterie", - "is_humidity": "{entity_name} humidit\u00e9", - "is_illuminance": "{entity_name} \u00e9clairement", + "is_battery_level": "Le niveau de la batterie de {entity_name}", + "is_humidity": "L'humidit\u00e9 de {entity_name}", + "is_illuminance": "L'\u00e9clairement de {entity_name}", "is_power": "{entity_name} puissance", "is_pressure": "{entity_name} pression", "is_signal_strength": "{entity_name} force du signal", - "is_temperature": "{entity_name} temp\u00e9rature", + "is_temperature": "La temp\u00e9rature de {entity_name}", "is_timestamp": "{entity_name} horodatage", "is_value": "{entity_name} valeur" }, "trigger_type": { - "battery_level": "{entity_name} niveau batterie", - "humidity": "{entity_name} humidit\u00e9", - "illuminance": "{entity_name} \u00e9clairement", + "battery_level": "Le niveau de la batterie de {entity_name}", + "humidity": "L'humidit\u00e9 de {entity_name}", + "illuminance": "L'\u00e9clairement de {entity_name}", "power": "{entity_name} puissance", "pressure": "{entity_name} pression", "signal_strength": "{entity_name} force du signal", - "temperature": "{entity_name} temp\u00e9rature", + "temperature": "La temp\u00e9rature de {entity_name}", "timestamp": "{entity_name} horodatage", "value": "{entity_name} valeur" } diff --git a/homeassistant/components/soma/.translations/es.json b/homeassistant/components/soma/.translations/es.json index b539130ea59..86922622704 100644 --- a/homeassistant/components/soma/.translations/es.json +++ b/homeassistant/components/soma/.translations/es.json @@ -13,7 +13,9 @@ "data": { "host": "Host", "port": "Puerto" - } + }, + "description": "Por favor, introduzca los ajustes de conexi\u00f3n de SOMA Connect.", + "title": "SOMA Connect" } }, "title": "Soma" From da094e09fa270b883abbe1f67256e51254f9dce3 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 23 Oct 2019 01:01:03 -0400 Subject: [PATCH 581/639] Implement ToggleController, RangeController, and ModeController in alexa (#27302) * Implement AlexaToggleController, AlexaRangeController, and AlexaModeController interfaces. * Implement AlexaToggleController, AlexaRangeController, and AlexaModeController interfaces. * Unkerfuffled comments to please the pydocstyle gods. * Unkerfuffled comments in Tests to please the pydocstyle gods. * Added additional test for more coverage. * Removed OSCILLATING property check from from ModeController. * Added capability report tests for ModeController, ToggleController, RangeController, PowerLevelController. * Update homeassistant/components/alexa/capabilities.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/alexa/capabilities.py Co-Authored-By: Paulus Schoutsen * Corrected mis-spelling of AlexaCapability class. * Changed instance from method to property in AlexaCapability class. * Refactored to add {entity.domain}.{entity.attribute} to the instance name. * Improved type handling for configuration object. Added additional test for configuration object. * Added Tests for unsupported domains for ModeController and RangeController * Made changes to improve future scaling for other domains. * Split fan range to speed maps into multiple constants. --- .../components/alexa/capabilities.py | 375 ++++++++++++++++-- homeassistant/components/alexa/const.py | 172 +++++++- homeassistant/components/alexa/entities.py | 16 + homeassistant/components/alexa/errors.py | 14 + homeassistant/components/alexa/handlers.py | 194 ++++++++- homeassistant/components/alexa/messages.py | 5 +- tests/components/alexa/__init__.py | 11 +- tests/components/alexa/test_capabilities.py | 69 +++- tests/components/alexa/test_smart_home.py | 339 +++++++++++++++- tests/components/alexa/test_state_report.py | 48 +++ 10 files changed, 1190 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 7be3188fea1..f4d93026649 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -23,19 +23,20 @@ import homeassistant.util.color as color_util import homeassistant.util.dt as dt_util from .const import ( + Catalog, API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, DATE_FORMAT, PERCENTAGE_FAN_MAP, + RANGE_FAN_MAP, ) from .errors import UnsupportedProperty - _LOGGER = logging.getLogger(__name__) -class AlexaCapibility: +class AlexaCapability: """Base class for Alexa capability interfaces. The Smart Home Skills API defines a number of "capability interfaces", @@ -45,9 +46,10 @@ class AlexaCapibility: https://developer.amazon.com/docs/device-apis/message-guide.html """ - def __init__(self, entity): - """Initialize an Alexa capibility.""" + def __init__(self, entity, instance=None): + """Initialize an Alexa capability.""" self.entity = entity + self.instance = instance def name(self): """Return the Alexa API name of this interface.""" @@ -68,6 +70,11 @@ class AlexaCapibility: """Return True if properties can be retrieved.""" return False + @staticmethod + def properties_non_controllable(): + """Return True if non controllable.""" + return None + @staticmethod def get_property(name): """Read and return a property. @@ -84,9 +91,14 @@ class AlexaCapibility: """Applicable only to scenes.""" return None + @staticmethod + def capability_resources(): + """Applicable to ToggleController, RangeController, and ModeController interfaces.""" + return [] + @staticmethod def configuration(): - """Applicable only to security control panel.""" + """Return the Configuration object.""" return [] def serialize_discovery(self): @@ -102,15 +114,29 @@ class AlexaCapibility: }, } + # pylint: disable=assignment-from-none + non_controllable = self.properties_non_controllable() + if non_controllable is not None: + result["properties"]["nonControllable"] = non_controllable + # pylint: disable=assignment-from-none supports_deactivation = self.supports_deactivation() if supports_deactivation is not None: result["supportsDeactivation"] = supports_deactivation + capability_resources = self.serialize_capability_resources() + if capability_resources: + result["capabilityResources"] = capability_resources + configuration = self.configuration() if configuration: result["configuration"] = configuration + # pylint: disable=assignment-from-none + instance = self.instance + if instance is not None: + result["instance"] = instance + return result def serialize_properties(self): @@ -120,16 +146,51 @@ class AlexaCapibility: # pylint: disable=assignment-from-no-return prop_value = self.get_property(prop_name) if prop_value is not None: - yield { + result = { "name": prop_name, "namespace": self.name(), "value": prop_value, "timeOfSample": dt_util.utcnow().strftime(DATE_FORMAT), "uncertaintyInMilliseconds": 0, } + instance = self.instance + if instance is not None: + result["instance"] = instance + + yield result + + def serialize_capability_resources(self): + """Return capabilityResources friendlyNames serialized for an API response.""" + resources = self.capability_resources() + if resources: + return {"friendlyNames": self.serialize_friendly_names(resources)} + + return None + + @staticmethod + def serialize_friendly_names(resources): + """Return capabilityResources, ModeResources, or presetResources friendlyNames serialized for an API response.""" + friendly_names = [] + for resource in resources: + if resource["type"] == Catalog.LABEL_ASSET: + friendly_names.append( + { + "@type": Catalog.LABEL_ASSET, + "value": {"assetId": resource["value"]}, + } + ) + else: + friendly_names.append( + { + "@type": Catalog.LABEL_TEXT, + "value": {"text": resource["value"], "locale": "en-US"}, + } + ) + + return friendly_names -class AlexaEndpointHealth(AlexaCapibility): +class AlexaEndpointHealth(AlexaCapability): """Implements Alexa.EndpointHealth. https://developer.amazon.com/docs/smarthome/state-reporting-for-a-smart-home-skill.html#report-state-when-alexa-requests-it @@ -166,7 +227,7 @@ class AlexaEndpointHealth(AlexaCapibility): return {"value": "OK"} -class AlexaPowerController(AlexaCapibility): +class AlexaPowerController(AlexaCapability): """Implements Alexa.PowerController. https://developer.amazon.com/docs/device-apis/alexa-powercontroller.html @@ -202,7 +263,7 @@ class AlexaPowerController(AlexaCapibility): return "ON" if is_on else "OFF" -class AlexaLockController(AlexaCapibility): +class AlexaLockController(AlexaCapability): """Implements Alexa.LockController. https://developer.amazon.com/docs/device-apis/alexa-lockcontroller.html @@ -236,7 +297,7 @@ class AlexaLockController(AlexaCapibility): return "JAMMED" -class AlexaSceneController(AlexaCapibility): +class AlexaSceneController(AlexaCapability): """Implements Alexa.SceneController. https://developer.amazon.com/docs/device-apis/alexa-scenecontroller.html @@ -252,7 +313,7 @@ class AlexaSceneController(AlexaCapibility): return "Alexa.SceneController" -class AlexaBrightnessController(AlexaCapibility): +class AlexaBrightnessController(AlexaCapability): """Implements Alexa.BrightnessController. https://developer.amazon.com/docs/device-apis/alexa-brightnesscontroller.html @@ -283,7 +344,7 @@ class AlexaBrightnessController(AlexaCapibility): return 0 -class AlexaColorController(AlexaCapibility): +class AlexaColorController(AlexaCapability): """Implements Alexa.ColorController. https://developer.amazon.com/docs/device-apis/alexa-colorcontroller.html @@ -315,7 +376,7 @@ class AlexaColorController(AlexaCapibility): } -class AlexaColorTemperatureController(AlexaCapibility): +class AlexaColorTemperatureController(AlexaCapability): """Implements Alexa.ColorTemperatureController. https://developer.amazon.com/docs/device-apis/alexa-colortemperaturecontroller.html @@ -344,7 +405,7 @@ class AlexaColorTemperatureController(AlexaCapibility): return None -class AlexaPercentageController(AlexaCapibility): +class AlexaPercentageController(AlexaCapability): """Implements Alexa.PercentageController. https://developer.amazon.com/docs/device-apis/alexa-percentagecontroller.html @@ -378,7 +439,7 @@ class AlexaPercentageController(AlexaCapibility): return 0 -class AlexaSpeaker(AlexaCapibility): +class AlexaSpeaker(AlexaCapability): """Implements Alexa.Speaker. https://developer.amazon.com/docs/device-apis/alexa-speaker.html @@ -389,7 +450,7 @@ class AlexaSpeaker(AlexaCapibility): return "Alexa.Speaker" -class AlexaStepSpeaker(AlexaCapibility): +class AlexaStepSpeaker(AlexaCapability): """Implements Alexa.StepSpeaker. https://developer.amazon.com/docs/device-apis/alexa-stepspeaker.html @@ -400,7 +461,7 @@ class AlexaStepSpeaker(AlexaCapibility): return "Alexa.StepSpeaker" -class AlexaPlaybackController(AlexaCapibility): +class AlexaPlaybackController(AlexaCapability): """Implements Alexa.PlaybackController. https://developer.amazon.com/docs/device-apis/alexa-playbackcontroller.html @@ -411,7 +472,7 @@ class AlexaPlaybackController(AlexaCapibility): return "Alexa.PlaybackController" -class AlexaInputController(AlexaCapibility): +class AlexaInputController(AlexaCapability): """Implements Alexa.InputController. https://developer.amazon.com/docs/device-apis/alexa-inputcontroller.html @@ -422,7 +483,7 @@ class AlexaInputController(AlexaCapibility): return "Alexa.InputController" -class AlexaTemperatureSensor(AlexaCapibility): +class AlexaTemperatureSensor(AlexaCapability): """Implements Alexa.TemperatureSensor. https://developer.amazon.com/docs/device-apis/alexa-temperaturesensor.html @@ -472,7 +533,7 @@ class AlexaTemperatureSensor(AlexaCapibility): return {"value": temp, "scale": API_TEMP_UNITS[unit]} -class AlexaContactSensor(AlexaCapibility): +class AlexaContactSensor(AlexaCapability): """Implements Alexa.ContactSensor. The Alexa.ContactSensor interface describes the properties and events used @@ -514,7 +575,7 @@ class AlexaContactSensor(AlexaCapibility): return "NOT_DETECTED" -class AlexaMotionSensor(AlexaCapibility): +class AlexaMotionSensor(AlexaCapability): """Implements Alexa.MotionSensor. https://developer.amazon.com/docs/device-apis/alexa-motionsensor.html @@ -551,7 +612,7 @@ class AlexaMotionSensor(AlexaCapibility): return "NOT_DETECTED" -class AlexaThermostatController(AlexaCapibility): +class AlexaThermostatController(AlexaCapability): """Implements Alexa.ThermostatController. https://developer.amazon.com/docs/device-apis/alexa-thermostatcontroller.html @@ -631,7 +692,7 @@ class AlexaThermostatController(AlexaCapibility): return {"value": temp, "scale": API_TEMP_UNITS[unit]} -class AlexaPowerLevelController(AlexaCapibility): +class AlexaPowerLevelController(AlexaCapability): """Implements Alexa.PowerLevelController. https://developer.amazon.com/docs/device-apis/alexa-powerlevelcontroller.html @@ -666,7 +727,7 @@ class AlexaPowerLevelController(AlexaCapibility): return None -class AlexaSecurityPanelController(AlexaCapibility): +class AlexaSecurityPanelController(AlexaCapability): """Implements Alexa.SecurityPanelController. https://developer.amazon.com/docs/device-apis/alexa-securitypanelcontroller.html @@ -710,9 +771,271 @@ class AlexaSecurityPanelController(AlexaCapibility): return "DISARMED" def configuration(self): - """Return supported authorization types.""" + """Return configuration object with supported authorization types.""" code_format = self.entity.attributes.get(ATTR_CODE_FORMAT) if code_format == FORMAT_NUMBER: return {"supportedAuthorizationTypes": [{"type": "FOUR_DIGIT_PIN"}]} - return [] + return None + + +class AlexaModeController(AlexaCapability): + """Implements Alexa.ModeController. + + https://developer.amazon.com/docs/device-apis/alexa-modecontroller.html + """ + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ModeController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "mode"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + + def get_property(self, name): + """Read and return a property.""" + if name != "mode": + raise UnsupportedProperty(name) + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + return self.entity.attributes.get(fan.ATTR_DIRECTION) + + return None + + def configuration(self): + """Return configuration with modeResources.""" + return self.serialize_mode_resources() + + def capability_resources(self): + """Return capabilityResources object.""" + capability_resources = [] + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + capability_resources = [ + {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_DIRECTION} + ] + + return capability_resources + + def mode_resources(self): + """Return modeResources object.""" + mode_resources = None + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + mode_resources = { + "ordered": False, + "resources": [ + { + "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_FORWARD}", + "friendly_names": [ + {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_FORWARD} + ], + }, + { + "value": f"{fan.ATTR_DIRECTION}.{fan.DIRECTION_REVERSE}", + "friendly_names": [ + {"type": Catalog.LABEL_TEXT, "value": fan.DIRECTION_REVERSE} + ], + }, + ], + } + + return mode_resources + + def serialize_mode_resources(self): + """Return ModeResources, friendlyNames serialized for an API response.""" + mode_resources = [] + resources = self.mode_resources() + ordered = resources["ordered"] + for resource in resources["resources"]: + mode_value = resource["value"] + friendly_names = resource["friendly_names"] + result = { + "value": mode_value, + "modeResources": { + "friendlyNames": self.serialize_friendly_names(friendly_names) + }, + } + mode_resources.append(result) + + return {"ordered": ordered, "supportedModes": mode_resources} + + +class AlexaRangeController(AlexaCapability): + """Implements Alexa.RangeController. + + https://developer.amazon.com/docs/device-apis/alexa-rangecontroller.html + """ + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.RangeController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "rangeValue"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "rangeValue": + raise UnsupportedProperty(name) + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + speed = self.entity.attributes.get(fan.ATTR_SPEED) + return RANGE_FAN_MAP.get(speed, 0) + + return None + + def configuration(self): + """Return configuration with presetResources.""" + return self.serialize_preset_resources() + + def capability_resources(self): + """Return capabilityResources object.""" + capability_resources = [] + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + return [{"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_FANSPEED}] + + return capability_resources + + def preset_resources(self): + """Return presetResources object.""" + preset_resources = [] + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + preset_resources = { + "minimumValue": 1, + "maximumValue": 3, + "precision": 1, + "presets": [ + { + "rangeValue": 1, + "names": [ + { + "type": Catalog.LABEL_ASSET, + "value": Catalog.VALUE_MINIMUM, + }, + {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_LOW}, + ], + }, + { + "rangeValue": 2, + "names": [ + {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_MEDIUM} + ], + }, + { + "rangeValue": 3, + "names": [ + { + "type": Catalog.LABEL_ASSET, + "value": Catalog.VALUE_MAXIMUM, + }, + {"type": Catalog.LABEL_ASSET, "value": Catalog.VALUE_HIGH}, + ], + }, + ], + } + + return preset_resources + + def serialize_preset_resources(self): + """Return PresetResources, friendlyNames serialized for an API response.""" + preset_resources = [] + resources = self.preset_resources() + for preset in resources["presets"]: + preset_resources.append( + { + "rangeValue": preset["rangeValue"], + "presetResources": { + "friendlyNames": self.serialize_friendly_names(preset["names"]) + }, + } + ) + + return { + "supportedRange": { + "minimumValue": resources["minimumValue"], + "maximumValue": resources["maximumValue"], + "precision": resources["precision"], + }, + "presets": preset_resources, + } + + +class AlexaToggleController(AlexaCapability): + """Implements Alexa.ToggleController. + + https://developer.amazon.com/docs/device-apis/alexa-togglecontroller.html + """ + + def __init__(self, entity, instance, non_controllable=False): + """Initialize the entity.""" + super().__init__(entity, instance) + self.properties_non_controllable = lambda: non_controllable + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ToggleController" + + def properties_supported(self): + """Return what properties this entity supports.""" + return [{"name": "toggleState"}] + + def properties_proactively_reported(self): + """Return True if properties asynchronously reported.""" + return True + + def properties_retrievable(self): + """Return True if properties can be retrieved.""" + return True + + def get_property(self, name): + """Read and return a property.""" + if name != "toggleState": + raise UnsupportedProperty(name) + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + is_on = bool(self.entity.attributes.get(fan.ATTR_OSCILLATING)) + return "ON" if is_on else "OFF" + + return None + + def capability_resources(self): + """Return capabilityResources object.""" + capability_resources = [] + + if self.instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + capability_resources = [ + {"type": Catalog.LABEL_ASSET, "value": Catalog.SETTING_OSCILLATE}, + {"type": Catalog.LABEL_TEXT, "value": "Rotate"}, + {"type": Catalog.LABEL_TEXT, "value": "Rotation"}, + ] + + return capability_resources diff --git a/homeassistant/components/alexa/const.py b/homeassistant/components/alexa/const.py index cd0cb85a0a5..8d1f0ac95a5 100644 --- a/homeassistant/components/alexa/const.py +++ b/homeassistant/components/alexa/const.py @@ -5,7 +5,6 @@ from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.components.climate import const as climate from homeassistant.components import fan - DOMAIN = "alexa" # Flash briefing constants @@ -69,6 +68,20 @@ PERCENTAGE_FAN_MAP = { fan.SPEED_HIGH: 100, } +RANGE_FAN_MAP = { + fan.SPEED_OFF: 0, + fan.SPEED_LOW: 1, + fan.SPEED_MEDIUM: 2, + fan.SPEED_HIGH: 3, +} + +SPEED_FAN_MAP = { + 0: fan.SPEED_OFF, + 1: fan.SPEED_LOW, + 2: fan.SPEED_MEDIUM, + 3: fan.SPEED_HIGH, +} + class Cause: """Possible causes for property changes. @@ -101,3 +114,160 @@ class Cause: # Indicates that the event was caused by a voice interaction with Alexa. # For example a user speaking to their Echo device. VOICE_INTERACTION = "VOICE_INTERACTION" + + +class Catalog: + """The Global Alexa catalog. + + https://developer.amazon.com/docs/device-apis/resources-and-assets.html#global-alexa-catalog + + You can use the global Alexa catalog for pre-defined names of devices, settings, values, and units. + This catalog is localized into all the languages that Alexa supports. + + You can reference the following catalog of pre-defined friendly names. + Each item in the following list is an asset identifier followed by its supported friendly names. + The first friendly name for each identifier is the one displayed in the Alexa mobile app. + """ + + LABEL_ASSET = "asset" + LABEL_TEXT = "text" + + # Shower + DEVICENAME_SHOWER = "Alexa.DeviceName.Shower" + + # Washer, Washing Machine + DEVICENAME_WASHER = "Alexa.DeviceName.Washer" + + # Router, Internet Router, Network Router, Wifi Router, Net Router + DEVICENAME_ROUTER = "Alexa.DeviceName.Router" + + # Fan, Blower + DEVICENAME_FAN = "Alexa.DeviceName.Fan" + + # Air Purifier, Air Cleaner,Clean Air Machine + DEVICENAME_AIRPURIFIER = "Alexa.DeviceName.AirPurifier" + + # Space Heater, Portable Heater + DEVICENAME_SPACEHEATER = "Alexa.DeviceName.SpaceHeater" + + # Rain Head, Overhead shower, Rain Shower, Rain Spout, Rain Faucet + SHOWER_RAINHEAD = "Alexa.Shower.RainHead" + + # Handheld Shower, Shower Wand, Hand Shower + SHOWER_HANDHELD = "Alexa.Shower.HandHeld" + + # Water Temperature, Water Temp, Water Heat + SETTING_WATERTEMPERATURE = "Alexa.Setting.WaterTemperature" + + # Temperature, Temp + SETTING_TEMPERATURE = "Alexa.Setting.Temperature" + + # Wash Cycle, Wash Preset, Wash setting + SETTING_WASHCYCLE = "Alexa.Setting.WashCycle" + + # 2.4G Guest Wi-Fi, 2.4G Guest Network, Guest Network 2.4G, 2G Guest Wifi + SETTING_2GGUESTWIFI = "Alexa.Setting.2GGuestWiFi" + + # 5G Guest Wi-Fi, 5G Guest Network, Guest Network 5G, 5G Guest Wifi + SETTING_5GGUESTWIFI = "Alexa.Setting.5GGuestWiFi" + + # Guest Wi-fi, Guest Network, Guest Net + SETTING_GUESTWIFI = "Alexa.Setting.GuestWiFi" + + # Auto, Automatic, Automatic Mode, Auto Mode + SETTING_AUTO = "Alexa.Setting.Auto" + + # #Night, Night Mode + SETTING_NIGHT = "Alexa.Setting.Night" + + # Quiet, Quiet Mode, Noiseless, Silent + SETTING_QUIET = "Alexa.Setting.Quiet" + + # Oscillate, Swivel, Oscillation, Spin, Back and forth + SETTING_OSCILLATE = "Alexa.Setting.Oscillate" + + # Fan Speed, Airflow speed, Wind Speed, Air speed, Air velocity + SETTING_FANSPEED = "Alexa.Setting.FanSpeed" + + # Preset, Setting + SETTING_PRESET = "Alexa.Setting.Preset" + + # Mode + SETTING_MODE = "Alexa.Setting.Mode" + + # Direction + SETTING_DIRECTION = "Alexa.Setting.Direction" + + # Delicates, Delicate + VALUE_DELICATE = "Alexa.Value.Delicate" + + # Quick Wash, Fast Wash, Wash Quickly, Speed Wash + VALUE_QUICKWASH = "Alexa.Value.QuickWash" + + # Maximum, Max + VALUE_MAXIMUM = "Alexa.Value.Maximum" + + # Minimum, Min + VALUE_MINIMUM = "Alexa.Value.Minimum" + + # High + VALUE_HIGH = "Alexa.Value.High" + + # Low + VALUE_LOW = "Alexa.Value.Low" + + # Medium, Mid + VALUE_MEDIUM = "Alexa.Value.Medium" + + +class Unit: + """Alexa Units of Measure. + + https://developer.amazon.com/docs/device-apis/alexa-property-schemas.html#units-of-measure + """ + + ANGLE_DEGREES = "Alexa.Unit.Angle.Degrees" + + ANGLE_RADIANS = "Alexa.Unit.Angle.Radians" + + DISTANCE_FEET = "Alexa.Unit.Distance.Feet" + + DISTANCE_INCHES = "Alexa.Unit.Distance.Inches" + + DISTANCE_KILOMETERS = "Alexa.Unit.Distance.Kilometers" + + DISTANCE_METERS = "Alexa.Unit.Distance.Meters" + + DISTANCE_MILES = "Alexa.Unit.Distance.Miles" + + DISTANCE_YARDS = "Alexa.Unit.Distance.Yards" + + MASS_GRAMS = "Alexa.Unit.Mass.Grams" + + MASS_KILOGRAMS = "Alexa.Unit.Mass.Kilograms" + + PERCENT = "Alexa.Unit.Percent" + + TEMPERATURE_CELSIUS = "Alexa.Unit.Temperature.Celsius" + + TEMPERATURE_DEGREES = "Alexa.Unit.Temperature.Degrees" + + TEMPERATURE_FAHRENHEIT = "Alexa.Unit.Temperature.Fahrenheit" + + TEMPERATURE_KELVIN = "Alexa.Unit.Temperature.Kelvin" + + VOLUME_CUBICFEET = "Alexa.Unit.Volume.CubicFeet" + + VOLUME_CUBICMETERS = "Alexa.Unit.Volume.CubicMeters" + + VOLUME_GALLONS = "Alexa.Unit.Volume.Gallons" + + VOLUME_LITERS = "Alexa.Unit.Volume.Liters" + + VOLUME_PINTS = "Alexa.Unit.Volume.Pints" + + VOLUME_QUARTS = "Alexa.Unit.Volume.Quarts" + + WEIGHT_OUNCES = "Alexa.Unit.Weight.Ounces" + + WEIGHT_POUNDS = "Alexa.Unit.Weight.Pounds" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 0f07e525fa9..dd640aed0a6 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -40,17 +40,20 @@ from .capabilities import ( AlexaEndpointHealth, AlexaInputController, AlexaLockController, + AlexaModeController, AlexaMotionSensor, AlexaPercentageController, AlexaPlaybackController, AlexaPowerController, AlexaPowerLevelController, + AlexaRangeController, AlexaSceneController, AlexaSecurityPanelController, AlexaSpeaker, AlexaStepSpeaker, AlexaTemperatureSensor, AlexaThermostatController, + AlexaToggleController, ) ENTITY_ADAPTERS = Registry() @@ -348,6 +351,19 @@ class FanCapabilities(AlexaEntity): if supported & fan.SUPPORT_SET_SPEED: yield AlexaPercentageController(self.entity) yield AlexaPowerLevelController(self.entity) + yield AlexaRangeController( + self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_SPEED}" + ) + + if supported & fan.SUPPORT_OSCILLATE: + yield AlexaToggleController( + self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}" + ) + if supported & fan.SUPPORT_DIRECTION: + yield AlexaModeController( + self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}" + ) + yield AlexaEndpointHealth(self.hass, self.entity) diff --git a/homeassistant/components/alexa/errors.py b/homeassistant/components/alexa/errors.py index 8e32ed9c7ee..b0600313fc2 100644 --- a/homeassistant/components/alexa/errors.py +++ b/homeassistant/components/alexa/errors.py @@ -97,3 +97,17 @@ class AlexaSecurityPanelAuthorizationRequired(AlexaError): namespace = "Alexa.SecurityPanelController" error_type = "AUTHORIZATION_REQUIRED" + + +class AlexaAlreadyInOperationError(AlexaError): + """Class to represent AlreadyInOperation errors.""" + + namespace = "Alexa" + error_type = "ALREADY_IN_OPERATION" + + +class AlexaInvalidDirectiveError(AlexaError): + """Class to represent InvalidDirective errors.""" + + namespace = "Alexa" + error_type = "INVALID_DIRECTIVE" diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 139defe8313..64feacb92f5 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -36,9 +36,18 @@ import homeassistant.util.dt as dt_util from homeassistant.util.decorator import Registry from homeassistant.util.temperature import convert as convert_temperature -from .const import API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause +from .const import ( + API_TEMP_UNITS, + API_THERMOSTAT_MODES, + API_THERMOSTAT_PRESETS, + Cause, + PERCENTAGE_FAN_MAP, + RANGE_FAN_MAP, + SPEED_FAN_MAP, +) from .entities import async_get_entities from .errors import ( + AlexaInvalidDirectiveError, AlexaInvalidValueError, AlexaSecurityPanelAuthorizationRequired, AlexaSecurityPanelUnauthorizedError, @@ -356,15 +365,7 @@ async def async_api_adjust_percentage(hass, config, directive, context): if entity.domain == fan.DOMAIN: service = fan.SERVICE_SET_SPEED speed = entity.attributes.get(fan.ATTR_SPEED) - - if speed == "off": - current = 0 - elif speed == "low": - current = 33 - elif speed == "medium": - current = 66 - elif speed == "high": - current = 100 + current = PERCENTAGE_FAN_MAP.get(speed, 100) # set percentage percentage = max(0, percentage_delta + current) @@ -827,20 +828,11 @@ async def async_api_adjust_power_level(hass, config, directive, context): percentage_delta = int(directive.payload["powerLevelDelta"]) service = None data = {ATTR_ENTITY_ID: entity.entity_id} - current = 0 if entity.domain == fan.DOMAIN: service = fan.SERVICE_SET_SPEED speed = entity.attributes.get(fan.ATTR_SPEED) - - if speed == "off": - current = 0 - elif speed == "low": - current = 33 - elif speed == "medium": - current = 66 - else: - current = 100 + current = PERCENTAGE_FAN_MAP.get(speed, 100) # set percentage percentage = max(0, percentage_delta + current) @@ -928,3 +920,165 @@ async def async_api_disarm(hass, config, directive, context): ) return response + + +@HANDLERS.register(("Alexa.ModeController", "SetMode")) +async def async_api_set_mode(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + mode = directive.payload["mode"] + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_DIRECTION}": + mode, direction = mode.split(".") + if direction in [fan.DIRECTION_REVERSE, fan.DIRECTION_FORWARD]: + service = fan.SERVICE_SET_DIRECTION + data[fan.ATTR_DIRECTION] = direction + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.ModeController", "AdjustMode")) +async def async_api_adjust_mode(hass, config, directive, context): + """Process a AdjustMode request. + + Requires modeResources to be ordered. + Only modes that are ordered support the adjustMode directive. + """ + entity = directive.entity + instance = directive.instance + domain = entity.domain + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance is None: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + # No modeResources are currently ordered to support this request. + + return directive.response() + + +@HANDLERS.register(("Alexa.ToggleController", "TurnOn")) +async def async_api_toggle_on(hass, config, directive, context): + """Process a toggle on request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + service = fan.SERVICE_OSCILLATE + data[fan.ATTR_OSCILLATING] = True + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.ToggleController", "TurnOff")) +async def async_api_toggle_off(hass, config, directive, context): + """Process a toggle off request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}": + service = fan.SERVICE_OSCILLATE + data[fan.ATTR_OSCILLATING] = False + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.RangeController", "SetRangeValue")) +async def async_api_set_range(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + range_value = int(directive.payload["rangeValue"]) + + if domain != fan.DOMAIN: + msg = "Entity does not support directive" + raise AlexaInvalidDirectiveError(msg) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + service = fan.SERVICE_SET_SPEED + speed = SPEED_FAN_MAP.get(range_value, None) + + if not speed: + msg = "Entity does not support value" + raise AlexaInvalidValueError(msg) + + if speed == fan.SPEED_OFF: + service = fan.SERVICE_TURN_OFF + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() + + +@HANDLERS.register(("Alexa.RangeController", "AdjustRangeValue")) +async def async_api_adjust_range(hass, config, directive, context): + """Process a next request.""" + entity = directive.entity + instance = directive.instance + domain = entity.domain + service = None + data = {ATTR_ENTITY_ID: entity.entity_id} + range_delta = int(directive.payload["rangeValueDelta"]) + + if instance == f"{fan.DOMAIN}.{fan.ATTR_SPEED}": + service = fan.SERVICE_SET_SPEED + + # adjust range + current_range = RANGE_FAN_MAP.get(entity.attributes.get(fan.ATTR_SPEED), 0) + speed = SPEED_FAN_MAP.get(max(0, range_delta + current_range), fan.SPEED_OFF) + + if speed == fan.SPEED_OFF: + service = fan.SERVICE_TURN_OFF + + data[fan.ATTR_SPEED] = speed + + await hass.services.async_call( + domain, service, data, blocking=False, context=context + ) + + return directive.response() diff --git a/homeassistant/components/alexa/messages.py b/homeassistant/components/alexa/messages.py index 3195656ed09..cb78f269f8f 100644 --- a/homeassistant/components/alexa/messages.py +++ b/homeassistant/components/alexa/messages.py @@ -28,7 +28,7 @@ class AlexaDirective: self.payload = self._directive[API_PAYLOAD] self.has_endpoint = API_ENDPOINT in self._directive - self.entity = self.entity_id = self.endpoint = None + self.entity = self.entity_id = self.endpoint = self.instance = None def load_entity(self, hass, config): """Set attributes related to the entity for this request. @@ -38,6 +38,7 @@ class AlexaDirective: - entity - entity_id - endpoint + - instance (when header includes instance property) Behavior when self.has_endpoint is False is undefined. @@ -52,6 +53,8 @@ class AlexaDirective: raise AlexaInvalidEndpointError(_endpoint_id) self.endpoint = ENTITY_ADAPTERS[self.entity.domain](hass, config, self.entity) + if "instance" in self._directive[API_HEADER]: + self.instance = self._directive[API_HEADER]["instance"] def response(self, name="Response", namespace="Alexa", payload=None): """Create an API formatted response. diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 48406a11aef..4fd8bf6f2a9 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -67,13 +67,22 @@ def get_new_request(namespace, name, endpoint=None): async def assert_request_calls_service( - namespace, name, endpoint, service, hass, response_type="Response", payload=None + namespace, + name, + endpoint, + service, + hass, + response_type="Response", + payload=None, + instance=None, ): """Assert an API request calls a hass service.""" context = Context() request = get_new_request(namespace, name, endpoint) if payload: request["directive"]["payload"] = payload + if instance: + request["directive"]["header"]["instance"] = instance domain, service_name = service.split(".") calls = async_mock_service(hass, domain, service_name) diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 280a76dc3f0..be4a2ba4806 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -305,7 +305,7 @@ async def test_report_colored_temp_light_state(hass): async def test_report_fan_speed_state(hass): - """Test PercentageController reports fan speed correctly.""" + """Test PercentageController, PowerLevelController, RangeController reports fan speed correctly.""" hass.states.async_set( "fan.off", "off", @@ -333,15 +333,82 @@ async def test_report_fan_speed_state(hass): properties = await reported_properties(hass, "fan.off") properties.assert_equal("Alexa.PercentageController", "percentage", 0) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 0) + properties.assert_equal("Alexa.RangeController", "rangeValue", 0) properties = await reported_properties(hass, "fan.low_speed") properties.assert_equal("Alexa.PercentageController", "percentage", 33) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 33) + properties.assert_equal("Alexa.RangeController", "rangeValue", 1) properties = await reported_properties(hass, "fan.medium_speed") properties.assert_equal("Alexa.PercentageController", "percentage", 66) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 66) + properties.assert_equal("Alexa.RangeController", "rangeValue", 2) properties = await reported_properties(hass, "fan.high_speed") properties.assert_equal("Alexa.PercentageController", "percentage", 100) + properties.assert_equal("Alexa.PowerLevelController", "powerLevel", 100) + properties.assert_equal("Alexa.RangeController", "rangeValue", 3) + + +async def test_report_fan_oscillating(hass): + """Test ToggleController reports fan oscillating correctly.""" + hass.states.async_set( + "fan.off", + "off", + {"friendly_name": "Off fan", "speed": "off", "supported_features": 3}, + ) + hass.states.async_set( + "fan.low_speed", + "on", + { + "friendly_name": "Low speed fan", + "speed": "low", + "oscillating": True, + "supported_features": 3, + }, + ) + + properties = await reported_properties(hass, "fan.off") + properties.assert_equal("Alexa.ToggleController", "toggleState", "OFF") + + properties = await reported_properties(hass, "fan.low_speed") + properties.assert_equal("Alexa.ToggleController", "toggleState", "ON") + + +async def test_report_fan_direction(hass): + """Test ModeController reports fan direction correctly.""" + hass.states.async_set( + "fan.off", "off", {"friendly_name": "Off fan", "supported_features": 4} + ) + hass.states.async_set( + "fan.reverse", + "on", + { + "friendly_name": "Fan Reverse", + "direction": "reverse", + "supported_features": 4, + }, + ) + hass.states.async_set( + "fan.forward", + "on", + { + "friendly_name": "Fan Forward", + "direction": "forward", + "supported_features": 4, + }, + ) + + properties = await reported_properties(hass, "fan.off") + properties.assert_not_has_property("Alexa.ModeController", "mode") + + properties = await reported_properties(hass, "fan.reverse") + properties.assert_equal("Alexa.ModeController", "mode", "reverse") + + properties = await reported_properties(hass, "fan.forward") + properties.assert_equal("Alexa.ModeController", "mode", "forward") async def test_report_cover_percentage_state(hass): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 186cb850e34..5a39036a30f 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -310,10 +310,14 @@ async def test_fan(hass): assert appliance["endpointId"] == "fan#test_1" assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 1" - assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, "Alexa.PowerController", "Alexa.EndpointHealth" ) + power_capability = get_capability(capabilities, "Alexa.PowerController") + assert "capabilityResources" not in power_capability + assert "configuration" not in power_capability + async def test_variable_fan(hass): """Test fan discovery. @@ -336,14 +340,33 @@ async def test_variable_fan(hass): assert appliance["displayCategories"][0] == "FAN" assert appliance["friendlyName"] == "Test fan 2" - assert_endpoint_capabilities( + capabilities = assert_endpoint_capabilities( appliance, "Alexa.PercentageController", "Alexa.PowerController", "Alexa.PowerLevelController", + "Alexa.RangeController", "Alexa.EndpointHealth", ) + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "fan.speed" + + properties = range_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "rangeValue"} in properties["supported"] + + capability_resources = range_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.FanSpeed"}, + } in capability_resources["friendlyNames"] + + configuration = range_capability["configuration"] + assert configuration is not None + call, _ = await assert_request_calls_service( "Alexa.PercentageController", "SetPercentage", @@ -377,7 +400,7 @@ async def test_variable_fan(hass): await assert_percentage_changes( hass, - [("high", "-5"), ("high", "5"), ("low", "-80")], + [("high", "-5"), ("medium", "-50"), ("low", "-80")], "Alexa.PowerLevelController", "AdjustPowerLevel", "fan#test_2", @@ -387,6 +410,251 @@ async def test_variable_fan(hass): ) +async def test_oscillating_fan(hass): + """Test oscillating fan discovery.""" + device = ( + "fan.test_3", + "off", + {"friendly_name": "Test fan 3", "supported_features": 3}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "fan#test_3" + assert appliance["displayCategories"][0] == "FAN" + assert appliance["friendlyName"] == "Test fan 3" + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PercentageController", + "Alexa.PowerController", + "Alexa.PowerLevelController", + "Alexa.RangeController", + "Alexa.ToggleController", + "Alexa.EndpointHealth", + ) + + toggle_capability = get_capability(capabilities, "Alexa.ToggleController") + assert toggle_capability is not None + assert toggle_capability["instance"] == "fan.oscillating" + + properties = toggle_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "toggleState"} in properties["supported"] + + capability_resources = toggle_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Oscillate"}, + } in capability_resources["friendlyNames"] + + call, _ = await assert_request_calls_service( + "Alexa.ToggleController", + "TurnOn", + "fan#test_3", + "fan.oscillate", + hass, + payload={}, + instance="fan.oscillating", + ) + assert call.data["oscillating"] + + call, _ = await assert_request_calls_service( + "Alexa.ToggleController", + "TurnOff", + "fan#test_3", + "fan.oscillate", + hass, + payload={}, + instance="fan.oscillating", + ) + assert not call.data["oscillating"] + + +async def test_direction_fan(hass): + """Test direction fan discovery.""" + device = ( + "fan.test_4", + "on", + { + "friendly_name": "Test fan 4", + "supported_features": 5, + "direction": "forward", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "fan#test_4" + assert appliance["displayCategories"][0] == "FAN" + assert appliance["friendlyName"] == "Test fan 4" + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PercentageController", + "Alexa.PowerController", + "Alexa.PowerLevelController", + "Alexa.RangeController", + "Alexa.ModeController", + "Alexa.EndpointHealth", + ) + + mode_capability = get_capability(capabilities, "Alexa.ModeController") + assert mode_capability is not None + assert mode_capability["instance"] == "fan.direction" + + properties = mode_capability["properties"] + assert properties["nonControllable"] is False + assert {"name": "mode"} in properties["supported"] + + capability_resources = mode_capability["capabilityResources"] + assert capability_resources is not None + assert { + "@type": "asset", + "value": {"assetId": "Alexa.Setting.Direction"}, + } in capability_resources["friendlyNames"] + + configuration = mode_capability["configuration"] + assert configuration is not None + assert configuration["ordered"] is False + + supported_modes = configuration["supportedModes"] + assert supported_modes is not None + assert { + "value": "direction.forward", + "modeResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "forward", "locale": "en-US"}} + ] + }, + } in supported_modes + assert { + "value": "direction.reverse", + "modeResources": { + "friendlyNames": [ + {"@type": "text", "value": {"text": "reverse", "locale": "en-US"}} + ] + }, + } in supported_modes + + call, _ = await assert_request_calls_service( + "Alexa.ModeController", + "SetMode", + "fan#test_4", + "fan.set_direction", + hass, + payload={"mode": "direction.reverse"}, + instance="fan.direction", + ) + assert call.data["direction"] == "reverse" + + # Test for AdjustMode instance=None Error coverage + with pytest.raises(AssertionError): + call, _ = await assert_request_calls_service( + "Alexa.ModeController", + "AdjustMode", + "fan#test_4", + "fan.set_direction", + hass, + payload={}, + instance=None, + ) + assert call.data + + +async def test_fan_range(hass): + """Test fan discovery with range controller. + + This one has variable speed. + """ + device = ( + "fan.test_5", + "off", + { + "friendly_name": "Test fan 5", + "supported_features": 1, + "speed_list": ["low", "medium", "high"], + "speed": "medium", + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "fan#test_5" + assert appliance["displayCategories"][0] == "FAN" + assert appliance["friendlyName"] == "Test fan 5" + + capabilities = assert_endpoint_capabilities( + appliance, + "Alexa.PercentageController", + "Alexa.PowerController", + "Alexa.PowerLevelController", + "Alexa.RangeController", + "Alexa.EndpointHealth", + ) + + range_capability = get_capability(capabilities, "Alexa.RangeController") + assert range_capability is not None + assert range_capability["instance"] == "fan.speed" + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "fan#test_5", + "fan.set_speed", + hass, + payload={"rangeValue": "1"}, + instance="fan.speed", + ) + assert call.data["speed"] == "low" + + await assert_range_changes( + hass, + [("low", "-1"), ("high", "1"), ("medium", "0")], + "Alexa.RangeController", + "AdjustRangeValue", + "fan#test_5", + False, + "fan.set_speed", + "speed", + instance="fan.speed", + ) + + +async def test_fan_range_off(hass): + """Test fan range controller 0 turns_off fan.""" + device = ( + "fan.test_6", + "off", + { + "friendly_name": "Test fan 6", + "supported_features": 1, + "speed_list": ["low", "medium", "high"], + "speed": "high", + }, + ) + await discovery_test(device, hass) + + call, _ = await assert_request_calls_service( + "Alexa.RangeController", + "SetRangeValue", + "fan#test_6", + "fan.turn_off", + hass, + payload={"rangeValue": "0"}, + instance="fan.speed", + ) + assert call.data["speed"] == "off" + + await assert_range_changes( + hass, + [("off", "-3")], + "Alexa.RangeController", + "AdjustRangeValue", + "fan#test_6", + False, + "fan.turn_off", + "speed", + instance="fan.speed", + ) + + async def test_lock(hass): """Test lock discovery.""" device = ("lock.test", "off", {"friendly_name": "Test lock"}) @@ -729,6 +997,33 @@ async def assert_percentage_changes( assert call.data[changed_parameter] == result_volume +async def assert_range_changes( + hass, + adjustments, + namespace, + name, + endpoint, + delta_default, + service, + changed_parameter, + instance, +): + """Assert an API request making range changes works. + + AdjustRangeValue are examples of such requests. + """ + for result_range, adjustment in adjustments: + payload = { + "rangeValueDelta": adjustment, + "rangeValueDeltaDefault": delta_default, + } + + call, _ = await assert_request_calls_service( + namespace, name, endpoint, service, hass, payload=payload, instance=instance + ) + assert call.data[changed_parameter] == result_range + + async def test_temp_sensor(hass): """Test temperature sensor discovery.""" device = ( @@ -1438,3 +1733,41 @@ async def test_alarm_control_panel_code_arm_required(hass): {"friendly_name": "Test Alarm Control Panel 3", "code_arm_required": True}, ) await discovery_test(device, hass, expected_endpoints=0) + + +async def test_range_unsupported_domain(hass): + """Test rangeController with unsupported domain.""" + device = ("switch.test", "on", {"friendly_name": "Test switch"}) + await discovery_test(device, hass) + + context = Context() + request = get_new_request("Alexa.RangeController", "SetRangeValue", "switch#test") + request["directive"]["payload"] = {"rangeValue": "1"} + request["directive"]["header"]["instance"] = "switch.speed" + + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" + + +async def test_mode_unsupported_domain(hass): + """Test modeController with unsupported domain.""" + device = ("switch.test", "on", {"friendly_name": "Test switch"}) + await discovery_test(device, hass) + + context = Context() + request = get_new_request("Alexa.ModeController", "SetMode", "switch#test") + request["directive"]["payload"] = {"mode": "testMode"} + request["directive"]["header"]["instance"] = "switch.direction" + + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request, context) + + assert "event" in msg + msg = msg["event"] + assert msg["header"]["name"] == "ErrorResponse" + assert msg["header"]["namespace"] == "Alexa" + assert msg["payload"]["type"] == "INVALID_DIRECTIVE" diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index c05eed2a89b..310180ef5d0 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -37,6 +37,54 @@ async def test_report_state(hass, aioclient_mock): assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_contact" +async def test_report_state_instance(hass, aioclient_mock): + """Test proactive state reports with instance.""" + aioclient_mock.post(TEST_URL, text="", status=202) + + hass.states.async_set( + "fan.test_fan", + "off", + { + "friendly_name": "Test fan", + "supported_features": 3, + "speed": "off", + "oscillating": False, + }, + ) + + await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) + + hass.states.async_set( + "fan.test_fan", + "on", + { + "friendly_name": "Test fan", + "supported_features": 3, + "speed": "high", + "oscillating": True, + }, + ) + + # To trigger event listener + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + assert call_json["event"]["header"]["namespace"] == "Alexa" + assert call_json["event"]["header"]["name"] == "ChangeReport" + + change_reports = call_json["event"]["payload"]["change"]["properties"] + for report in change_reports: + if report["name"] == "toggleState": + assert report["value"] == "ON" + assert report["instance"] == "fan.oscillating" + assert report["namespace"] == "Alexa.ToggleController" + + assert call_json["event"]["endpoint"]["endpointId"] == "fan#test_fan" + + async def test_send_add_or_update_message(hass, aioclient_mock): """Test sending an AddOrUpdateReport message.""" aioclient_mock.post(TEST_URL, text="") From e3f0c904b05a4a8e90ee8b7ff7b4a702f283df3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20H=C3=B6rsken?= Date: Wed, 23 Oct 2019 07:06:21 +0200 Subject: [PATCH 582/639] Add option to specify mDNS advertised IP address for HomeKit Bridge (#26791) * Add options to specify advertised IP and MAC for HomeKit Bridge This makes use of HAP-python's new feature in version 2.6.0 that allows to specify the mDNS advertised IP and MAC address. This is a requirement for the following use cases: - Running Home Assistant behind a NAT, e.g. inside Docker. - Running it on a system with multiple interfaces there the default IP address, DNS entry and hostname diverge. The forwarding of the required mDNS packets can be done with an avahi-daemon based gateway, e.g. by using enable-reflector=yes. Specifying the MAC address makes it possible to identify an accessory in case HA is run inside a ephemeral docker container. Whitespace changes were performed due to black and flake8. * Update tests for HomeKit Bridge due to IP and MAC advertising Whitespace changes were performed due to black and flake8. * Remove the possibility to set the MAC address of the HomeKit Bridge Since the MAC address is a random device ID, there is no need for the user to be able to set a custom MAC address value for it. Whitespace changes were performed due to black and flake8. --- homeassistant/components/homekit/__init__.py | 31 +++++++++++++++-- homeassistant/components/homekit/const.py | 1 + tests/components/homekit/test_homekit.py | 35 +++++++++++++++++--- 3 files changed, 60 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index d8aafb8e238..4c300e0a934 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -31,6 +31,7 @@ from homeassistant.util.decorator import Registry from .const import ( BRIDGE_NAME, + CONF_ADVERTISE_IP, CONF_AUTO_START, CONF_ENTITY_CONFIG, CONF_FEATURE_LIST, @@ -89,6 +90,9 @@ CONFIG_SCHEMA = vol.Schema( ), vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_IP_ADDRESS): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_ADVERTISE_IP): vol.All( + ipaddress.ip_address, cv.string + ), vol.Optional(CONF_AUTO_START, default=DEFAULT_AUTO_START): cv.boolean, vol.Optional(CONF_SAFE_MODE, default=DEFAULT_SAFE_MODE): cv.boolean, vol.Optional(CONF_FILTER, default={}): FILTER_SCHEMA, @@ -112,13 +116,21 @@ async def async_setup(hass, config): name = conf[CONF_NAME] port = conf[CONF_PORT] ip_address = conf.get(CONF_IP_ADDRESS) + advertise_ip = conf.get(CONF_ADVERTISE_IP) auto_start = conf[CONF_AUTO_START] safe_mode = conf[CONF_SAFE_MODE] entity_filter = conf[CONF_FILTER] entity_config = conf[CONF_ENTITY_CONFIG] homekit = HomeKit( - hass, name, port, ip_address, entity_filter, entity_config, safe_mode + hass, + name, + port, + ip_address, + entity_filter, + entity_config, + safe_mode, + advertise_ip, ) await hass.async_add_executor_job(homekit.setup) @@ -265,7 +277,15 @@ class HomeKit: """Class to handle all actions between HomeKit and Home Assistant.""" def __init__( - self, hass, name, port, ip_address, entity_filter, entity_config, safe_mode + self, + hass, + name, + port, + ip_address, + entity_filter, + entity_config, + safe_mode, + advertise_ip=None, ): """Initialize a HomeKit object.""" self.hass = hass @@ -275,6 +295,7 @@ class HomeKit: self._filter = entity_filter self._config = entity_config self._safe_mode = safe_mode + self._advertise_ip = advertise_ip self.status = STATUS_READY self.bridge = None @@ -289,7 +310,11 @@ class HomeKit: ip_addr = self._ip_address or get_local_ip() path = self.hass.config.path(HOMEKIT_FILE) self.driver = HomeDriver( - self.hass, address=ip_addr, port=self._port, persist_file=path + self.hass, + address=ip_addr, + port=self._port, + persist_file=path, + advertised_address=self._advertise_ip, ) self.bridge = HomeBridge(self.hass, self.driver, self._name) if self._safe_mode: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index d225225237f..82ec296da4b 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -10,6 +10,7 @@ ATTR_DISPLAY_NAME = "display_name" ATTR_VALUE = "value" # #### Config #### +CONF_ADVERTISE_IP = "advertise_ip" CONF_AUTO_START = "auto_start" CONF_ENTITY_CONFIG = "entity_config" CONF_FEATURE = "feature" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 61893af7008..97838eaa852 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -69,7 +69,7 @@ async def test_setup_min(hass): assert await setup.async_setup_component(hass, DOMAIN, {DOMAIN: {}}) mock_homekit.assert_any_call( - hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE + hass, BRIDGE_NAME, DEFAULT_PORT, None, ANY, {}, DEFAULT_SAFE_MODE, None ) assert mock_homekit().setup.called is True @@ -98,7 +98,7 @@ async def test_setup_auto_start_disabled(hass): assert await setup.async_setup_component(hass, DOMAIN, config) mock_homekit.assert_any_call( - hass, "Test Name", 11111, "172.0.0.0", ANY, {}, DEFAULT_SAFE_MODE + hass, "Test Name", 11111, "172.0.0.0", ANY, {}, DEFAULT_SAFE_MODE, None ) assert mock_homekit().setup.called is True @@ -136,7 +136,11 @@ async def test_homekit_setup(hass, hk_driver): path = hass.config.path(HOMEKIT_FILE) assert isinstance(homekit.bridge, HomeBridge) mock_driver.assert_called_with( - hass, address=IP_ADDRESS, port=DEFAULT_PORT, persist_file=path + hass, + address=IP_ADDRESS, + port=DEFAULT_PORT, + persist_file=path, + advertised_address=None, ) assert homekit.driver.safe_mode is False @@ -153,7 +157,30 @@ async def test_homekit_setup_ip_address(hass, hk_driver): ) as mock_driver: await hass.async_add_job(homekit.setup) mock_driver.assert_called_with( - hass, address="172.0.0.0", port=DEFAULT_PORT, persist_file=ANY + hass, + address="172.0.0.0", + port=DEFAULT_PORT, + persist_file=ANY, + advertised_address=None, + ) + + +async def test_homekit_setup_advertise_ip(hass, hk_driver): + """Test setup with given IP address to advertise.""" + homekit = HomeKit( + hass, BRIDGE_NAME, DEFAULT_PORT, "0.0.0.0", {}, {}, None, "192.168.1.100" + ) + + with patch( + PATH_HOMEKIT + ".accessories.HomeDriver", return_value=hk_driver + ) as mock_driver: + await hass.async_add_job(homekit.setup) + mock_driver.assert_called_with( + hass, + address="0.0.0.0", + port=DEFAULT_PORT, + persist_file=ANY, + advertised_address="192.168.1.100", ) From 4cb984842aba70520c4522be3d047286234b7f1b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 23 Oct 2019 01:26:29 -0400 Subject: [PATCH 583/639] Support custom source type for MQTT device tracker (#27838) * support custom source type for MQTT device tracker * fix typo * add abbreviation --- .../components/mqtt/abbreviations.py | 1 + .../components/mqtt/device_tracker.py | 13 +++++--- tests/components/mqtt/test_device_tracker.py | 31 ++++++++++++++++++- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index 5a5ed4555db..5e995494a64 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -130,6 +130,7 @@ ABBREVIATIONS = { "spd_stat_t": "speed_state_topic", "spd_val_tpl": "speed_value_template", "spds": "speeds", + "src_type": "source_type", "stat_clsd": "state_closed", "stat_off": "state_off", "stat_on": "state_on", diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index c9cce3ebeda..d25d7ce21d3 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import mqtt -from homeassistant.components.device_tracker import PLATFORM_SCHEMA +from homeassistant.components.device_tracker import PLATFORM_SCHEMA, SOURCE_TYPES from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_DEVICES, STATE_NOT_HOME, STATE_HOME @@ -15,12 +15,14 @@ _LOGGER = logging.getLogger(__name__) CONF_PAYLOAD_HOME = "payload_home" CONF_PAYLOAD_NOT_HOME = "payload_not_home" +CONF_SOURCE_TYPE = "source_type" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(mqtt.SCHEMA_BASE).extend( { vol.Required(CONF_DEVICES): {cv.string: mqtt.valid_subscribe_topic}, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, + vol.Optional(CONF_SOURCE_TYPE): vol.In(SOURCE_TYPES), } ) @@ -31,6 +33,7 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): qos = config[CONF_QOS] payload_home = config[CONF_PAYLOAD_HOME] payload_not_home = config[CONF_PAYLOAD_NOT_HOME] + source_type = config.get(CONF_SOURCE_TYPE) for dev_id, topic in devices.items(): @@ -44,9 +47,11 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): else: location_name = msg.payload - hass.async_create_task( - async_see(dev_id=dev_id, location_name=location_name) - ) + see_args = {"dev_id": dev_id, "location_name": location_name} + if source_type: + see_args["source_type"] = source_type + + hass.async_create_task(async_see(**see_args)) await mqtt.async_subscribe(hass, topic, async_message_received, qos) diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 14180d2dcf9..71348fcf5cb 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -3,7 +3,10 @@ from asynctest import patch import pytest from homeassistant.components import device_tracker -from homeassistant.components.device_tracker.const import ENTITY_ID_FORMAT +from homeassistant.components.device_tracker.const import ( + ENTITY_ID_FORMAT, + SOURCE_TYPE_BLUETOOTH, +) from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component @@ -218,3 +221,29 @@ async def test_not_matching_custom_payload_for_home_and_not_home( await hass.async_block_till_done() assert hass.states.get(entity_id).state != STATE_HOME assert hass.states.get(entity_id).state != STATE_NOT_HOME + + +async def test_matching_source_type(hass, mock_device_tracker_conf): + """Test setting source type.""" + dev_id = "paulus" + entity_id = ENTITY_ID_FORMAT.format(dev_id) + topic = "/location/paulus" + source_type = SOURCE_TYPE_BLUETOOTH + location = "work" + + hass.config.components = set(["mqtt", "zone"]) + assert await async_setup_component( + hass, + device_tracker.DOMAIN, + { + device_tracker.DOMAIN: { + CONF_PLATFORM: "mqtt", + "devices": {dev_id: topic}, + "source_type": source_type, + } + }, + ) + + async_fire_mqtt_message(hass, topic, location) + await hass.async_block_till_done() + assert hass.states.get(entity_id).attributes["source_type"] == SOURCE_TYPE_BLUETOOTH From 09d8a4204ab907d816b84e54d8f822e90c3e9724 Mon Sep 17 00:00:00 2001 From: Fredrik Erlandsson Date: Wed, 23 Oct 2019 07:43:28 +0200 Subject: [PATCH 584/639] Add support for resource_template for rest sensor (#27869) * add support for resource_template * fix tests * updated tests and xor(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE) --- homeassistant/components/rest/sensor.py | 23 +++++++++++++- homeassistant/const.py | 1 + tests/components/rest/test_sensor.py | 42 +++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 01d974e7006..41adb855903 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PAYLOAD, CONF_RESOURCE, + CONF_RESOURCE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME, CONF_TIMEOUT, @@ -42,7 +43,8 @@ METHODS = ["POST", "GET"] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_RESOURCE): cv.url, + vol.Exclusive(CONF_RESOURCE, CONF_RESOURCE): cv.url, + vol.Exclusive(CONF_RESOURCE_TEMPLATE, CONF_RESOURCE): cv.template, vol.Optional(CONF_AUTHENTICATION): vol.In( [HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION] ), @@ -62,11 +64,16 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +PLATFORM_SCHEMA = vol.All( + cv.has_at_least_one_key(CONF_RESOURCE, CONF_RESOURCE_TEMPLATE), PLATFORM_SCHEMA +) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the RESTful sensor.""" name = config.get(CONF_NAME) resource = config.get(CONF_RESOURCE) + resource_template = config.get(CONF_RESOURCE_TEMPLATE) method = config.get(CONF_METHOD) payload = config.get(CONF_PAYLOAD) verify_ssl = config.get(CONF_VERIFY_SSL) @@ -83,6 +90,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if value_template is not None: value_template.hass = hass + if resource_template is not None: + resource_template.hass = hass + resource = resource_template.render() + if username and password: if config.get(CONF_AUTHENTICATION) == HTTP_DIGEST_AUTHENTICATION: auth = HTTPDigestAuth(username, password) @@ -108,6 +119,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): value_template, json_attrs, force_update, + resource_template, ) ], True, @@ -127,6 +139,7 @@ class RestSensor(Entity): value_template, json_attrs, force_update, + resource_template, ): """Initialize the REST sensor.""" self._hass = hass @@ -139,6 +152,7 @@ class RestSensor(Entity): self._json_attrs = json_attrs self._attributes = None self._force_update = force_update + self._resource_template = resource_template @property def name(self): @@ -172,6 +186,9 @@ class RestSensor(Entity): def update(self): """Get the latest data from REST API and update the state.""" + if self._resource_template is not None: + self.rest.set_url(self._resource_template.render()) + self.rest.update() value = self.rest.data @@ -217,6 +234,10 @@ class RestData: self._timeout = timeout self.data = None + def set_url(self, url): + """Set url.""" + self._request.prepare_url(url, None) + def update(self): """Get the latest data from REST service with provided method.""" _LOGGER.debug("Updating from %s", self._request.url) diff --git a/homeassistant/const.py b/homeassistant/const.py index 592f6b60bc6..cac0386b812 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -124,6 +124,7 @@ CONF_RECIPIENT = "recipient" CONF_REGION = "region" CONF_RESOURCE = "resource" CONF_RESOURCES = "resources" +CONF_RESOURCE_TEMPLATE = "resource_template" CONF_RGB = "rgb" CONF_ROOM = "room" CONF_SCAN_INTERVAL = "scan_interval" diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index d117678ccc7..50acb053347 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -76,6 +76,40 @@ class TestRestSensorSetup(unittest.TestCase): ) assert 2 == mock_req.call_count + @requests_mock.Mocker() + def test_setup_minimum_resource_template(self, mock_req): + """Test setup with minimum configuration (resource_template).""" + mock_req.get("http://localhost", status_code=200) + with assert_setup_component(1, "sensor"): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "rest", + "resource_template": "http://localhost", + } + }, + ) + assert mock_req.call_count == 2 + + @requests_mock.Mocker() + def test_setup_duplicate_resource(self, mock_req): + """Test setup with duplicate resources.""" + mock_req.get("http://localhost", status_code=200) + with assert_setup_component(0, "sensor"): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "rest", + "resource": "http://localhost", + "resource_template": "http://localhost", + } + }, + ) + @requests_mock.Mocker() def test_setup_get(self, mock_req): """Test setup with valid configuration.""" @@ -152,6 +186,7 @@ class TestRestSensor(unittest.TestCase): self.value_template = template("{{ value_json.key }}") self.value_template.hass = self.hass self.force_update = False + self.resource_template = None self.sensor = rest.RestSensor( self.hass, @@ -162,6 +197,7 @@ class TestRestSensor(unittest.TestCase): self.value_template, [], self.force_update, + self.resource_template, ) def tearDown(self): @@ -222,6 +258,7 @@ class TestRestSensor(unittest.TestCase): None, [], self.force_update, + self.resource_template, ) self.sensor.update() assert "plain_state" == self.sensor.state @@ -242,6 +279,7 @@ class TestRestSensor(unittest.TestCase): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert "some_json_value" == self.sensor.device_state_attributes["key"] @@ -261,6 +299,7 @@ class TestRestSensor(unittest.TestCase): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -282,6 +321,7 @@ class TestRestSensor(unittest.TestCase): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -303,6 +343,7 @@ class TestRestSensor(unittest.TestCase): None, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() assert {} == self.sensor.device_state_attributes @@ -326,6 +367,7 @@ class TestRestSensor(unittest.TestCase): self.value_template, ["key"], self.force_update, + self.resource_template, ) self.sensor.update() From ab22c617646387af18f088aaf26aba348d606bce Mon Sep 17 00:00:00 2001 From: Matt Kasa Date: Tue, 22 Oct 2019 22:46:18 -0700 Subject: [PATCH 585/639] Support SmartStrip type devices (HS300, HS107) in tplink component (#26220) * Add support for SmartStrip type devices (HS300, HS107) to tplink component * Incorporate feedback from @MartinHjelmare using changes suggested by @shbatm - Setting `_state` now uses a list comprehension - `_alias` will use aliases from the Kasa app - `_device_id` will be set to `_mac` for single plugs to retain backwards compatibility --- homeassistant/components/tplink/__init__.py | 4 ++++ homeassistant/components/tplink/common.py | 20 +++++++++++++++--- homeassistant/components/tplink/switch.py | 23 ++++++++++++++++++--- 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/tplink/__init__.py b/homeassistant/components/tplink/__init__.py index 85258b5e94e..7aa261564f3 100644 --- a/homeassistant/components/tplink/__init__.py +++ b/homeassistant/components/tplink/__init__.py @@ -14,6 +14,7 @@ from .common import ( CONF_DISCOVERY, CONF_LIGHT, CONF_SWITCH, + CONF_STRIP, SmartDevices, async_discover_devices, get_static_devices, @@ -36,6 +37,9 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_SWITCH, default=[]): vol.All( cv.ensure_list, [TPLINK_HOST_SCHEMA] ), + vol.Optional(CONF_STRIP, default=[]): vol.All( + cv.ensure_list, [TPLINK_HOST_SCHEMA] + ), vol.Optional(CONF_DIMMER, default=[]): vol.All( cv.ensure_list, [TPLINK_HOST_SCHEMA] ), diff --git a/homeassistant/components/tplink/common.py b/homeassistant/components/tplink/common.py index 75636c8dc28..548edc6822c 100644 --- a/homeassistant/components/tplink/common.py +++ b/homeassistant/components/tplink/common.py @@ -4,7 +4,14 @@ from datetime import timedelta import logging from typing import Any, Callable, List -from pyHS100 import Discover, SmartBulb, SmartDevice, SmartDeviceException, SmartPlug +from pyHS100 import ( + Discover, + SmartBulb, + SmartDevice, + SmartDeviceException, + SmartPlug, + SmartStrip, +) from homeassistant.helpers.typing import HomeAssistantType @@ -15,6 +22,7 @@ ATTR_CONFIG = "config" CONF_DIMMER = "dimmer" CONF_DISCOVERY = "discovery" CONF_LIGHT = "light" +CONF_STRIP = "strip" CONF_SWITCH = "switch" @@ -74,7 +82,10 @@ async def async_discover_devices( if existing_devices.has_device_with_host(dev.host): continue - if isinstance(dev, SmartPlug): + if isinstance(dev, SmartStrip): + for plug in dev.plugs.values(): + switches.append(plug) + elif isinstance(dev, SmartPlug): try: if dev.is_dimmable: # Dimmers act as lights lights.append(dev) @@ -99,7 +110,7 @@ def get_static_devices(config_data) -> SmartDevices: lights = [] switches = [] - for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_DIMMER]: + for type_ in [CONF_LIGHT, CONF_SWITCH, CONF_STRIP, CONF_DIMMER]: for entry in config_data[type_]: host = entry["host"] @@ -107,6 +118,9 @@ def get_static_devices(config_data) -> SmartDevices: lights.append(SmartBulb(host)) elif type_ == CONF_SWITCH: switches.append(SmartPlug(host)) + elif type_ == CONF_STRIP: + for plug in SmartStrip(host).plugs.values(): + switches.append(plug) # Dimmers need to be defined as smart plugs to work correctly. elif type_ == CONF_DIMMER: lights.append(SmartPlug(host)) diff --git a/homeassistant/components/tplink/switch.py b/homeassistant/components/tplink/switch.py index ebeac984515..791d358c509 100644 --- a/homeassistant/components/tplink/switch.py +++ b/homeassistant/components/tplink/switch.py @@ -69,11 +69,12 @@ class SmartPlugSwitch(SwitchDevice): self._mac = None self._alias = None self._model = None + self._device_id = None @property def unique_id(self): """Return a unique ID.""" - return self._mac + return self._device_id @property def name(self): @@ -120,10 +121,26 @@ class SmartPlugSwitch(SwitchDevice): if not self._sysinfo: self._sysinfo = self.smartplug.sys_info self._mac = self.smartplug.mac - self._alias = self.smartplug.alias self._model = self.smartplug.model + if self.smartplug.context is None: + self._alias = self.smartplug.alias + self._device_id = self._mac + else: + self._alias = [ + child + for child in self.smartplug.sys_info["children"] + if child["id"] == self.smartplug.context + ][0]["alias"] + self._device_id = self.smartplug.context - self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON + if self.smartplug.context is None: + self._state = self.smartplug.state == self.smartplug.SWITCH_STATE_ON + else: + self._state = [ + child + for child in self.smartplug.sys_info["children"] + if child["id"] == self.smartplug.context + ][0]["state"] == 1 if self.smartplug.has_emeter: emeter_readings = self.smartplug.get_emeter_realtime() From 25fd930d67ec79e6e9cb09d50451e6677cce24be Mon Sep 17 00:00:00 2001 From: SteveDinn Date: Wed, 23 Oct 2019 02:51:29 -0300 Subject: [PATCH 586/639] Add template filters to convert objects to and from JSON strings (#27909) * Added filters to convert objects to and from JSON strings. * Added extra spacing. * Removed try/catch to get native exceptions * Added tests. --- homeassistant/helpers/template.py | 12 ++++++++++++ tests/helpers/test_template.py | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 9af1998e894..1d9ca691451 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -884,6 +884,16 @@ def ordinal(value): ) +def from_json(value): + """Convert a JSON string to an object.""" + return json.loads(value) + + +def to_json(value): + """Convert an object to a JSON string.""" + return json.dumps(value) + + @contextfilter def random_every_time(context, values): """Choose a random value. @@ -916,6 +926,8 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): self.filters["timestamp_custom"] = timestamp_custom self.filters["timestamp_local"] = timestamp_local self.filters["timestamp_utc"] = timestamp_utc + self.filters["to_json"] = to_json + self.filters["from_json"] = from_json self.filters["is_defined"] = fail_when_undefined self.filters["max"] = max self.filters["min"] = min diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index cc1f7707df6..b69fdb17e35 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -501,6 +501,30 @@ def test_timestamp_local(hass): ) +def test_to_json(hass): + """Test the object to JSON string filter.""" + + # Note that we're not testing the actual json.loads and json.dumps methods, + # only the filters, so we don't need to be exhaustive with our sample JSON. + expected_result = '{"Foo": "Bar"}' + actual_result = template.Template( + "{{ {'Foo': 'Bar'} | to_json }}", hass + ).async_render() + assert actual_result == expected_result + + +def test_from_json(hass): + """Test the JSON string to object filter.""" + + # Note that we're not testing the actual json.loads and json.dumps methods, + # only the filters, so we don't need to be exhaustive with our sample JSON. + expected_result = "Bar" + actual_result = template.Template( + '{{ (\'{"Foo": "Bar"}\' | from_json).Foo }}', hass + ).async_render() + assert actual_result == expected_result + + def test_min(hass): """Test the min filter.""" assert template.Template("{{ [1, 2, 3] | min }}", hass).async_render() == "1" From 8bdec13baded6c8f671b9749a1ac5b1f90a16f01 Mon Sep 17 00:00:00 2001 From: javicalle <31999997+javicalle@users.noreply.github.com> Date: Wed, 23 Oct 2019 07:58:57 +0200 Subject: [PATCH 587/639] Move imports in hue component (#28121) --- homeassistant/components/hue/__init__.py | 8 ++-- homeassistant/components/hue/binary_sensor.py | 16 +++++++- homeassistant/components/hue/helpers.py | 2 +- homeassistant/components/hue/light.py | 7 ++-- homeassistant/components/hue/sensor.py | 26 ++++++++++--- homeassistant/components/hue/sensor_base.py | 37 ++----------------- 6 files changed, 48 insertions(+), 48 deletions(-) diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 064e18e7a81..027ec205195 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -8,11 +8,11 @@ from homeassistant import config_entries from homeassistant.const import CONF_FILENAME, CONF_HOST from homeassistant.helpers import config_validation as cv, device_registry as dr -from .const import DOMAIN from .bridge import HueBridge - -# Loading the config flow file will register the flow -from .config_flow import configured_hosts +from .config_flow import ( + configured_hosts, +) # Loading the config flow file will register the flow +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index 9c84cb5d61c..e4b7dd85e37 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -1,19 +1,31 @@ """Hue binary sensor entities.""" + +from aiohue.sensors import TYPE_ZLL_PRESENCE + from homeassistant.components.binary_sensor import ( - BinarySensorDevice, DEVICE_CLASS_MOTION, + BinarySensorDevice, ) from homeassistant.components.hue.sensor_base import ( GenericZLLSensor, + SensorManager, async_setup_entry as shared_async_setup_entry, ) - PRESENCE_NAME_FORMAT = "{} motion" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer binary sensor setup to the shared sensor module.""" + SensorManager.sensor_config_map.update( + { + TYPE_ZLL_PRESENCE: { + "binary": True, + "name_format": PRESENCE_NAME_FORMAT, + "class": HuePresence, + } + } + ) await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=True) diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index 388046bb8cb..971509ab647 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -1,6 +1,6 @@ """Helper functions for Philips Hue.""" -from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.helpers.entity_registry import async_get_registry as get_ent_reg from .const import DOMAIN diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index dcae1cf4f5d..041eb76c1d3 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -2,8 +2,8 @@ import asyncio from datetime import timedelta import logging -from time import monotonic import random +from time import monotonic import aiohue import async_timeout @@ -14,21 +14,22 @@ from homeassistant.components.light import ( ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, - ATTR_TRANSITION, ATTR_HS_COLOR, + ATTR_TRANSITION, EFFECT_COLORLOOP, EFFECT_RANDOM, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, - SUPPORT_COLOR, SUPPORT_TRANSITION, Light, ) from homeassistant.util import color + from .helpers import remove_devices SCAN_INTERVAL = timedelta(seconds=5) diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 457ed761202..f2e02d49ecf 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -1,15 +1,17 @@ """Hue sensor entities.""" +from aiohue.sensors import TYPE_ZLL_LIGHTLEVEL, TYPE_ZLL_TEMPERATURE + +from homeassistant.components.hue.sensor_base import ( + GenericZLLSensor, + SensorManager, + async_setup_entry as shared_async_setup_entry, +) from homeassistant.const import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, ) from homeassistant.helpers.entity import Entity -from homeassistant.components.hue.sensor_base import ( - GenericZLLSensor, - async_setup_entry as shared_async_setup_entry, -) - LIGHT_LEVEL_NAME_FORMAT = "{} light level" TEMPERATURE_NAME_FORMAT = "{} temperature" @@ -17,6 +19,20 @@ TEMPERATURE_NAME_FORMAT = "{} temperature" async def async_setup_entry(hass, config_entry, async_add_entities): """Defer sensor setup to the shared sensor module.""" + SensorManager.sensor_config_map.update( + { + TYPE_ZLL_LIGHTLEVEL: { + "binary": False, + "name_format": LIGHT_LEVEL_NAME_FORMAT, + "class": HueLightLevel, + }, + TYPE_ZLL_TEMPERATURE: { + "binary": False, + "name_format": TEMPERATURE_NAME_FORMAT, + "class": HueTemperature, + }, + } + ) await shared_async_setup_entry(hass, config_entry, async_add_entities, binary=False) diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 3f202d38bc5..7236dfbd886 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -4,6 +4,8 @@ from datetime import timedelta import logging from time import monotonic +from aiohue import AiohueException +from aiohue.sensors import TYPE_ZLL_PRESENCE import async_timeout from homeassistant.components import hue @@ -53,41 +55,12 @@ class SensorManager: def __init__(self, hass, bridge, config_entry): """Initialize the sensor manager.""" - import aiohue - from .binary_sensor import HuePresence, PRESENCE_NAME_FORMAT - from .sensor import ( - HueLightLevel, - HueTemperature, - LIGHT_LEVEL_NAME_FORMAT, - TEMPERATURE_NAME_FORMAT, - ) - self.hass = hass self.bridge = bridge self.config_entry = config_entry self._component_add_entities = {} self._started = False - self.sensor_config_map.update( - { - aiohue.sensors.TYPE_ZLL_LIGHTLEVEL: { - "binary": False, - "name_format": LIGHT_LEVEL_NAME_FORMAT, - "class": HueLightLevel, - }, - aiohue.sensors.TYPE_ZLL_TEMPERATURE: { - "binary": False, - "name_format": TEMPERATURE_NAME_FORMAT, - "class": HueTemperature, - }, - aiohue.sensors.TYPE_ZLL_PRESENCE: { - "binary": True, - "name_format": PRESENCE_NAME_FORMAT, - "class": HuePresence, - }, - } - ) - def register_component(self, binary, async_add_entities): """Register async_add_entities methods for components.""" self._component_add_entities[binary] = async_add_entities @@ -117,15 +90,13 @@ class SensorManager: async def async_update_items(self): """Update sensors from the bridge.""" - import aiohue - api = self.bridge.api.sensors try: start = monotonic() with async_timeout.timeout(4): await api.update() - except (asyncio.TimeoutError, aiohue.AiohueException) as err: + except (asyncio.TimeoutError, AiohueException) as err: _LOGGER.debug("Failed to fetch sensor: %s", err) if not self.bridge.available: @@ -164,7 +135,7 @@ class SensorManager: # finding the remaining ones that may or may not be related to the # presence sensors. for item_id in api: - if api[item_id].type != aiohue.sensors.TYPE_ZLL_PRESENCE: + if api[item_id].type != TYPE_ZLL_PRESENCE: continue primary_sensor_devices[_device_id(api[item_id])] = api[item_id] From 703cd961863dc0ea370642bbc5dbb94117644862 Mon Sep 17 00:00:00 2001 From: Santobert Date: Wed, 23 Oct 2019 08:03:38 +0200 Subject: [PATCH 588/639] Add improved scene support to the input_datetime integration (#28105) * input_datetime reproduce state * simplify service decision --- .../input_datetime/reproduce_state.py | 111 ++++++++++++++++++ .../input_datetime/test_reproduce_state.py | 69 +++++++++++ 2 files changed, 180 insertions(+) create mode 100644 homeassistant/components/input_datetime/reproduce_state.py create mode 100644 tests/components/input_datetime/test_reproduce_state.py diff --git a/homeassistant/components/input_datetime/reproduce_state.py b/homeassistant/components/input_datetime/reproduce_state.py new file mode 100644 index 00000000000..09a30e65210 --- /dev/null +++ b/homeassistant/components/input_datetime/reproduce_state.py @@ -0,0 +1,111 @@ +"""Reproduce an Input datetime state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import dt as dt_util + +from . import ( + ATTR_DATE, + ATTR_DATETIME, + ATTR_TIME, + CONF_HAS_DATE, + CONF_HAS_TIME, + DOMAIN, + SERVICE_SET_DATETIME, +) + +_LOGGER = logging.getLogger(__name__) + + +def is_valid_datetime(string: str) -> bool: + """Test if string dt is a valid datetime.""" + try: + return dt_util.parse_datetime(string) is not None + except ValueError: + return False + + +def is_valid_date(string: str) -> bool: + """Test if string dt is a valid date.""" + try: + return dt_util.parse_date(string) is not None + except ValueError: + return False + + +def is_valid_time(string: str) -> bool: + """Test if string dt is a valid time.""" + try: + return dt_util.parse_time(string) is not None + except ValueError: + return False + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if not ( + ( + is_valid_datetime(state.state) + and cur_state.attributes.get(CONF_HAS_DATE) + and cur_state.attributes.get(CONF_HAS_TIME) + ) + or ( + is_valid_date(state.state) + and cur_state.attributes.get(CONF_HAS_DATE) + and not cur_state.attributes.get(CONF_HAS_TIME) + ) + or ( + is_valid_time(state.state) + and cur_state.attributes.get(CONF_HAS_TIME) + and not cur_state.attributes.get(CONF_HAS_DATE) + ) + ): + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state: + return + + service = SERVICE_SET_DATETIME + service_data = {ATTR_ENTITY_ID: state.entity_id} + + has_time = cur_state.attributes.get(CONF_HAS_TIME) + has_date = cur_state.attributes.get(CONF_HAS_DATE) + + if has_time and has_date: + service_data[ATTR_DATETIME] = state.state + elif has_time: + service_data[ATTR_TIME] = state.state + elif has_date: + service_data[ATTR_DATE] = state.state + else: + _LOGGER.warning("input_datetime needs either has_date or has_time or both") + return + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Input datetime states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py new file mode 100644 index 00000000000..71f0658923c --- /dev/null +++ b/tests/components/input_datetime/test_reproduce_state.py @@ -0,0 +1,69 @@ +"""Test reproduce state for Input datetime.""" +from homeassistant.core import State + +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Input datetime states.""" + hass.states.async_set( + "input_datetime.entity_datetime", + "2010-10-10 01:20:00", + {"has_date": True, "has_time": True}, + ) + hass.states.async_set( + "input_datetime.entity_time", "01:20:00", {"has_date": False, "has_time": True} + ) + hass.states.async_set( + "input_datetime.entity_date", + "2010-10-10", + {"has_date": True, "has_time": False}, + ) + + datetime_calls = async_mock_service(hass, "input_datetime", "set_datetime") + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("input_datetime.entity_datetime", "2010-10-10 01:20:00"), + State("input_datetime.entity_time", "01:20:00"), + State("input_datetime.entity_date", "2010-10-10"), + ], + blocking=True, + ) + + assert len(datetime_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("input_datetime.entity_datetime", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(datetime_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("input_datetime.entity_datetime", "2011-10-10 02:20:00"), + State("input_datetime.entity_time", "02:20:00"), + State("input_datetime.entity_date", "2011-10-10"), + # Should not raise + State("input_datetime.non_existing", "2010-10-10 01:20:00"), + ], + blocking=True, + ) + + valid_calls = [ + { + "entity_id": "input_datetime.entity_datetime", + "datetime": "2011-10-10 02:20:00", + }, + {"entity_id": "input_datetime.entity_time", "time": "02:20:00"}, + {"entity_id": "input_datetime.entity_date", "date": "2011-10-10"}, + ] + assert len(datetime_calls) == 3 + for call in datetime_calls: + assert call.domain == "input_datetime" + assert call.data in valid_calls + valid_calls.remove(call.data) From 65263bdef98e1d09eb5f33a833415f57175695c7 Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 23 Oct 2019 08:08:38 +0200 Subject: [PATCH 589/639] Fix #28104 - CalDav support for floating datetimes (#28123) * Fix #28104 - CalDav support for floating datetimes Timzones are optional in CalDav It is possible that an entry contains neither a TZID, nor is an UTC time. When this is the case, it should be treated as a floating date-time value, which represent the same hour, minute, and second value regardless of which time zone is currently being observed. For Home-Assistant the correct timezone therefore is whatever is configured as local time in the settings. See https://www.kanzaki.com/docs/ical/dateTime.html * Revert "Fix #28104 - CalDav support for floating datetimes" This reverts commit cf32a6e39058e340816ae1e3ebd4a2c236b91964. * add test case: floating events fail with error without patch * Fix #28104 - CalDav support for floating datetimes Timzones are optional in CalDav It is possible that an entry contains neither a TZID, nor is an UTC time. When this is the case, it should be treated as a floating date-time value, which represent the same hour, minute, and second value regardless of which time zone is currently being observed. For Home-Assistant the correct timezone therefore is whatever is configured as local time in the settings. See https://www.kanzaki.com/docs/ical/dateTime.html * style fix --- homeassistant/components/caldav/calendar.py | 4 +++ tests/components/caldav/test_calendar.py | 36 +++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/homeassistant/components/caldav/calendar.py b/homeassistant/components/caldav/calendar.py index 2bbff2a6bc7..ad9dac1f727 100644 --- a/homeassistant/components/caldav/calendar.py +++ b/homeassistant/components/caldav/calendar.py @@ -278,6 +278,10 @@ class WebDavCalendarData: def to_datetime(obj): """Return a datetime.""" if isinstance(obj, datetime): + if obj.tzinfo is None: + # floating value, not bound to any time zone in particular + # represent same time regardless of which time zone is currently being observed + return obj.replace(tzinfo=dt.DEFAULT_TIME_ZONE) return obj return dt.as_local(dt.dt.datetime.combine(obj, dt.dt.time.min)) diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 209ab780265..c0be635988a 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -111,6 +111,19 @@ LOCATION:San Francisco DESCRIPTION:Sunny day END:VEVENT END:VCALENDAR +""", + """BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Global Corp.//CalDAV Client//EN +BEGIN:VEVENT +UID:8 +DTSTART:20171127T190000 +DTEND:20171127T200000 +SUMMARY:This is a floating Event +LOCATION:Hamburg +DESCRIPTION:What a day +END:VEVENT +END:VCALENDAR """, ] @@ -292,6 +305,29 @@ async def test_ongoing_event_different_tz(mock_now, hass, calendar): } +@patch("homeassistant.util.dt.now", return_value=_local_datetime(19, 10)) +async def test_ongoing_floating_event_returned(mock_now, hass, calendar): + """Test that floating events without timezones work.""" + assert await async_setup_component(hass, "calendar", {"calendar": CALDAV_CONFIG}) + await hass.async_block_till_done() + + state = hass.states.get("calendar.private") + print(dt.DEFAULT_TIME_ZONE) + print(state) + assert state.name == calendar.name + assert state.state == STATE_ON + assert dict(state.attributes) == { + "friendly_name": "Private", + "message": "This is a floating Event", + "all_day": False, + "offset_reached": False, + "start_time": "2017-11-27 19:00:00", + "end_time": "2017-11-27 20:00:00", + "location": "Hamburg", + "description": "What a day", + } + + @patch("homeassistant.util.dt.now", return_value=_local_datetime(8, 30)) async def test_ongoing_event_with_offset(mock_now, hass, calendar): """Test that the offset is taken into account.""" From f67813e145baa65b10ebc75a8c76c417bb17c833 Mon Sep 17 00:00:00 2001 From: Matt Schmitt Date: Wed, 23 Oct 2019 02:09:28 -0400 Subject: [PATCH 590/639] Fix service descriptions (#28122) --- homeassistant/components/nest/services.yaml | 49 +++++++++++++++------ 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/nest/services.yaml b/homeassistant/components/nest/services.yaml index 0015c83342d..e10e6264643 100644 --- a/homeassistant/components/nest/services.yaml +++ b/homeassistant/components/nest/services.yaml @@ -1,16 +1,37 @@ -set_mode: - description: 'Set the home/away mode for a Nest structure. Set to away mode will - also set Estimated Arrival Time if provided. Set ETA will cause the thermostat - to begin warming or cooling the home before the user arrives. After ETA set other - Automation can read ETA sensor as a signal to prepare the home for the user''s - arrival. +# Describes the format for available Nest services - ' +set_away_mode: + description: Set the away mode for a Nest structure. fields: - eta: {description: Optional Estimated Arrival Time from now., example: '0:10'} - eta_window: {description: Optional ETA window. Default is 1 minute., example: '0:5'} - home_mode: {description: home or away, example: home} - structure: {description: Optional structure name. Default set all structures managed - by Home Assistant., example: My Home} - trip_id: {description: Optional identity of a trip. Using the same trip_ID will - update the estimation., example: trip_back_home} + away_mode: + description: New mode to set. Valid modes are "away" or "home". + example: "away" + structure: + description: Name(s) of structure(s) to change. Defaults to all structures if not specified. + example: "Apartment" + +set_eta: + description: Set or update the estimated time of arrival window for a Nest structure. + fields: + eta: + description: Estimated time of arrival from now. + example: "00:10:30" + eta_window: + description: Estimated time of arrival window. Default is 1 minute. + example: "00:05" + trip_id: + description: Unique ID for the trip. Default is auto-generated using a timestamp. + example: "Leave Work" + structure: + description: Name(s) of structure(s) to change. Defaults to all structures if not specified. + example: "Apartment" + +cancel_eta: + description: Cancel an existing estimated time of arrival window for a Nest structure. + fields: + trip_id: + description: Unique ID for the trip. + example: "Leave Work" + structure: + description: Name(s) of structure(s) to change. Defaults to all structures if not specified. + example: "Apartment" From 50e9a9df4f4f59f2e4d85a20a98af58fc5b99c58 Mon Sep 17 00:00:00 2001 From: Santobert Date: Wed, 23 Oct 2019 08:12:17 +0200 Subject: [PATCH 591/639] Timer reproduce state (#28117) --- .../components/timer/reproduce_state.py | 70 ++++++++++++++++ .../components/timer/test_reproduce_state.py | 84 +++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 homeassistant/components/timer/reproduce_state.py create mode 100644 tests/components/timer/test_reproduce_state.py diff --git a/homeassistant/components/timer/reproduce_state.py b/homeassistant/components/timer/reproduce_state.py new file mode 100644 index 00000000000..c765ed7da9c --- /dev/null +++ b/homeassistant/components/timer/reproduce_state.py @@ -0,0 +1,70 @@ +"""Reproduce an Timer state.""" +import asyncio +import logging +from typing import Iterable, Optional + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import Context, State +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + ATTR_DURATION, + DOMAIN, + SERVICE_CANCEL, + SERVICE_PAUSE, + SERVICE_START, + STATUS_ACTIVE, + STATUS_IDLE, + STATUS_PAUSED, +) + +_LOGGER = logging.getLogger(__name__) + +VALID_STATES = {STATUS_IDLE, STATUS_ACTIVE, STATUS_PAUSED} + + +async def _async_reproduce_state( + hass: HomeAssistantType, state: State, context: Optional[Context] = None +) -> None: + """Reproduce a single state.""" + cur_state = hass.states.get(state.entity_id) + + if cur_state is None: + _LOGGER.warning("Unable to find entity %s", state.entity_id) + return + + if state.state not in VALID_STATES: + _LOGGER.warning( + "Invalid state specified for %s: %s", state.entity_id, state.state + ) + return + + # Return if we are already at the right state. + if cur_state.state == state.state and cur_state.attributes.get( + ATTR_DURATION + ) == state.attributes.get(ATTR_DURATION): + return + + service_data = {ATTR_ENTITY_ID: state.entity_id} + + if state.state == STATUS_ACTIVE: + service = SERVICE_START + if ATTR_DURATION in state.attributes: + service_data[ATTR_DURATION] = state.attributes[ATTR_DURATION] + elif state.state == STATUS_PAUSED: + service = SERVICE_PAUSE + elif state.state == STATUS_IDLE: + service = SERVICE_CANCEL + + await hass.services.async_call( + DOMAIN, service, service_data, context=context, blocking=True + ) + + +async def async_reproduce_states( + hass: HomeAssistantType, states: Iterable[State], context: Optional[Context] = None +) -> None: + """Reproduce Timer states.""" + await asyncio.gather( + *(_async_reproduce_state(hass, state, context) for state in states) + ) diff --git a/tests/components/timer/test_reproduce_state.py b/tests/components/timer/test_reproduce_state.py new file mode 100644 index 00000000000..5539d8610c3 --- /dev/null +++ b/tests/components/timer/test_reproduce_state.py @@ -0,0 +1,84 @@ +"""Test reproduce state for Timer.""" +from homeassistant.components.timer import ( + ATTR_DURATION, + SERVICE_CANCEL, + SERVICE_PAUSE, + SERVICE_START, + STATUS_ACTIVE, + STATUS_IDLE, + STATUS_PAUSED, +) +from homeassistant.core import State +from tests.common import async_mock_service + + +async def test_reproducing_states(hass, caplog): + """Test reproducing Timer states.""" + hass.states.async_set("timer.entity_idle", STATUS_IDLE, {}) + hass.states.async_set("timer.entity_paused", STATUS_PAUSED, {}) + hass.states.async_set("timer.entity_active", STATUS_ACTIVE, {}) + hass.states.async_set( + "timer.entity_active_attr", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"} + ) + + start_calls = async_mock_service(hass, "timer", SERVICE_START) + pause_calls = async_mock_service(hass, "timer", SERVICE_PAUSE) + cancel_calls = async_mock_service(hass, "timer", SERVICE_CANCEL) + + # These calls should do nothing as entities already in desired state + await hass.helpers.state.async_reproduce_state( + [ + State("timer.entity_idle", STATUS_IDLE), + State("timer.entity_paused", STATUS_PAUSED), + State("timer.entity_active", STATUS_ACTIVE), + State( + "timer.entity_active_attr", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"} + ), + ], + blocking=True, + ) + + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(cancel_calls) == 0 + + # Test invalid state is handled + await hass.helpers.state.async_reproduce_state( + [State("timer.entity_idle", "not_supported")], blocking=True + ) + + assert "not_supported" in caplog.text + assert len(start_calls) == 0 + assert len(pause_calls) == 0 + assert len(cancel_calls) == 0 + + # Make sure correct services are called + await hass.helpers.state.async_reproduce_state( + [ + State("timer.entity_idle", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"}), + State("timer.entity_paused", STATUS_ACTIVE), + State("timer.entity_active", STATUS_IDLE), + State("timer.entity_active_attr", STATUS_PAUSED), + # Should not raise + State("timer.non_existing", "on"), + ], + blocking=True, + ) + + valid_start_calls = [ + {"entity_id": "timer.entity_idle", ATTR_DURATION: "00:01:00"}, + {"entity_id": "timer.entity_paused"}, + ] + assert len(start_calls) == 2 + for call in start_calls: + assert call.domain == "timer" + assert call.data in valid_start_calls + valid_start_calls.remove(call.data) + + assert len(pause_calls) == 1 + assert pause_calls[0].domain == "timer" + assert pause_calls[0].data == {"entity_id": "timer.entity_active_attr"} + + assert len(cancel_calls) == 1 + assert cancel_calls[0].domain == "timer" + assert cancel_calls[0].data == {"entity_id": "timer.entity_active"} From c8b2860167f597871a1fb0aae2554b55829c5d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 23 Oct 2019 09:12:57 +0300 Subject: [PATCH 592/639] Fix bootstrap dev dependencies message (#28114) https://github.com/home-assistant/home-assistant/pull/28060#discussion_r337701541 --- script/bootstrap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bootstrap b/script/bootstrap index ba594cbb341..211f1355b7d 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -6,5 +6,5 @@ set -e cd "$(dirname "$0")/.." -echo "Installing test dependencies..." +echo "Installing development dependencies..." python3 -m pip install tox colorlog pre-commit $(grep mypy requirements_test.txt) From b4054add616af44963bc49add37239c16d106c62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Wed, 23 Oct 2019 06:14:52 +0000 Subject: [PATCH 593/639] Move imports in wake_on_lan component (#28100) * Move imports in wake_on_lan component * Fix tox tests --- .gitignore | 3 + .../components/wake_on_lan/__init__.py | 2 +- .../components/wake_on_lan/switch.py | 8 +-- tests/components/wake_on_lan/test_init.py | 66 +++++++++---------- 4 files changed, 37 insertions(+), 42 deletions(-) diff --git a/.gitignore b/.gitignore index 15f0896975d..2473aeb4bf6 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,6 @@ monkeytype.sqlite3 # This is left behind by Azure Restore Cache tmp_cache + +# python-language-server / Rope +.ropeproject diff --git a/homeassistant/components/wake_on_lan/__init__.py b/homeassistant/components/wake_on_lan/__init__.py index 421f6265c0c..b4aad4925b9 100644 --- a/homeassistant/components/wake_on_lan/__init__.py +++ b/homeassistant/components/wake_on_lan/__init__.py @@ -3,6 +3,7 @@ from functools import partial import logging import voluptuous as vol +import wakeonlan from homeassistant.const import CONF_MAC import homeassistant.helpers.config_validation as cv @@ -22,7 +23,6 @@ WAKE_ON_LAN_SEND_MAGIC_PACKET_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the wake on LAN component.""" - import wakeonlan async def send_magic_packet(call): """Send magic packet to wake up a device.""" diff --git a/homeassistant/components/wake_on_lan/switch.py b/homeassistant/components/wake_on_lan/switch.py index 453685b13f6..01f69679829 100644 --- a/homeassistant/components/wake_on_lan/switch.py +++ b/homeassistant/components/wake_on_lan/switch.py @@ -4,6 +4,7 @@ import platform import subprocess as sp import voluptuous as vol +import wakeonlan from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice from homeassistant.const import CONF_HOST, CONF_NAME @@ -48,8 +49,6 @@ class WOLSwitch(SwitchDevice): def __init__(self, hass, name, host, mac_address, off_action, broadcast_address): """Initialize the WOL switch.""" - import wakeonlan - self._hass = hass self._name = name self._host = host @@ -57,7 +56,6 @@ class WOLSwitch(SwitchDevice): self._broadcast_address = broadcast_address self._off_script = Script(hass, off_action) if off_action else None self._state = False - self._wol = wakeonlan @property def is_on(self): @@ -72,11 +70,11 @@ class WOLSwitch(SwitchDevice): def turn_on(self, **kwargs): """Turn the device on.""" if self._broadcast_address: - self._wol.send_magic_packet( + wakeonlan.send_magic_packet( self._mac_address, ip_address=self._broadcast_address ) else: - self._wol.send_magic_packet(self._mac_address) + wakeonlan.send_magic_packet(self._mac_address) def turn_off(self, **kwargs): """Turn the device off if an off action is present.""" diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py index d71f15e6109..c2ee0930895 100644 --- a/tests/components/wake_on_lan/test_init.py +++ b/tests/components/wake_on_lan/test_init.py @@ -1,52 +1,46 @@ """Tests for Wake On LAN component.""" -import asyncio -from unittest import mock - import pytest import voluptuous as vol -from homeassistant.setup import async_setup_component +from homeassistant.components import wake_on_lan from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET +from homeassistant.setup import async_setup_component + +from tests.common import MockDependency -@pytest.fixture -def mock_wakeonlan(): - """Mock mock_wakeonlan.""" - module = mock.MagicMock() - with mock.patch.dict("sys.modules", {"wakeonlan": module}): - yield module - - -@asyncio.coroutine -def test_send_magic_packet(hass, caplog, mock_wakeonlan): +async def test_send_magic_packet(hass): """Test of send magic packet service call.""" - mac = "aa:bb:cc:dd:ee:ff" - bc_ip = "192.168.255.255" + with MockDependency("wakeonlan") as mocked_wakeonlan: + mac = "aa:bb:cc:dd:ee:ff" + bc_ip = "192.168.255.255" - yield from async_setup_component(hass, DOMAIN, {}) + wake_on_lan.wakeonlan = mocked_wakeonlan - yield from hass.services.async_call( - DOMAIN, - SERVICE_SEND_MAGIC_PACKET, - {"mac": mac, "broadcast_address": bc_ip}, - blocking=True, - ) - assert len(mock_wakeonlan.mock_calls) == 1 - assert mock_wakeonlan.mock_calls[-1][1][0] == mac - assert mock_wakeonlan.mock_calls[-1][2]["ip_address"] == bc_ip + await async_setup_component(hass, DOMAIN, {}) - with pytest.raises(vol.Invalid): - yield from hass.services.async_call( + await hass.services.async_call( DOMAIN, SERVICE_SEND_MAGIC_PACKET, - {"broadcast_address": bc_ip}, + {"mac": mac, "broadcast_address": bc_ip}, blocking=True, ) - assert len(mock_wakeonlan.mock_calls) == 1 + assert len(mocked_wakeonlan.mock_calls) == 1 + assert mocked_wakeonlan.mock_calls[-1][1][0] == mac + assert mocked_wakeonlan.mock_calls[-1][2]["ip_address"] == bc_ip - yield from hass.services.async_call( - DOMAIN, SERVICE_SEND_MAGIC_PACKET, {"mac": mac}, blocking=True - ) - assert len(mock_wakeonlan.mock_calls) == 2 - assert mock_wakeonlan.mock_calls[-1][1][0] == mac - assert not mock_wakeonlan.mock_calls[-1][2] + with pytest.raises(vol.Invalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SEND_MAGIC_PACKET, + {"broadcast_address": bc_ip}, + blocking=True, + ) + assert len(mocked_wakeonlan.mock_calls) == 1 + + await hass.services.async_call( + DOMAIN, SERVICE_SEND_MAGIC_PACKET, {"mac": mac}, blocking=True + ) + assert len(mocked_wakeonlan.mock_calls) == 2 + assert mocked_wakeonlan.mock_calls[-1][1][0] == mac + assert not mocked_wakeonlan.mock_calls[-1][2] From 62a3dc1a9405d0c21a02656423beecbe9a32fa32 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Wed, 23 Oct 2019 09:17:34 +0300 Subject: [PATCH 594/639] Open Hardware Monitor Sensor reconnect (#28052) * raise PlatformNotReady * Don't show errors on reconnect --- homeassistant/components/openhardwaremonitor/sensor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/openhardwaremonitor/sensor.py b/homeassistant/components/openhardwaremonitor/sensor.py index fc228ee26fb..0729943a770 100644 --- a/homeassistant/components/openhardwaremonitor/sensor.py +++ b/homeassistant/components/openhardwaremonitor/sensor.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -38,6 +39,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Open Hardware Monitor platform.""" data = OpenHardwareMonitorData(config, hass) + if data.data is None: + raise PlatformNotReady add_entities(data.devices, True) @@ -130,7 +133,7 @@ class OpenHardwareMonitorData: response = requests.get(data_url, timeout=30) self.data = response.json() except requests.exceptions.ConnectionError: - _LOGGER.error("ConnectionError: Is OpenHardwareMonitor running?") + _LOGGER.debug("ConnectionError: Is OpenHardwareMonitor running?") def initialize(self, now): """Parse of the sensors and adding of devices.""" From 734704c1f7e934e4863c05c0f432ceb6b56f7df2 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Wed, 23 Oct 2019 09:18:00 +0300 Subject: [PATCH 595/639] Squeezebox LMS reconnect (#27378) * Fix * Review --- homeassistant/components/squeezebox/media_player.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 6d67f67a3ce..d8574223307 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -41,6 +41,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv +from homeassistant.exceptions import PlatformNotReady from homeassistant.util.dt import utcnow _LOGGER = logging.getLogger(__name__) @@ -126,18 +127,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Get IP of host, to prevent duplication of same host (different DNS names) try: ipaddr = socket.gethostbyname(host) - except (OSError) as error: + except OSError as error: _LOGGER.error("Could not communicate with %s:%d: %s", host, port, error) - return False + raise PlatformNotReady from error if ipaddr in known_servers: return - known_servers.add(ipaddr) _LOGGER.debug("Creating LMS object for %s", ipaddr) lms = LogitechMediaServer(hass, host, port, username, password) players = await lms.create_players() + if players is None: + raise PlatformNotReady + + known_servers.add(ipaddr) hass.data[DATA_SQUEEZEBOX].extend(players) async_add_entities(players) @@ -194,7 +198,7 @@ class LogitechMediaServer: result = [] data = await self.async_query("players", "status") if data is False: - return result + return None for players in data.get("players_loop", []): player = SqueezeBoxDevice(self, players["playerid"], players["name"]) await player.async_update() From 6025630772786e9f6edd72417abd67529da8b810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Wed, 23 Oct 2019 06:19:00 +0000 Subject: [PATCH 596/639] Move imports in melissa component (#28021) * Move imports in melissa component * Fix tox tests --- homeassistant/components/melissa/__init__.py | 5 ++--- tests/components/melissa/test_init.py | 5 +++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/melissa/__init__.py b/homeassistant/components/melissa/__init__.py index 830036b072a..c03939e3e9c 100644 --- a/homeassistant/components/melissa/__init__.py +++ b/homeassistant/components/melissa/__init__.py @@ -1,9 +1,10 @@ """Support for Melissa climate.""" import logging +import melissa import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform @@ -28,8 +29,6 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the Melissa Climate component.""" - import melissa - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) diff --git a/tests/components/melissa/test_init.py b/tests/components/melissa/test_init.py index dfdaf80981f..892f4d60a44 100644 --- a/tests/components/melissa/test_init.py +++ b/tests/components/melissa/test_init.py @@ -1,14 +1,15 @@ """The test for the Melissa Climate component.""" -from tests.common import MockDependency, mock_coro_func - from homeassistant.components import melissa +from tests.common import MockDependency, mock_coro_func + VALID_CONFIG = {"melissa": {"username": "********", "password": "********"}} async def test_setup(hass): """Test setting up the Melissa component.""" with MockDependency("melissa") as mocked_melissa: + melissa.melissa = mocked_melissa mocked_melissa.AsyncMelissa().async_connect = mock_coro_func() await melissa.async_setup(hass, VALID_CONFIG) From acc3646ef3867f110a3695238a69320027995d3f Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Wed, 23 Oct 2019 08:31:43 +0200 Subject: [PATCH 597/639] Add Solar-Log platform (#27036) * Add Solar-Log sensor * Codeowners update * Update homeassistant/components/solarlog/manifest.json Co-Authored-By: Paulus Schoutsen * remove sunwatcher from gen_requirements_all.py * remove sunwatcher from requirements_test_all.txt * Remove scan_interval as configuration variable I've set it to a fixed scan_interval of 1 minute. Removed the configuration option. * Fix black format * Config flow added (__init__.py) * Config flow added (manifest.json) * Config flow added (const.py) * Config flow added (config_flow.py) * Config flow added (strings.json) * Config flow added (en.json translation) * Config flow added (sensor.py rewritten) * Config flow added (sensor.py) * Config flow added (config_flows.py) * resolve conflict config_flows.py * Add tests * add tests * add tests * Update .coverage to include all files for solarlog * Fix await the unload * Adjust icons, add http:// to default host * Change icons * Add http:// to host if not provided, fix await * Add http:// to host if not provided, fix await * Adjust tests for http:// added to host * remove line * Remove without http:// requirement * Remove without http;// requirement --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/solarlog/.translations/en.json | 21 +++ homeassistant/components/solarlog/__init__.py | 21 +++ .../components/solarlog/config_flow.py | 107 ++++++++++++ homeassistant/components/solarlog/const.py | 89 ++++++++++ .../components/solarlog/manifest.json | 9 + homeassistant/components/solarlog/sensor.py | 159 ++++++++++++++++++ .../components/solarlog/strings.json | 21 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/solarlog/__init__.py | 1 + tests/components/solarlog/test_config_flow.py | 135 +++++++++++++++ 14 files changed, 572 insertions(+) create mode 100644 homeassistant/components/solarlog/.translations/en.json create mode 100644 homeassistant/components/solarlog/__init__.py create mode 100644 homeassistant/components/solarlog/config_flow.py create mode 100644 homeassistant/components/solarlog/const.py create mode 100644 homeassistant/components/solarlog/manifest.json create mode 100644 homeassistant/components/solarlog/sensor.py create mode 100644 homeassistant/components/solarlog/strings.json create mode 100644 tests/components/solarlog/__init__.py create mode 100644 tests/components/solarlog/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index f40b0c30342..f34b0baac17 100644 --- a/.coveragerc +++ b/.coveragerc @@ -620,6 +620,7 @@ omit = homeassistant/components/solaredge/__init__.py homeassistant/components/solaredge/sensor.py homeassistant/components/solaredge_local/sensor.py + homeassistant/components/solarlog/* homeassistant/components/solax/sensor.py homeassistant/components/soma/cover.py homeassistant/components/soma/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index e1ff7b36ff1..a5ad222323b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -265,6 +265,7 @@ homeassistant/components/smartthings/* @andrewsayre homeassistant/components/smarty/* @z0mbieprocess homeassistant/components/smtp/* @fabaff homeassistant/components/solaredge_local/* @drobtravels @scheric +homeassistant/components/solarlog/* @Ernst79 homeassistant/components/solax/* @squishykid homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne diff --git a/homeassistant/components/solarlog/.translations/en.json b/homeassistant/components/solarlog/.translations/en.json new file mode 100644 index 00000000000..5399d5176c9 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Solar-Log", + "step": { + "user": { + "title": "Define your Solar-Log connection", + "data": { + "host": "The hostname or ip-address of your Solar-Log device", + "name": "The prefix to be used for your Solar-Log sensors" + } + } + }, + "error": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect, please verify host address" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/homeassistant/components/solarlog/__init__.py b/homeassistant/components/solarlog/__init__.py new file mode 100644 index 00000000000..c8035e1f7e6 --- /dev/null +++ b/homeassistant/components/solarlog/__init__.py @@ -0,0 +1,21 @@ +"""Solar-Log integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + + +async def async_setup(hass, config): + """Component setup, do nothing.""" + return True + + +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up a config entry for solarlog.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "sensor") + ) + return True + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.config_entries.async_forward_entry_unload(entry, "sensor") diff --git a/homeassistant/components/solarlog/config_flow.py b/homeassistant/components/solarlog/config_flow.py new file mode 100644 index 00000000000..5cb2d5deec1 --- /dev/null +++ b/homeassistant/components/solarlog/config_flow.py @@ -0,0 +1,107 @@ +"""Config flow for solarlog integration.""" +import logging +from urllib.parse import ParseResult, urlparse + +from requests.exceptions import HTTPError, Timeout +from sunwatcher.solarlog.solarlog import SolarLog +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.util import slugify + +from .const import DEFAULT_HOST, DEFAULT_NAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +@callback +def solarlog_entries(hass: HomeAssistant): + """Return the hosts already configured.""" + return set( + entry.data[CONF_HOST] for entry in hass.config_entries.async_entries(DOMAIN) + ) + + +class SolarLogConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for solarlog.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self) -> None: + """Initialize the config flow.""" + self._errors = {} + + def _host_in_configuration_exists(self, host) -> bool: + """Return True if host exists in configuration.""" + if host in solarlog_entries(self.hass): + return True + return False + + async def _test_connection(self, host): + """Check if we can connect to the Solar-Log device.""" + try: + await self.hass.async_add_executor_job(SolarLog, host) + return True + except (OSError, HTTPError, Timeout): + self._errors[CONF_HOST] = "cannot_connect" + _LOGGER.error( + "Could not connect to Solar-Log device at %s, check host ip address", + host, + ) + return False + + async def async_step_user(self, user_input=None): + """Step when user intializes a integration.""" + self._errors = {} + if user_input is not None: + # set some defaults in case we need to return to the form + name = slugify(user_input.get(CONF_NAME, DEFAULT_NAME)) + host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) + + url = urlparse(host_entry, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + host = url.geturl() + + if self._host_in_configuration_exists(host): + self._errors[CONF_HOST] = "already_configured" + else: + if await self._test_connection(host): + return self.async_create_entry(title=name, data={CONF_HOST: host}) + else: + user_input = {} + user_input[CONF_NAME] = DEFAULT_NAME + user_input[CONF_HOST] = DEFAULT_HOST + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) + ): str, + vol.Required( + CONF_HOST, default=user_input.get(CONF_HOST, DEFAULT_HOST) + ): str, + } + ), + errors=self._errors, + ) + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + host_entry = user_input.get(CONF_HOST, DEFAULT_HOST) + + url = urlparse(host_entry, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + host = url.geturl() + + if self._host_in_configuration_exists(host): + return self.async_abort(reason="already_configured") + return await self.async_step_user(user_input) diff --git a/homeassistant/components/solarlog/const.py b/homeassistant/components/solarlog/const.py new file mode 100644 index 00000000000..67eb8006cec --- /dev/null +++ b/homeassistant/components/solarlog/const.py @@ -0,0 +1,89 @@ +"""Constants for the Solar-Log integration.""" +from datetime import timedelta + +from homeassistant.const import POWER_WATT, ENERGY_KILO_WATT_HOUR + +DOMAIN = "solarlog" + +"""Default config for solarlog.""" +DEFAULT_HOST = "http://solar-log" +DEFAULT_NAME = "solarlog" + +"""Fixed constants.""" +SCAN_INTERVAL = timedelta(seconds=60) + +"""Supported sensor types.""" +SENSOR_TYPES = { + "time": ["TIME", "last update", None, "mdi:calendar-clock"], + "power_ac": ["powerAC", "power AC", POWER_WATT, "mdi:solar-power"], + "power_dc": ["powerDC", "power DC", POWER_WATT, "mdi:solar-power"], + "voltage_ac": ["voltageAC", "voltage AC", "V", "mdi:flash"], + "voltage_dc": ["voltageDC", "voltage DC", "V", "mdi:flash"], + "yield_day": ["yieldDAY", "yield day", ENERGY_KILO_WATT_HOUR, "mdi:solar-power"], + "yield_yesterday": [ + "yieldYESTERDAY", + "yield yesterday", + ENERGY_KILO_WATT_HOUR, + "mdi:solar-power", + ], + "yield_month": [ + "yieldMONTH", + "yield month", + ENERGY_KILO_WATT_HOUR, + "mdi:solar-power", + ], + "yield_year": ["yieldYEAR", "yield year", ENERGY_KILO_WATT_HOUR, "mdi:solar-power"], + "yield_total": [ + "yieldTOTAL", + "yield total", + ENERGY_KILO_WATT_HOUR, + "mdi:solar-power", + ], + "consumption_ac": ["consumptionAC", "consumption AC", POWER_WATT, "mdi:power-plug"], + "consumption_day": [ + "consumptionDAY", + "consumption day", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_yesterday": [ + "consumptionYESTERDAY", + "consumption yesterday", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_month": [ + "consumptionMONTH", + "consumption month", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_year": [ + "consumptionYEAR", + "consumption year", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "consumption_total": [ + "consumptionTOTAL", + "consumption total", + ENERGY_KILO_WATT_HOUR, + "mdi:power-plug", + ], + "total_power": ["totalPOWER", "total power", "Wp", "mdi:solar-power"], + "alternator_loss": [ + "alternatorLOSS", + "alternator loss", + POWER_WATT, + "mdi:solar-power", + ], + "capacity": ["CAPACITY", "capacity", "%", "mdi:solar-power"], + "efficiency": ["EFFICIENCY", "efficiency", "% W/Wp", "mdi:solar-power"], + "power_available": [ + "powerAVAILABLE", + "power available", + POWER_WATT, + "mdi:solar-power", + ], + "usage": ["USAGE", "usage", None, "mdi:solar-power"], +} diff --git a/homeassistant/components/solarlog/manifest.json b/homeassistant/components/solarlog/manifest.json new file mode 100644 index 00000000000..9331628e027 --- /dev/null +++ b/homeassistant/components/solarlog/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "solarlog", + "name": "Solar-Log", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integration/solarlog", + "dependencies": [], + "codeowners": ["@Ernst79"], + "requirements": ["sunwatcher==0.2.1"] +} diff --git a/homeassistant/components/solarlog/sensor.py b/homeassistant/components/solarlog/sensor.py new file mode 100644 index 00000000000..583529ffe87 --- /dev/null +++ b/homeassistant/components/solarlog/sensor.py @@ -0,0 +1,159 @@ +"""Platform for solarlog sensors.""" +import logging +from urllib.parse import ParseResult, urlparse + +from requests.exceptions import HTTPError, Timeout +from sunwatcher.solarlog.solarlog import SolarLog +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +from .const import DOMAIN, DEFAULT_HOST, DEFAULT_NAME, SCAN_INTERVAL, SENSOR_TYPES + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + } +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import YAML configuration when available.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=dict(config) + ) + ) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Add solarlog entry.""" + host_entry = entry.data[CONF_HOST] + + url = urlparse(host_entry, "http") + netloc = url.netloc or url.path + path = url.path if url.netloc else "" + url = ParseResult("http", netloc, path, *url[3:]) + host = url.geturl() + + platform_name = entry.title + + try: + api = await hass.async_add_executor_job(SolarLog, host) + _LOGGER.debug("Connected to Solar-Log device, setting up entries") + except (OSError, HTTPError, Timeout): + _LOGGER.error( + "Could not connect to Solar-Log device at %s, check host ip address", host + ) + return + + # Create solarlog data service which will retrieve and update the data. + data = await hass.async_add_executor_job(SolarlogData, hass, api, host) + + # Create a new sensor for each sensor type. + entities = [] + for sensor_key in SENSOR_TYPES: + sensor = SolarlogSensor(platform_name, sensor_key, data) + entities.append(sensor) + + async_add_entities(entities, True) + return True + + +class SolarlogSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, platform_name, sensor_key, data): + """Initialize the sensor.""" + self.platform_name = platform_name + self.sensor_key = sensor_key + self.data = data + self._state = None + + self._json_key = SENSOR_TYPES[self.sensor_key][0] + self._unit_of_measurement = SENSOR_TYPES[self.sensor_key][2] + + @property + def name(self): + """Return the name of the sensor.""" + return "{} ({})".format(self.platform_name, SENSOR_TYPES[self.sensor_key][1]) + + @property + def unit_of_measurement(self): + """Return the state of the sensor.""" + return self._unit_of_measurement + + @property + def icon(self): + """Return the sensor icon.""" + return SENSOR_TYPES[self.sensor_key][3] + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data from the sensor and update the state.""" + self.data.update() + self._state = self.data.data[self._json_key] + + +class SolarlogData: + """Get and update the latest data.""" + + def __init__(self, hass, api, host): + """Initialize the data object.""" + self.api = api + self.hass = hass + self.host = host + self.update = Throttle(SCAN_INTERVAL)(self._update) + self.data = {} + + def _update(self): + """Update the data from the SolarLog device.""" + try: + self.api = SolarLog(self.host) + response = self.api.time + _LOGGER.debug( + "Connection to Solarlog successful. Retrieving latest Solarlog update of %s", + response, + ) + except (OSError, Timeout, HTTPError): + _LOGGER.error("Connection error, Could not retrieve data, skipping update") + return + + try: + self.data["TIME"] = self.api.time + self.data["powerAC"] = self.api.power_ac + self.data["powerDC"] = self.api.power_dc + self.data["voltageAC"] = self.api.voltage_ac + self.data["voltageDC"] = self.api.voltage_dc + self.data["yieldDAY"] = self.api.yield_day / 1000 + self.data["yieldYESTERDAY"] = self.api.yield_yesterday / 1000 + self.data["yieldMONTH"] = self.api.yield_month / 1000 + self.data["yieldYEAR"] = self.api.yield_year / 1000 + self.data["yieldTOTAL"] = self.api.yield_total / 1000 + self.data["consumptionAC"] = self.api.consumption_ac + self.data["consumptionDAY"] = self.api.consumption_day / 1000 + self.data["consumptionYESTERDAY"] = self.api.consumption_yesterday / 1000 + self.data["consumptionMONTH"] = self.api.consumption_month / 1000 + self.data["consumptionYEAR"] = self.api.consumption_year / 1000 + self.data["consumptionTOTAL"] = self.api.consumption_total / 1000 + self.data["totalPOWER"] = self.api.total_power + self.data["alternatorLOSS"] = self.api.alternator_loss + self.data["CAPACITY"] = round(self.api.capacity * 100, 0) + self.data["EFFICIENCY"] = round(self.api.efficiency * 100, 0) + self.data["powerAVAILABLE"] = self.api.power_available + self.data["USAGE"] = self.api.usage + _LOGGER.debug("Updated Solarlog overview data: %s", self.data) + except AttributeError: + _LOGGER.error("Missing details data in Solarlog response") diff --git a/homeassistant/components/solarlog/strings.json b/homeassistant/components/solarlog/strings.json new file mode 100644 index 00000000000..5399d5176c9 --- /dev/null +++ b/homeassistant/components/solarlog/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "title": "Solar-Log", + "step": { + "user": { + "title": "Define your Solar-Log connection", + "data": { + "host": "The hostname or ip-address of your Solar-Log device", + "name": "The prefix to be used for your Solar-Log sensors" + } + } + }, + "error": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect, please verify host address" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 60aa610ec07..bf63869bc9b 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -60,6 +60,7 @@ FLOWS = [ "smartthings", "smhi", "solaredge", + "solarlog", "soma", "somfy", "sonos", diff --git a/requirements_all.txt b/requirements_all.txt index 884155f3b4c..868855ed851 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1842,6 +1842,9 @@ stringcase==1.2.0 # homeassistant.components.ecovacs sucks==0.9.4 +# homeassistant.components.solarlog +sunwatcher==0.2.1 + # homeassistant.components.swiss_hydrological_data swisshydrodata==0.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71cbac4de0f..2fd91e21d96 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -583,6 +583,9 @@ statsd==3.2.1 # homeassistant.components.traccar stringcase==1.2.0 +# homeassistant.components.solarlog +sunwatcher==0.2.1 + # homeassistant.components.tellduslive tellduslive==0.10.10 diff --git a/tests/components/solarlog/__init__.py b/tests/components/solarlog/__init__.py new file mode 100644 index 00000000000..9074cab8416 --- /dev/null +++ b/tests/components/solarlog/__init__.py @@ -0,0 +1 @@ +"""Tests for the solarlog integration.""" diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py new file mode 100644 index 00000000000..86f3b05d975 --- /dev/null +++ b/tests/components/solarlog/test_config_flow.py @@ -0,0 +1,135 @@ +"""Test the solarlog config flow.""" +from unittest.mock import patch +import pytest + +from homeassistant import data_entry_flow +from homeassistant import config_entries, setup +from homeassistant.components.solarlog import config_flow +from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME + +from tests.common import MockConfigEntry, mock_coro + +NAME = "Solarlog test 1 2 3" +HOST = "http://1.1.1.1" + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", + return_value=mock_coro({"title": "solarlog test 1 2 3"}), + ), patch( + "homeassistant.components.solarlog.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.solarlog.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": HOST, "name": NAME} + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "solarlog_test_1_2_3" + assert result2["data"] == {"host": "http://1.1.1.1"} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.fixture(name="test_connect") +def mock_controller(): + """Mock a successfull _host_in_configuration_exists.""" + with patch( + "homeassistant.components.solarlog.config_flow.SolarLogConfigFlow._test_connection", + side_effect=lambda *_: mock_coro(True), + ): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + flow = config_flow.SolarLogConfigFlow() + flow.hass = hass + return flow + + +async def test_user(hass, test_connect): + """Test user config.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # tets with all provided + result = await flow.async_step_user({CONF_NAME: NAME, CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + + +async def test_import(hass, test_connect): + """Test import step.""" + flow = init_config_flow(hass) + + # import with only host + result = await flow.async_step_import({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog" + assert result["data"][CONF_HOST] == HOST + + # import with only name + result = await flow.async_step_import({CONF_NAME: NAME}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == DEFAULT_HOST + + # import with host and name + result = await flow.async_step_import({CONF_HOST: HOST, CONF_NAME: NAME}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == HOST + + +async def test_abort_if_already_setup(hass, test_connect): + """Test we abort if the device is already setup.""" + flow = init_config_flow(hass) + MockConfigEntry( + domain="solarlog", data={CONF_NAME: NAME, CONF_HOST: HOST} + ).add_to_hass(hass) + + # Should fail, same HOST different NAME (default) + result = await flow.async_step_import( + {CONF_HOST: HOST, CONF_NAME: "solarlog_test_7_8_9"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same HOST and NAME + result = await flow.async_step_user({CONF_HOST: HOST, CONF_NAME: NAME}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "already_configured"} + + # SHOULD pass, diff HOST (without http://), different NAME + result = await flow.async_step_import( + {CONF_HOST: "2.2.2.2", CONF_NAME: "solarlog_test_7_8_9"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_7_8_9" + assert result["data"][CONF_HOST] == "http://2.2.2.2" + + # SHOULD pass, diff HOST, same NAME + result = await flow.async_step_import( + {CONF_HOST: "http://2.2.2.2", CONF_NAME: NAME} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "solarlog_test_1_2_3" + assert result["data"][CONF_HOST] == "http://2.2.2.2" From a644182b5e200296c97656b038e941e3b9f8b87d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 23 Oct 2019 01:32:57 -0500 Subject: [PATCH 598/639] Save client identifier from Plex auth for future use (#27951) * Save client identifier from auth for future use * Bump requirements * Stick with version 1 --- homeassistant/components/plex/config_flow.py | 5 +++++ homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/manifest.json | 2 +- homeassistant/components/plex/server.py | 9 +++++++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index a11fb9119a6..c03b958b2da 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -19,6 +19,7 @@ from homeassistant.util.json import load_json from .const import ( # pylint: disable=unused-import AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, + CONF_CLIENT_IDENTIFIER, CONF_SERVER, CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, @@ -65,6 +66,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.available_servers = None self.plexauth = None self.token = None + self.client_id = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -116,6 +118,8 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): token = server_config.get(CONF_TOKEN) entry_config = {CONF_URL: url} + if self.client_id: + entry_config[CONF_CLIENT_IDENTIFIER] = self.client_id if token: entry_config[CONF_TOKEN] = token if url.startswith("https"): @@ -216,6 +220,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_external_step_done(next_step_id="timed_out") self.token = token + self.client_id = self.plexauth.client_identifier return self.async_external_step_done(next_step_id="use_external_token") async def async_step_timed_out(self, user_input=None): diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index d3a3a866361..0d512101e11 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -21,6 +21,7 @@ PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" +CONF_CLIENT_IDENTIFIER = "client_id" CONF_SERVER = "server" CONF_SERVER_IDENTIFIER = "server_id" CONF_USE_EPISODE_ART = "use_episode_art" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index d4f2ae0517a..3c570a0e64c 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ "plexapi==3.0.6", - "plexauth==0.0.4" + "plexauth==0.0.5" ], "dependencies": [ "http" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index d7825ae82c3..c0461ee0f54 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -12,6 +12,7 @@ from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( + CONF_CLIENT_IDENTIFIER, CONF_SERVER, CONF_SHOW_ALL_CONTROLS, CONF_USE_EPISODE_ART, @@ -33,8 +34,6 @@ plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT plexapi.X_PLEX_VERSION = X_PLEX_VERSION -plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers() -plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() class PlexServer: @@ -52,6 +51,12 @@ class PlexServer: self.options = options self.server_choice = None + # Header conditionally added as it is not available in config entry v1 + if CONF_CLIENT_IDENTIFIER in server_config: + plexapi.X_PLEX_IDENTIFIER = server_config[CONF_CLIENT_IDENTIFIER] + plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers() + plexapi.server.BASE_HEADERS = plexapi.reset_base_headers() + def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" diff --git a/requirements_all.txt b/requirements_all.txt index 868855ed851..207b955f249 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -971,7 +971,7 @@ pizzapi==0.0.3 plexapi==3.0.6 # homeassistant.components.plex -plexauth==0.0.4 +plexauth==0.0.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2fd91e21d96..e61732e632b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,7 +343,7 @@ pillow==6.2.0 plexapi==3.0.6 # homeassistant.components.plex -plexauth==0.0.4 +plexauth==0.0.5 # homeassistant.components.mhz19 # homeassistant.components.serial_pm From 44bf9e9ddc74458887a0f1376f793a697421e5ed Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 23 Oct 2019 01:34:12 -0500 Subject: [PATCH 599/639] Additional SSL validation checks for cert_expiry (#28047) * Additional SSL validation checks * Add validity attribute, log errors on import * Don't log from sensor --- .../components/cert_expiry/config_flow.py | 21 ++++++++++++++++--- .../components/cert_expiry/sensor.py | 20 +++++++++++++----- .../components/cert_expiry/strings.json | 3 ++- .../cert_expiry/test_config_flow.py | 20 ++++++++++++++++-- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/cert_expiry/config_flow.py b/homeassistant/components/cert_expiry/config_flow.py index 43931fe5830..78450d247b9 100644 --- a/homeassistant/components/cert_expiry/config_flow.py +++ b/homeassistant/components/cert_expiry/config_flow.py @@ -1,5 +1,7 @@ """Config flow for the Cert Expiry platform.""" +import logging import socket +import ssl import voluptuous as vol from homeassistant import config_entries @@ -9,6 +11,8 @@ from homeassistant.core import HomeAssistant, callback from .const import DOMAIN, DEFAULT_PORT, DEFAULT_NAME from .helper import get_cert +_LOGGER = logging.getLogger(__name__) + @callback def certexpiry_entries(hass: HomeAssistant): @@ -39,17 +43,28 @@ class CertexpiryConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _test_connection(self, user_input=None): """Test connection to the server and try to get the certtificate.""" + host = user_input[CONF_HOST] try: await self.hass.async_add_executor_job( - get_cert, user_input[CONF_HOST], user_input.get(CONF_PORT, DEFAULT_PORT) + get_cert, host, user_input.get(CONF_PORT, DEFAULT_PORT) ) return True except socket.gaierror: + _LOGGER.error("Host cannot be resolved: %s", host) self._errors[CONF_HOST] = "resolve_failed" except socket.timeout: + _LOGGER.error("Timed out connecting to %s", host) self._errors[CONF_HOST] = "connection_timeout" - except OSError: - self._errors[CONF_HOST] = "certificate_fetch_failed" + except ssl.CertificateError as err: + if "doesn't match" in err.args[0]: + _LOGGER.error("Certificate does not match host: %s", host) + self._errors[CONF_HOST] = "wrong_host" + else: + _LOGGER.error("Certificate could not be validated: %s", host) + self._errors[CONF_HOST] = "certificate_error" + except ssl.SSLError: + _LOGGER.error("Certificate could not be validated: %s", host) + self._errors[CONF_HOST] = "certificate_error" return False async def async_step_user(self, user_input=None): diff --git a/homeassistant/components/cert_expiry/sensor.py b/homeassistant/components/cert_expiry/sensor.py index 2d578ef2c3b..3022c7bd42b 100644 --- a/homeassistant/components/cert_expiry/sensor.py +++ b/homeassistant/components/cert_expiry/sensor.py @@ -70,6 +70,7 @@ class SSLCertificate(Entity): self._name = sensor_name self._state = None self._available = False + self._valid = False @property def name(self): @@ -122,16 +123,17 @@ class SSLCertificate(Entity): except socket.gaierror: _LOGGER.error("Cannot resolve hostname: %s", self.server_name) self._available = False + self._valid = False return except socket.timeout: _LOGGER.error("Connection timeout with server: %s", self.server_name) self._available = False + self._valid = False return - except OSError: - _LOGGER.error( - "Cannot fetch certificate from %s", self.server_name, exc_info=1 - ) - self._available = False + except (ssl.CertificateError, ssl.SSLError): + self._available = True + self._state = 0 + self._valid = False return ts_seconds = ssl.cert_time_to_seconds(cert["notAfter"]) @@ -139,3 +141,11 @@ class SSLCertificate(Entity): expiry = timestamp - datetime.today() self._available = True self._state = expiry.days + self._valid = True + + @property + def device_state_attributes(self): + """Return additional sensor state attributes.""" + attr = {"is_valid": self._valid} + + return attr diff --git a/homeassistant/components/cert_expiry/strings.json b/homeassistant/components/cert_expiry/strings.json index 3e2fea2342e..e5e670d214f 100644 --- a/homeassistant/components/cert_expiry/strings.json +++ b/homeassistant/components/cert_expiry/strings.json @@ -15,7 +15,8 @@ "host_port_exists": "This host and port combination is already configured", "resolve_failed": "This host can not be resolved", "connection_timeout": "Timeout when connecting to this host", - "certificate_fetch_failed": "Can not fetch certificate from this host and port combination" + "certificate_error": "Certificate could not be validated", + "wrong_host": "Certificate does not match hostname" }, "abort": { "host_port_exists": "This host and port combination is already configured" diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index 988f3e97106..3754551c230 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the Cert Expiry config flow.""" import pytest +import ssl import socket from unittest.mock import patch @@ -131,7 +132,22 @@ async def test_abort_on_socket_failed(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {CONF_HOST: "connection_timeout"} - with patch("socket.create_connection", side_effect=OSError()): + with patch( + "socket.create_connection", + side_effect=ssl.CertificateError(f"{HOST} doesn't match somethingelse.com"), + ): result = await flow.async_step_user({CONF_HOST: HOST}) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_HOST: "certificate_fetch_failed"} + assert result["errors"] == {CONF_HOST: "wrong_host"} + + with patch( + "socket.create_connection", side_effect=ssl.CertificateError("different error") + ): + result = await flow.async_step_user({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "certificate_error"} + + with patch("socket.create_connection", side_effect=ssl.SSLError()): + result = await flow.async_step_user({CONF_HOST: HOST}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {CONF_HOST: "certificate_error"} From 852cbad965981531c443580631e248312d2244b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per-=C3=98yvind=20Bruun?= Date: Wed, 23 Oct 2019 09:32:14 +0200 Subject: [PATCH 600/639] New platform for Microsoft Teams (#27981) * New Microsoft Teams notification service * Updated codeowners * Updated requirements_all * Changed from WEBHOOK_ID to URL * Moved try/except block --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/msteams/__init__.py | 1 + .../components/msteams/manifest.json | 8 +++ homeassistant/components/msteams/notify.py | 67 +++++++++++++++++++ requirements_all.txt | 3 + 6 files changed, 81 insertions(+) create mode 100644 homeassistant/components/msteams/__init__.py create mode 100644 homeassistant/components/msteams/manifest.json create mode 100644 homeassistant/components/msteams/notify.py diff --git a/.coveragerc b/.coveragerc index f34b0baac17..83e6971cc6a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -418,6 +418,7 @@ omit = homeassistant/components/mpchc/media_player.py homeassistant/components/mpd/media_player.py homeassistant/components/mqtt_room/sensor.py + homeassistant/components/msteams/notify.py homeassistant/components/mvglive/sensor.py homeassistant/components/mychevy/* homeassistant/components/mycroft/* diff --git a/CODEOWNERS b/CODEOWNERS index a5ad222323b..eb29ee28915 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -190,6 +190,7 @@ homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core +homeassistant/components/msteams/* @peroyvind homeassistant/components/mysensors/* @MartinHjelmare homeassistant/components/mystrom/* @fabaff homeassistant/components/neato/* @dshokouhi @Santobert diff --git a/homeassistant/components/msteams/__init__.py b/homeassistant/components/msteams/__init__.py new file mode 100644 index 00000000000..42423887fa6 --- /dev/null +++ b/homeassistant/components/msteams/__init__.py @@ -0,0 +1 @@ +"""The Microsoft Teams component.""" diff --git a/homeassistant/components/msteams/manifest.json b/homeassistant/components/msteams/manifest.json new file mode 100644 index 00000000000..f907cf570bb --- /dev/null +++ b/homeassistant/components/msteams/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "msteams", + "name": "Microsoft Teams", + "documentation": "https://www.home-assistant.io/integrations/msteams", + "requirements": ["pymsteams==0.1.12"], + "dependencies": [], + "codeowners": ["@peroyvind"] +} diff --git a/homeassistant/components/msteams/notify.py b/homeassistant/components/msteams/notify.py new file mode 100644 index 00000000000..c986f1d2363 --- /dev/null +++ b/homeassistant/components/msteams/notify.py @@ -0,0 +1,67 @@ +"""Microsoft Teams platform for notify component.""" +import logging + +import pymsteams +import voluptuous as vol + +from homeassistant.components.notify import ( + ATTR_DATA, + ATTR_TITLE, + ATTR_TITLE_DEFAULT, + PLATFORM_SCHEMA, + BaseNotificationService, +) +from homeassistant.const import CONF_URL +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +ATTR_FILE_URL = "image_url" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_URL): cv.url}) + + +def get_service(hass, config, discovery_info=None): + """Get the Microsoft Teams notification service.""" + webhook_url = config.get(CONF_URL) + + try: + return MSTeamsNotificationService(webhook_url) + + except RuntimeError as err: + _LOGGER.exception("Error in creating a new Microsoft Teams message: %s", err) + return None + + +class MSTeamsNotificationService(BaseNotificationService): + """Implement the notification service for Microsoft Teams.""" + + def __init__(self, webhook_url): + """Initialize the service.""" + self._webhook_url = webhook_url + self.teams_message = pymsteams.connectorcard(self._webhook_url) + + def send_message(self, message=None, **kwargs): + """Send a message to the webhook.""" + title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) + data = kwargs.get(ATTR_DATA) + + self.teams_message.title(title) + + self.teams_message.text(message) + + if data is not None: + file_url = data.get(ATTR_FILE_URL) + + if file_url is not None: + if not file_url.startswith("http"): + _LOGGER.error("URL should start with http or https") + return + + message_section = pymsteams.cardsection() + message_section.addImage(file_url) + self.teams_message.addSection(message_section) + try: + self.teams_message.send() + except RuntimeError as err: + _LOGGER.error("Could not send notification. Error: %s", err) diff --git a/requirements_all.txt b/requirements_all.txt index 207b955f249..8f22d229f56 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1318,6 +1318,9 @@ pymodbus==1.5.2 # homeassistant.components.monoprice pymonoprice==0.3 +# homeassistant.components.msteams +pymsteams==0.1.12 + # homeassistant.components.yamaha_musiccast pymusiccast==0.1.6 From 09b322b8a4d6053d1348b3fd172d30723a608519 Mon Sep 17 00:00:00 2001 From: rolfberkenbosch <30292281+rolfberkenbosch@users.noreply.github.com> Date: Wed, 23 Oct 2019 15:49:47 +0200 Subject: [PATCH 601/639] Fix issues with new tile 2020 devices (#28133) * Update meteoalertapi to version 0.1.6 * Fix tile to supporting 2020 tile devices --- homeassistant/components/tile/device_tracker.py | 8 +++++--- homeassistant/components/tile/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index e8ed5b06d27..924fa913d30 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -43,7 +43,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_scanner(hass, config, async_see, discovery_info=None): """Validate the configuration and return a Tile scanner.""" - from pytile import Client + from pytile import async_login websession = aiohttp_client.async_get_clientsession(hass) @@ -52,14 +52,16 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): ) config_data = await hass.async_add_job(load_json, config_file) if config_data: - client = Client( + client = await async_login( config[CONF_USERNAME], config[CONF_PASSWORD], websession, client_uuid=config_data["client_uuid"], ) else: - client = Client(config[CONF_USERNAME], config[CONF_PASSWORD], websession) + client = await async_login( + config[CONF_USERNAME], config[CONF_PASSWORD], websession + ) config_data = {"client_uuid": client.client_uuid} await hass.async_add_job(save_json, config_file, config_data) diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 5e40c89369a..0dd0b70ef52 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,7 +3,7 @@ "name": "Tile", "documentation": "https://www.home-assistant.io/integrations/tile", "requirements": [ - "pytile==2.0.6" + "pytile==3.0.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8f22d229f56..cbe4a93f812 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1600,7 +1600,7 @@ python_opendata_transport==0.1.4 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==2.0.6 +pytile==3.0.0 # homeassistant.components.touchline pytouchline==0.7 From b1a374062beca1fee0f3b14a0b81881a7a4ab239 Mon Sep 17 00:00:00 2001 From: Alain Turbide <7193213+Dilbert66@users.noreply.github.com> Date: Wed, 23 Oct 2019 11:28:23 -0400 Subject: [PATCH 602/639] Add Alexa.ChannelController functions for media players (#27671) * Added missing Alexa.ChannelController functions. Specifically ChangeChannel and SkipChannel commands. These functions will call the play_media function in a media_player app if it has the capability published and pass on the channel# or channel name. The selected media player can then use this to select the channel on the device it is associated to. Modified the existing Alexa.StepSpeaker Setvolume function to actually do a stepped volume change using the steps sent by Alexa. The Alexa default step of 10 for a simple volume up/down can be changed via an exposed media_player attribute called volume_step_default. The default is set to 1. Any other value then default will be sent as sequential volume up /down to the media_player. * The test code has some weird behaviour with passed boolean values. Had to surround them in quotes for the tests to pass properly. * Reverted test_smart_home.py change. Issue was not the boolean value but the behavior in the handler. The test suite does not like multiple await calls in a loop. Will investigate further. The handler code works though. * Added ChannelController/SkipChannels test in test_smart_home.py Added test for callSign payload attribute. * Modified smart home test to allow more than one call to services * Added more tests for ChannelChange functions for various payload options. Removed name options from metadata payload section. not needed. * Reverted assert call change in alexa test __init__.py back to ==1. Not sure if it was the cause of the pytest's failing on github * Corrected a comment. First commit after a rebase. * Comment line change. Also wanted to force a code check on github. * Added a loop delay in StepSpeaker and SkipChannel functions for safety * Removed uneeded sleep from for loops. Let remote handle delays Moved service type decision out of for loops in ChannelController and StepSpeaker Used constants instead of numeric values for support options in test module * Change media_player const import to be more specific in source * Modifed test_smart_home to use media_play constants instead of hardcode valu * Removed unecessary test volume_step_default attribute from test_smart_home * Removed uneeded comment in StepSpeaker function. Re-ordered constants in test_smart_home.py * Modified call to media_player play_media service to use media_player constant instead of hard coded value. * Changed constant use to be consistant with rest of function. * Correct merge conflicts in handlers.py and capablities.py --- .../components/alexa/capabilities.py | 11 ++ homeassistant/components/alexa/entities.py | 4 + homeassistant/components/alexa/handlers.py | 106 ++++++++++++++++-- tests/components/alexa/test_smart_home.py | 93 ++++++++++++++- 4 files changed, 201 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index f4d93026649..246429ad6c9 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -1039,3 +1039,14 @@ class AlexaToggleController(AlexaCapability): ] return capability_resources + + +class AlexaChannelController(AlexaCapability): + """Implements Alexa.ChannelController. + + https://developer.amazon.com/docs/device-apis/alexa-channelcontroller.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.ChannelController" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index dd640aed0a6..f6fc9936a02 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -34,6 +34,7 @@ from homeassistant.components import ( from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES from .capabilities import ( AlexaBrightnessController, + AlexaChannelController, AlexaColorController, AlexaColorTemperatureController, AlexaContactSensor, @@ -420,6 +421,9 @@ class MediaPlayerCapabilities(AlexaEntity): if supported & media_player.SUPPORT_SELECT_SOURCE: yield AlexaInputController(self.entity) + if supported & media_player.const.SUPPORT_PLAY_MEDIA: + yield AlexaChannelController(self.entity) + @ENTITY_ADAPTERS.register(scene.DOMAIN) class SceneCapabilities(AlexaEntity): diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 64feacb92f5..331990dc4a4 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -521,20 +521,28 @@ async def async_api_adjust_volume_step(hass, config, directive, context): """Process an adjust volume step request.""" # media_player volume up/down service does not support specifying steps # each component handles it differently e.g. via config. - # For now we use the volumeSteps returned to figure out if we - # should step up/down - volume_step = directive.payload["volumeSteps"] + # This workaround will simply call the volume up/Volume down the amount of steps asked for + # When no steps are called in the request, Alexa sends a default of 10 steps which for most + # purposes is too high. The default is set 1 in this case. entity = directive.entity + volume_int = int(directive.payload["volumeSteps"]) + is_default = bool(directive.payload["volumeStepsDefault"]) + default_steps = 1 + + if volume_int < 0: + service_volume = SERVICE_VOLUME_DOWN + if is_default: + volume_int = -default_steps + else: + service_volume = SERVICE_VOLUME_UP + if is_default: + volume_int = default_steps data = {ATTR_ENTITY_ID: entity.entity_id} - if volume_step > 0: + for _ in range(0, abs(volume_int)): await hass.services.async_call( - entity.domain, SERVICE_VOLUME_UP, data, blocking=False, context=context - ) - elif volume_step < 0: - await hass.services.async_call( - entity.domain, SERVICE_VOLUME_DOWN, data, blocking=False, context=context + entity.domain, service_volume, data, blocking=False, context=context ) return directive.response() @@ -546,7 +554,6 @@ async def async_api_set_mute(hass, config, directive, context): """Process a set mute request.""" mute = bool(directive.payload["mute"]) entity = directive.entity - data = { ATTR_ENTITY_ID: entity.entity_id, media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute, @@ -1082,3 +1089,82 @@ async def async_api_adjust_range(hass, config, directive, context): ) return directive.response() + + +@HANDLERS.register(("Alexa.ChannelController", "ChangeChannel")) +async def async_api_changechannel(hass, config, directive, context): + """Process a change channel request.""" + channel = "0" + entity = directive.entity + payload = directive.payload["channel"] + payload_name = "number" + + if "number" in payload: + channel = payload["number"] + payload_name = "number" + elif "callSign" in payload: + channel = payload["callSign"] + payload_name = "callSign" + elif "affiliateCallSign" in payload: + channel = payload["affiliateCallSign"] + payload_name = "affiliateCallSign" + elif "uri" in payload: + channel = payload["uri"] + payload_name = "uri" + + data = { + ATTR_ENTITY_ID: entity.entity_id, + media_player.const.ATTR_MEDIA_CONTENT_ID: channel, + media_player.const.ATTR_MEDIA_CONTENT_TYPE: media_player.const.MEDIA_TYPE_CHANNEL, + } + + await hass.services.async_call( + entity.domain, + media_player.const.SERVICE_PLAY_MEDIA, + data, + blocking=False, + context=context, + ) + + response = directive.response() + + response.add_context_property( + { + "namespace": "Alexa.ChannelController", + "name": "channel", + "value": {payload_name: channel}, + } + ) + + return response + + +@HANDLERS.register(("Alexa.ChannelController", "SkipChannels")) +async def async_api_skipchannel(hass, config, directive, context): + """Process a skipchannel request.""" + channel = int(directive.payload["channelCount"]) + entity = directive.entity + + data = {ATTR_ENTITY_ID: entity.entity_id} + + if channel < 0: + service_media = SERVICE_MEDIA_PREVIOUS_TRACK + else: + service_media = SERVICE_MEDIA_NEXT_TRACK + + for _ in range(0, abs(channel)): + await hass.services.async_call( + entity.domain, service_media, data, blocking=False, context=context + ) + + response = directive.response() + + response.add_context_property( + { + "namespace": "Alexa.ChannelController", + "name": "channel", + "value": {"number": ""}, + } + ) + + return response diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 5a39036a30f..139c8c9740b 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -4,6 +4,19 @@ import pytest from homeassistant.core import Context, callback from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.components.alexa import smart_home, messages +from homeassistant.components.media_player.const import ( + SUPPORT_NEXT_TRACK, + SUPPORT_PAUSE, + SUPPORT_PLAY, + SUPPORT_PLAY_MEDIA, + SUPPORT_PREVIOUS_TRACK, + SUPPORT_SELECT_SOURCE, + SUPPORT_STOP, + SUPPORT_TURN_OFF, + SUPPORT_TURN_ON, + SUPPORT_VOLUME_MUTE, + SUPPORT_VOLUME_SET, +) from homeassistant.helpers import entityfilter from tests.common import async_mock_service @@ -693,7 +706,17 @@ async def test_media_player(hass): "off", { "friendly_name": "Test media player", - "supported_features": 0x59BD, + "supported_features": SUPPORT_NEXT_TRACK + | SUPPORT_PAUSE + | SUPPORT_PLAY + | SUPPORT_PLAY_MEDIA + | SUPPORT_PREVIOUS_TRACK + | SUPPORT_SELECT_SOURCE + | SUPPORT_STOP + | SUPPORT_TURN_OFF + | SUPPORT_TURN_ON + | SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET, "volume_level": 0.75, }, ) @@ -711,6 +734,7 @@ async def test_media_player(hass): "Alexa.StepSpeaker", "Alexa.PlaybackController", "Alexa.EndpointHealth", + "Alexa.ChannelController", ) await assert_power_controller_works( @@ -824,7 +848,7 @@ async def test_media_player(hass): "media_player#test", "media_player.volume_up", hass, - payload={"volumeSteps": 20}, + payload={"volumeSteps": 1, "volumeStepsDefault": False}, ) call, _ = await assert_request_calls_service( @@ -833,7 +857,69 @@ async def test_media_player(hass): "media_player#test", "media_player.volume_down", hass, - payload={"volumeSteps": -20}, + payload={"volumeSteps": -1, "volumeStepsDefault": False}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.StepSpeaker", + "AdjustVolume", + "media_player#test", + "media_player.volume_up", + hass, + payload={"volumeSteps": 10, "volumeStepsDefault": True}, + ) + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"number": 24}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"callSign": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"affiliateCallSign": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "ChangeChannel", + "media_player#test", + "media_player.play_media", + hass, + payload={"channel": {"uri": "ABC"}}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "SkipChannels", + "media_player#test", + "media_player.media_next_track", + hass, + payload={"channelCount": 1}, + ) + + call, _ = await assert_request_calls_service( + "Alexa.ChannelController", + "SkipChannels", + "media_player#test", + "media_player.media_previous_track", + hass, + payload={"channelCount": -1}, ) @@ -862,6 +948,7 @@ async def test_media_player_power(hass): "Alexa.StepSpeaker", "Alexa.PlaybackController", "Alexa.EndpointHealth", + "Alexa.ChannelController", ) await assert_request_calls_service( From 14be60e5bf70d92a8e8a9555e9a0ce42b3cb8318 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Wed, 23 Oct 2019 15:30:38 +0000 Subject: [PATCH 603/639] Move imports in nuheat component (#28038) * Move imports in nuheat component * Fix tox tests * Fix tox tests * Update tests/components/nuheat/test_init.py @Balloob suggested the change because direct replacement, the mock would never be reverted and impact the other tests. Co-Authored-By: Paulus Schoutsen --- homeassistant/components/nuheat/__init__.py | 8 +++----- homeassistant/components/nuheat/climate.py | 2 +- tests/components/nuheat/test_init.py | 9 +++++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index f83611d3e40..88e10270d18 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -1,11 +1,11 @@ """Support for NuHeat thermostats.""" import logging +import nuheat import voluptuous as vol -from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_DEVICES -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import config_validation as cv, discovery _LOGGER = logging.getLogger(__name__) @@ -29,8 +29,6 @@ CONFIG_SCHEMA = vol.Schema( def setup(hass, config): """Set up the NuHeat thermostat component.""" - import nuheat - conf = config[DOMAIN] username = conf.get(CONF_USERNAME) password = conf.get(CONF_PASSWORD) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 19780a35a20..5a4e4e233d1 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -9,9 +9,9 @@ from homeassistant.components.climate.const import ( HVAC_MODE_AUTO, HVAC_MODE_HEAT, HVAC_MODE_OFF, + PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, - PRESET_NONE, ) from homeassistant.const import ( ATTR_ENTITY_ID, diff --git a/tests/components/nuheat/test_init.py b/tests/components/nuheat/test_init.py index a6be3ac14f5..90a209fd897 100644 --- a/tests/components/nuheat/test_init.py +++ b/tests/components/nuheat/test_init.py @@ -1,11 +1,11 @@ """NuHeat component tests.""" import unittest - from unittest.mock import patch -from tests.common import get_test_home_assistant, MockDependency from homeassistant.components import nuheat +from tests.common import MockDependency, get_test_home_assistant + VALID_CONFIG = { "nuheat": {"username": "warm", "password": "feet", "devices": "thermostat123"} } @@ -27,11 +27,12 @@ class TestNuHeat(unittest.TestCase): @patch("homeassistant.helpers.discovery.load_platform") def test_setup(self, mocked_nuheat, mocked_load): """Test setting up the NuHeat component.""" - nuheat.setup(self.hass, self.config) + with patch.object(nuheat, "nuheat", mocked_nuheat): + nuheat.setup(self.hass, self.config) mocked_nuheat.NuHeat.assert_called_with("warm", "feet") assert nuheat.DOMAIN in self.hass.data - assert 2 == len(self.hass.data[nuheat.DOMAIN]) + assert len(self.hass.data[nuheat.DOMAIN]) == 2 assert isinstance( self.hass.data[nuheat.DOMAIN][0], type(mocked_nuheat.NuHeat()) ) From 4d8539e15128d97b5872d384168052b5ff94b2c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diefferson=20Koderer=20M=C3=B4ro?= Date: Wed, 23 Oct 2019 15:53:05 +0000 Subject: [PATCH 604/639] Move imports in raspihats component (#28088) * Move imports in raspihats component * Comment require * Disable pylint for import-error * Revert move imports * Remove unnecessary pylint disable error * Update homeassistant/components/raspihats/__init__.py Co-Authored-By: cgtobi * Apply suggestions from code review Co-Authored-By: cgtobi --- .../components/raspihats/__init__.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/raspihats/__init__.py b/homeassistant/components/raspihats/__init__.py index 963c2624362..8b7ea0a38d7 100644 --- a/homeassistant/components/raspihats/__init__.py +++ b/homeassistant/components/raspihats/__init__.py @@ -120,7 +120,9 @@ class I2CHatsManager(threading.Thread): with self._lock: i2c_hat = self._i2c_hats.get(address) if i2c_hat is None: - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error import raspihats.i2c_hats as module constructor = getattr(module, board) @@ -138,7 +140,9 @@ class I2CHatsManager(threading.Thread): def run(self): """Keep alive for I2C-HATs.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException _LOGGER.info(log_message(self, "starting")) @@ -199,7 +203,9 @@ class I2CHatsManager(threading.Thread): def read_di(self, address, channel): """Read a value from a I2C-HAT digital input.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException with self._lock: @@ -212,7 +218,9 @@ class I2CHatsManager(threading.Thread): def write_dq(self, address, channel, value): """Write a value to a I2C-HAT digital output.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException with self._lock: @@ -224,7 +232,9 @@ class I2CHatsManager(threading.Thread): def read_dq(self, address, channel): """Read a value from a I2C-HAT digital output.""" - # pylint: disable=import-error,no-name-in-module + # This is a Pi module and can't be installed in CI without + # breaking the build. + # pylint: disable=import-outside-toplevel,import-error from raspihats.i2c_hats import ResponseException with self._lock: From 7431e2675232e8c84e7aee681aeed45723f5162e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 23 Oct 2019 18:57:51 +0300 Subject: [PATCH 605/639] Round system monitor load averages to 2 decimal digits (#27558) On a Raspberry Pi 3, Python 3.7.4: # python3 -c "import os; print(os.getloadavg())" (0.2724609375, 0.3505859375, 0.3515625) --- homeassistant/components/systemmonitor/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index b4621c59798..53c5c104cd1 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -218,8 +218,8 @@ class SystemMonitorSensor(Entity): dt_util.utc_from_timestamp(psutil.boot_time()) ).isoformat() elif self.type == "load_1m": - self._state = os.getloadavg()[0] + self._state = round(os.getloadavg()[0], 2) elif self.type == "load_5m": - self._state = os.getloadavg()[1] + self._state = round(os.getloadavg()[1], 2) elif self.type == "load_15m": - self._state = os.getloadavg()[2] + self._state = round(os.getloadavg()[2], 2) From efae75103ac124f5391c435328999ac171c13702 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Wed, 23 Oct 2019 18:21:49 +0200 Subject: [PATCH 606/639] Cleanup typing and asserts for HomematicIP Cloud (#28144) * Cleanup assert in Homematic IP Cloud Tests * Cleanup typing --- .../components/homematicip_cloud/__init__.py | 7 ++-- .../homematicip_cloud/alarm_control_panel.py | 4 +-- .../homematicip_cloud/binary_sensor.py | 4 +-- .../components/homematicip_cloud/climate.py | 4 +-- .../homematicip_cloud/config_flow.py | 5 +-- .../components/homematicip_cloud/cover.py | 4 +-- .../components/homematicip_cloud/device.py | 6 ++-- .../components/homematicip_cloud/hap.py | 9 +++--- .../components/homematicip_cloud/light.py | 4 +-- .../components/homematicip_cloud/sensor.py | 4 +-- .../components/homematicip_cloud/switch.py | 4 +-- .../components/homematicip_cloud/weather.py | 4 +-- .../components/homematicip_cloud/conftest.py | 32 +++++++++++-------- .../components/homematicip_cloud/test_hap.py | 22 ++++++------- 14 files changed, 59 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 9a3191ac168..9a0eb65aa3f 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -7,11 +7,10 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME -from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import comp_entity_ids -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .config_flow import configured_haps from .const import ( @@ -98,7 +97,7 @@ SCHEMA_SET_ACTIVE_CLIMATE_PROFILE = vol.Schema( ) -async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" hass.data[DOMAIN] = {} @@ -252,7 +251,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up an access point from a config entry.""" hap = HomematicipHAP(hass, entry) hapid = entry.data[HMIPC_HAPID].replace("-", "").upper() diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 653c1ae147b..f61bf6f6b56 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -12,7 +12,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID from .hap import HomematicipHAP @@ -28,7 +28,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP alrm control panel from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 964ab4d8234..e308f96c208 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -36,7 +36,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_GROUP_MEMBER_UNREACHABLE @@ -82,7 +82,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud binary sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index f1f414169f6..74d647c8c33 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -20,7 +20,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .hap import HomematicipHAP @@ -41,7 +41,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP climate from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/config_flow.py b/homeassistant/components/homematicip_cloud/config_flow.py index a94ea7b53f1..1488f02f13b 100644 --- a/homeassistant/components/homematicip_cloud/config_flow.py +++ b/homeassistant/components/homematicip_cloud/config_flow.py @@ -4,7 +4,8 @@ from typing import Set import voluptuous as vol from homeassistant import config_entries -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback +from homeassistant.helpers.typing import HomeAssistantType from .const import ( _LOGGER, @@ -18,7 +19,7 @@ from .hap import HomematicipAuth @callback -def configured_haps(hass: HomeAssistant) -> Set[str]: +def configured_haps(hass: HomeAssistantType) -> Set[str]: """Return a set of the configured access points.""" return set( entry.data[HMIPC_HAPID] diff --git a/homeassistant/components/homematicip_cloud/cover.py b/homeassistant/components/homematicip_cloud/cover.py index c5821c4f75e..63ac6f7310c 100644 --- a/homeassistant/components/homematicip_cloud/cover.py +++ b/homeassistant/components/homematicip_cloud/cover.py @@ -10,7 +10,7 @@ from homeassistant.components.cover import ( CoverDevice, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice @@ -28,7 +28,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP cover from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 3d64014883d..b05c0e06928 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -5,11 +5,11 @@ from typing import Optional from homematicip.aio.device import AsyncDevice from homematicip.aio.group import AsyncGroup -from homeassistant.components import homematicip_cloud from homeassistant.core import callback from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity import Entity +from .const import DOMAIN as HMIPC_DOMAIN from .hap import HomematicipHAP _LOGGER = logging.getLogger(__name__) @@ -70,13 +70,13 @@ class HomematicipGenericDevice(Entity): return { "identifiers": { # Serial numbers of Homematic IP device - (homematicip_cloud.DOMAIN, self._device.id) + (HMIPC_DOMAIN, self._device.id) }, "name": self._device.label, "manufacturer": self._device.oem, "model": self._device.modelType, "sw_version": self._device.firmwareVersion, - "via_device": (homematicip_cloud.DOMAIN, self._device.homeId), + "via_device": (HMIPC_DOMAIN, self._device.homeId), } return None diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 64fbd4fd079..bef04180c6f 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -8,9 +8,10 @@ from homematicip.base.base_connection import HmipConnectionError from homematicip.base.enums import EventType from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import HomeAssistantType from .const import COMPONENTS, HMIPC_AUTHTOKEN, HMIPC_HAPID, HMIPC_NAME, HMIPC_PIN from .errors import HmipcConnectionError @@ -53,7 +54,7 @@ class HomematicipAuth: except HmipConnectionError: return False - async def get_auth(self, hass: HomeAssistant, hapid, pin): + async def get_auth(self, hass: HomeAssistantType, hapid, pin): """Create a HomematicIP access point object.""" auth = AsyncAuth(hass.loop, async_get_clientsession(hass)) try: @@ -69,7 +70,7 @@ class HomematicipAuth: class HomematicipHAP: """Manages HomematicIP HTTP and WebSocket connection.""" - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + def __init__(self, hass: HomeAssistantType, config_entry: ConfigEntry) -> None: """Initialize HomematicIP Cloud connection.""" self.hass = hass self.config_entry = config_entry @@ -224,7 +225,7 @@ class HomematicipHAP: return True async def get_hap( - self, hass: HomeAssistant, hapid: str, authtoken: str, name: str + self, hass: HomeAssistantType, hapid: str, authtoken: str, name: str ) -> AsyncHome: """Create a HomematicIP access point object.""" home = AsyncHome(hass.loop, async_get_clientsession(hass)) diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index bc704e2ef06..46a8d95729f 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -21,7 +21,7 @@ from homeassistant.components.light import ( Light, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .hap import HomematicipHAP @@ -38,7 +38,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud lights from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/sensor.py b/homeassistant/components/homematicip_cloud/sensor.py index 18d483f6adf..9caa72ba15f 100644 --- a/homeassistant/components/homematicip_cloud/sensor.py +++ b/homeassistant/components/homematicip_cloud/sensor.py @@ -31,7 +31,7 @@ from homeassistant.const import ( POWER_WATT, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_IS_GROUP, ATTR_MODEL_TYPE @@ -52,7 +52,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP Cloud sensors from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 3b54d3fc279..dae6019b378 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -15,7 +15,7 @@ from homematicip.aio.group import AsyncSwitchingGroup from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .device import ATTR_GROUP_MEMBER_UNREACHABLE @@ -30,7 +30,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP switch from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/homeassistant/components/homematicip_cloud/weather.py b/homeassistant/components/homematicip_cloud/weather.py index 6b92b639c7a..5aa3f28c45d 100644 --- a/homeassistant/components/homematicip_cloud/weather.py +++ b/homeassistant/components/homematicip_cloud/weather.py @@ -11,7 +11,7 @@ from homematicip.base.enums import WeatherCondition from homeassistant.components.weather import WeatherEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import TEMP_CELSIUS -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import HomeAssistantType from . import DOMAIN as HMIPC_DOMAIN, HMIPC_HAPID, HomematicipGenericDevice from .hap import HomematicipHAP @@ -43,7 +43,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities ) -> None: """Set up the HomematicIP weather sensor from a config entry.""" hap = hass.data[HMIPC_DOMAIN][config_entry.data[HMIPC_HAPID]] diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index b2fc53a28ec..f60f8d659b5 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -12,7 +12,7 @@ from homeassistant.components.homematicip_cloud import ( const as hmipc, hap as hmip_hap, ) -from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeTemplate @@ -20,7 +20,7 @@ from tests.common import MockConfigEntry, mock_coro @pytest.fixture(name="mock_connection") -def mock_connection_fixture(): +def mock_connection_fixture() -> AsyncConnection: """Return a mocked connection.""" connection = MagicMock(spec=AsyncConnection) @@ -35,7 +35,7 @@ def mock_connection_fixture(): @pytest.fixture(name="hmip_config_entry") -def hmip_config_entry_fixture(): +def hmip_config_entry_fixture() -> config_entries.ConfigEntry: """Create a mock config entriy for homematic ip cloud.""" entry_data = { hmipc.HMIPC_HAPID: HAPID, @@ -57,20 +57,24 @@ def hmip_config_entry_fixture(): @pytest.fixture(name="default_mock_home") -def default_mock_home_fixture(mock_connection): +def default_mock_home_fixture(mock_connection) -> AsyncHome: """Create a fake homematic async home.""" return HomeTemplate(connection=mock_connection).init_home().get_async_home_mock() @pytest.fixture(name="default_mock_hap") async def default_mock_hap_fixture( - hass: HomeAssistant, mock_connection, hmip_config_entry -): + hass: HomeAssistantType, mock_connection, hmip_config_entry +) -> hmip_hap.HomematicipHAP: """Create a mocked homematic access point.""" return await get_mock_hap(hass, mock_connection, hmip_config_entry) -async def get_mock_hap(hass: HomeAssistant, mock_connection, hmip_config_entry): +async def get_mock_hap( + hass: HomeAssistantType, + mock_connection, + hmip_config_entry: config_entries.ConfigEntry, +) -> hmip_hap.HomematicipHAP: """Create a mocked homematic access point.""" hass.config.components.add(HMIPC_DOMAIN) hap = hmip_hap.HomematicipHAP(hass, hmip_config_entry) @@ -81,7 +85,7 @@ async def get_mock_hap(hass: HomeAssistant, mock_connection, hmip_config_entry): .get_async_home_mock() ) with patch.object(hap, "get_hap", return_value=mock_coro(mock_home)): - assert await hap.async_setup() is True + assert await hap.async_setup() mock_home.on_update(hap.async_update) mock_home.on_create(hap.async_create_entity) @@ -93,7 +97,7 @@ async def get_mock_hap(hass: HomeAssistant, mock_connection, hmip_config_entry): @pytest.fixture(name="hmip_config") -def hmip_config_fixture(): +def hmip_config_fixture() -> ConfigType: """Create a config for homematic ip cloud.""" entry_data = { @@ -107,15 +111,15 @@ def hmip_config_fixture(): @pytest.fixture(name="dummy_config") -def dummy_config_fixture(): +def dummy_config_fixture() -> ConfigType: """Create a dummy config.""" return {"blabla": None} @pytest.fixture(name="mock_hap_with_service") async def mock_hap_with_service_fixture( - hass: HomeAssistant, default_mock_hap, dummy_config -): + hass: HomeAssistantType, default_mock_hap, dummy_config +) -> hmip_hap.HomematicipHAP: """Create a fake homematic access point with hass services.""" await hmip_async_setup(hass, dummy_config) await hass.async_block_till_done() @@ -124,7 +128,7 @@ async def mock_hap_with_service_fixture( @pytest.fixture(name="simple_mock_home") -def simple_mock_home_fixture(): +def simple_mock_home_fixture() -> AsyncHome: """Return a simple AsyncHome Mock.""" return Mock( spec=AsyncHome, @@ -139,6 +143,6 @@ def simple_mock_home_fixture(): @pytest.fixture(name="simple_mock_auth") -def simple_mock_auth_fixture(): +def simple_mock_auth_fixture() -> AsyncAuth: """Return a simple AsyncAuth Mock.""" return Mock(spec=AsyncAuth, pin=HAPPIN, create=True) diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 90f557b1f93..324649ef515 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -31,7 +31,7 @@ async def test_auth_setup(hass): } hap = hmipc.HomematicipAuth(hass, config) with patch.object(hap, "get_auth", return_value=mock_coro()): - assert await hap.async_setup() is True + assert await hap.async_setup() async def test_auth_setup_connection_error(hass): @@ -43,7 +43,7 @@ async def test_auth_setup_connection_error(hass): } hap = hmipc.HomematicipAuth(hass, config) with patch.object(hap, "get_auth", side_effect=errors.HmipcConnectionError): - assert await hap.async_setup() is False + assert not await hap.async_setup() async def test_auth_auth_check_and_register(hass): @@ -62,7 +62,7 @@ async def test_auth_auth_check_and_register(hass): ), patch.object( hap.auth, "confirmAuthToken", return_value=mock_coro() ): - assert await hap.async_checkbutton() is True + assert await hap.async_checkbutton() assert await hap.async_register() == "ABC" @@ -78,7 +78,7 @@ async def test_auth_auth_check_and_register_with_exception(hass): with patch.object( hap.auth, "isRequestAcknowledged", side_effect=HmipConnectionError ), patch.object(hap.auth, "requestAuthToken", side_effect=HmipConnectionError): - assert await hap.async_checkbutton() is False + assert not await hap.async_checkbutton() assert await hap.async_register() is False @@ -94,7 +94,7 @@ async def test_hap_setup_works(aioclient_mock): } hap = hmipc.HomematicipHAP(hass, entry) with patch.object(hap, "get_hap", return_value=mock_coro(home)): - assert await hap.async_setup() is True + assert await hap.async_setup() assert hap.home is home assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 8 @@ -140,7 +140,7 @@ async def test_hap_reset_unloads_entry_if_setup(): } hap = hmipc.HomematicipHAP(hass, entry) with patch.object(hap, "get_hap", return_value=mock_coro(home)): - assert await hap.async_setup() is True + assert await hap.async_setup() assert hap.home is home assert not hass.services.async_register.mock_calls @@ -161,7 +161,7 @@ async def test_hap_create(hass, hmip_config_entry, simple_mock_home): "homeassistant.components.homematicip_cloud.hap.AsyncHome", return_value=simple_mock_home, ), patch.object(hap, "async_connect", return_value=mock_coro(None)): - assert await hap.async_setup() is True + assert await hap.async_setup() async def test_hap_create_exception(hass, hmip_config_entry, simple_mock_home): @@ -197,7 +197,7 @@ async def test_auth_create(hass, simple_mock_auth): "homeassistant.components.homematicip_cloud.hap.AsyncAuth", return_value=simple_mock_auth, ): - assert await hmip_auth.async_setup() is True + assert await hmip_auth.async_setup() await hass.async_block_till_done() assert hmip_auth.auth.pin == HAPPIN @@ -216,12 +216,12 @@ async def test_auth_create_exception(hass, simple_mock_auth): "homeassistant.components.homematicip_cloud.hap.AsyncAuth", return_value=simple_mock_auth, ): - assert await hmip_auth.async_setup() is True + assert await hmip_auth.async_setup() await hass.async_block_till_done() - assert hmip_auth.auth is False + assert not hmip_auth.auth with patch( "homeassistant.components.homematicip_cloud.hap.AsyncAuth", return_value=simple_mock_auth, ): - assert await hmip_auth.get_auth(hass, HAPID, HAPPIN) is False + assert not await hmip_auth.get_auth(hass, HAPID, HAPPIN) From 86700ec1ace0ed6453aa2243b33a9c6de616a1d0 Mon Sep 17 00:00:00 2001 From: Marius Flage Date: Wed, 23 Oct 2019 18:25:00 +0200 Subject: [PATCH 607/639] Avoid query operations on a pjlink powered off projector (#28132) --- homeassistant/components/pjlink/media_player.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/pjlink/media_player.py b/homeassistant/components/pjlink/media_player.py index 44b4055e032..6474165a6cd 100644 --- a/homeassistant/components/pjlink/media_player.py +++ b/homeassistant/components/pjlink/media_player.py @@ -105,10 +105,12 @@ class PjLinkDevice(MediaPlayerDevice): pwstate = projector.get_power() if pwstate in ("on", "warm-up"): self._pwstate = STATE_ON + self._muted = projector.get_mute()[1] + self._current_source = format_input_source(*projector.get_input()) else: self._pwstate = STATE_OFF - self._muted = projector.get_mute()[1] - self._current_source = format_input_source(*projector.get_input()) + self._muted = False + self._current_source = None except KeyError as err: if str(err) == "'OK'": self._pwstate = STATE_OFF From a3f3ea4e2529e3c7de253197b188b10813030aeb Mon Sep 17 00:00:00 2001 From: Jon Gilmore <7232986+JonGilmore@users.noreply.github.com> Date: Wed, 23 Oct 2019 11:29:30 -0500 Subject: [PATCH 608/639] Fix Lutron Pico (#27059) * removed a nesting level * Lutron Pico fix * Reverted logging change Was unaware that f-strings aren't used in logging commands, reverted the usage * Reverted logging change Was unaware that f-strings aren't used in logging commands, reverted the usage * fixed logic --- homeassistant/components/lutron/__init__.py | 25 +++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index de3ca40fd1d..09ab0fc747b 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -68,18 +68,19 @@ def setup(hass, base_config): hass.data[LUTRON_DEVICES]["switch"].append((area.name, output)) for keypad in area.keypads: for button in keypad.buttons: - # This is the best way to determine if a button does anything - # useful until pylutron is updated to provide information on - # which buttons actually control scenes. - for led in keypad.leds: - if ( - led.number == button.number - and button.name != "Unknown Button" - and button.button_type in ("SingleAction", "Toggle") - ): - hass.data[LUTRON_DEVICES]["scene"].append( - (area.name, keypad.name, button, led) - ) + # If the button has a function assigned to it, add it as a scene + if button.name != "Unknown Button" and button.button_type in ( + "SingleAction", + "Toggle", + ): + # Associate an LED with a button if there is one + led = next( + (led for led in keypad.leds if led.number == button.number), + None, + ) + hass.data[LUTRON_DEVICES]["scene"].append( + (area.name, keypad.name, button, led) + ) hass.data[LUTRON_BUTTONS].append(LutronButton(hass, keypad, button)) if area.occupancy_group is not None: From b8d2c58c6057744b97790ec820303c0db66047d6 Mon Sep 17 00:00:00 2001 From: libots <989623+libots@users.noreply.github.com> Date: Wed, 23 Oct 2019 13:41:58 -0400 Subject: [PATCH 609/639] Support for additional Abode timeline events (#28124) * Support for additional timeline events * Update __init__.py * Expose details on user These lines expose apptype and event_by, which can be used to give information on events initiated by keypad users (vs. users on the mobile app, web app, or those initiated from HA through abodepy). --- homeassistant/components/abode/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index aeba437aceb..6a72ac64145 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -44,6 +44,8 @@ ATTR_EVENT_TYPE = "event_type" ATTR_EVENT_UTC = "event_utc" ATTR_SETTING = "setting" ATTR_USER_NAME = "user_name" +ATTR_APP_TYPE = "app_type" +ATTR_EVENT_BY = "event_by" ATTR_VALUE = "value" ABODE_DEVICE_ID_LIST_SCHEMA = vol.Schema([str]) @@ -247,6 +249,8 @@ def setup_abode_events(hass): ATTR_EVENT_TYPE: event_json.get(ATTR_EVENT_TYPE, ""), ATTR_EVENT_UTC: event_json.get(ATTR_EVENT_UTC, ""), ATTR_USER_NAME: event_json.get(ATTR_USER_NAME, ""), + ATTR_APP_TYPE: event_json.get(ATTR_APP_TYPE, ""), + ATTR_EVENT_BY: event_json.get(ATTR_EVENT_BY, ""), ATTR_DATE: event_json.get(ATTR_DATE, ""), ATTR_TIME: event_json.get(ATTR_TIME, ""), } @@ -259,6 +263,12 @@ def setup_abode_events(hass): TIMELINE.PANEL_FAULT_GROUP, TIMELINE.PANEL_RESTORE_GROUP, TIMELINE.AUTOMATION_GROUP, + TIMELINE.DISARM_GROUP, + TIMELINE.ARM_GROUP, + TIMELINE.TEST_GROUP, + TIMELINE.CAPTURE_GROUP, + TIMELINE.DEVICE_GROUP, + TIMELINE.AUTOMATION_EDIT_GROUP, ] for event in events: From 85eefe41da44f814477741b20b5b26f4b0f9c4f2 Mon Sep 17 00:00:00 2001 From: Adrien Foulon <6115458+Tofandel@users.noreply.github.com> Date: Wed, 23 Oct 2019 19:44:47 +0200 Subject: [PATCH 610/639] Fix supported_features in mqtt cover (#28120) * Correctly compute the supported_features in cover.mqtt * Update homeassistant/components/mqtt/cover.py Co-Authored-By: Paulus Schoutsen * Correctly compute the supported_features in cover.mqtt * Format --- homeassistant/components/mqtt/cover.py | 9 +++++++-- tests/components/mqtt/test_cover.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 66e14ca9a5a..e6cfab90c26 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -90,7 +90,7 @@ DEFAULT_TILT_MIN = 0 DEFAULT_TILT_OPEN_POSITION = 100 DEFAULT_TILT_OPTIMISTIC = False -OPEN_CLOSE_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP +OPEN_CLOSE_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE TILT_FEATURES = ( SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT @@ -122,7 +122,9 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): cv.string, vol.Optional(CONF_PAYLOAD_OPEN, default=DEFAULT_PAYLOAD_OPEN): cv.string, - vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): cv.string, + vol.Optional(CONF_PAYLOAD_STOP, default=DEFAULT_PAYLOAD_STOP): vol.Any( + cv.string, None + ), vol.Optional(CONF_POSITION_CLOSED, default=DEFAULT_POSITION_CLOSED): int, vol.Optional(CONF_POSITION_OPEN, default=DEFAULT_POSITION_OPEN): int, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, @@ -396,6 +398,9 @@ class MqttCover( if self._config.get(CONF_COMMAND_TOPIC) is not None: supported_features = OPEN_CLOSE_FEATURES + if self._config.get(CONF_PAYLOAD_STOP) is not None: + supported_features |= SUPPORT_STOP + if self._config.get(CONF_SET_POSITION_TOPIC) is not None: supported_features |= SUPPORT_SET_POSITION diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 12a43030aa8..bb734d2c03d 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -546,6 +546,27 @@ async def test_no_command_topic(hass, mqtt_mock): assert hass.states.get("cover.test").attributes["supported_features"] == 240 +async def test_no_payload_stop(hass, mqtt_mock): + """Test with no stop payload.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": None, + } + }, + ) + + assert hass.states.get("cover.test").attributes["supported_features"] == 3 + + async def test_with_command_topic_and_tilt(hass, mqtt_mock): """Test with command topic and tilt config.""" assert await async_setup_component( From 0771dc3a37d7884e9bcb9733d477d5166e703738 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 23 Oct 2019 20:37:37 +0200 Subject: [PATCH 611/639] Downgrade aioHTTP 3.6.2 to 3.6.1 (#28143) --- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 80357eccf71..63b4673b435 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiohttp==3.6.2 +aiohttp==3.6.1 aiohttp_cors==0.7.0 astral==1.10.1 async_timeout==3.0.1 diff --git a/requirements_all.txt b/requirements_all.txt index cbe4a93f812..297067f5220 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1,5 +1,5 @@ # Home Assistant core -aiohttp==3.6.2 +aiohttp==3.6.1 astral==1.10.1 async_timeout==3.0.1 attrs==19.2.0 diff --git a/setup.py b/setup.py index 0e8f8313a98..d2c4934713b 100755 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ PROJECT_URLS = { PACKAGES = find_packages(exclude=["tests", "tests.*"]) REQUIRES = [ - "aiohttp==3.6.2", + "aiohttp==3.6.1", "astral==1.10.1", "async_timeout==3.0.1", "attrs==19.2.0", From 65d8c703770537d4e7d14d636c4f96a95c9fe3fc Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Wed, 23 Oct 2019 14:41:26 -0400 Subject: [PATCH 612/639] Rebase Implement Alexa.DoorbellEventSource Interface Controller (#27726) --- .../components/alexa/capabilities.py | 42 ++++++++++-- homeassistant/components/alexa/entities.py | 8 ++- .../components/alexa/state_report.py | 66 ++++++++++++++++++- tests/components/alexa/__init__.py | 2 +- tests/components/alexa/test_smart_home.py | 22 +++++++ tests/components/alexa/test_state_report.py | 31 +++++++++ 6 files changed, 161 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 246429ad6c9..deb83813dbc 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -91,6 +91,15 @@ class AlexaCapability: """Applicable only to scenes.""" return None + @staticmethod + def capability_proactively_reported(): + """Return True if the capability is proactively reported. + + Set properties_proactively_reported() for proactively reported properties. + Applicable to DoorbellEventSource. + """ + return None + @staticmethod def capability_resources(): """Applicable to ToggleController, RangeController, and ModeController interfaces.""" @@ -103,16 +112,20 @@ class AlexaCapability: def serialize_discovery(self): """Serialize according to the Discovery API.""" - result = { - "type": "AlexaInterface", - "interface": self.name(), - "version": "3", - "properties": { + result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} + + properties_supported = self.properties_supported() + if properties_supported: + result["properties"] = { "supported": self.properties_supported(), "proactivelyReported": self.properties_proactively_reported(), "retrievable": self.properties_retrievable(), - }, - } + } + + # pylint: disable=assignment-from-none + proactively_reported = self.capability_proactively_reported() + if proactively_reported is not None: + result["proactivelyReported"] = proactively_reported # pylint: disable=assignment-from-none non_controllable = self.properties_non_controllable() @@ -1050,3 +1063,18 @@ class AlexaChannelController(AlexaCapability): def name(self): """Return the Alexa API name of this interface.""" return "Alexa.ChannelController" + + +class AlexaDoorbellEventSource(AlexaCapability): + """Implements Alexa.DoorbellEventSource. + + https://developer.amazon.com/docs/device-apis/alexa-doorbelleventsource.html + """ + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.DoorbellEventSource" + + def capability_proactively_reported(self): + """Return True for proactively reported capability.""" + return True diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index f6fc9936a02..d84848e9aba 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -38,6 +38,7 @@ from .capabilities import ( AlexaColorController, AlexaColorTemperatureController, AlexaContactSensor, + AlexaDoorbellEventSource, AlexaEndpointHealth, AlexaInputController, AlexaLockController, @@ -84,7 +85,7 @@ class DisplayCategory: DOOR = "DOOR" # Indicates a doorbell. - DOOR_BELL = "DOORBELL" + DOORBELL = "DOORBELL" # Indicates a fan. FAN = "FAN" @@ -500,6 +501,11 @@ class BinarySensorCapabilities(AlexaEntity): elif sensor_type is self.TYPE_MOTION: yield AlexaMotionSensor(self.hass, self.entity) + entity_conf = self.config.entity_config.get(self.entity.entity_id, {}) + if CONF_DISPLAY_CATEGORIES in entity_conf: + if entity_conf[CONF_DISPLAY_CATEGORIES] == DisplayCategory.DOORBELL: + yield AlexaDoorbellEventSource(self.entity) + yield AlexaEndpointHealth(self.hass, self.entity) def get_type(self): diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 42c16919a45..b5e1b741f0c 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -6,7 +6,8 @@ import logging import aiohttp import async_timeout -from homeassistant.const import MATCH_ALL +import homeassistant.util.dt as dt_util +from homeassistant.const import MATCH_ALL, STATE_ON from .const import API_CHANGE, Cause from .entities import ENTITY_ADAPTERS @@ -45,6 +46,14 @@ async def async_enable_proactive_mode(hass, smart_home_config): hass, smart_home_config, alexa_changed_entity ) return + if ( + interface.name() == "Alexa.DoorbellEventSource" + and new_state.state == STATE_ON + ): + await async_send_doorbell_event_message( + hass, smart_home_config, alexa_changed_entity + ) + return return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener @@ -184,3 +193,58 @@ async def async_send_delete_message(hass, config, entity_ids): return await session.post( config.endpoint, headers=headers, json=message_serialized, allow_redirects=True ) + + +async def async_send_doorbell_event_message(hass, config, alexa_entity): + """Send a DoorbellPress event message for an Alexa entity. + + https://developer.amazon.com/docs/smarthome/send-events-to-the-alexa-event-gateway.html + """ + token = await config.async_get_access_token() + + headers = {"Authorization": f"Bearer {token}"} + + endpoint = alexa_entity.alexa_id() + + message = AlexaResponse( + name="DoorbellPress", + namespace="Alexa.DoorbellEventSource", + payload={ + "cause": {"type": Cause.PHYSICAL_INTERACTION}, + "timestamp": f"{dt_util.utcnow().replace(tzinfo=None).isoformat()}Z", + }, + ) + + message.set_endpoint_full(token, endpoint) + + message_serialized = message.serialize() + session = hass.helpers.aiohttp_client.async_get_clientsession() + + try: + with async_timeout.timeout(DEFAULT_TIMEOUT): + response = await session.post( + config.endpoint, + headers=headers, + json=message_serialized, + allow_redirects=True, + ) + + except (asyncio.TimeoutError, aiohttp.ClientError): + _LOGGER.error("Timeout sending report to Alexa.") + return + + response_text = await response.text() + + _LOGGER.debug("Sent: %s", json.dumps(message_serialized)) + _LOGGER.debug("Received (%s): %s", response.status, response_text) + + if response.status == 202: + return + + response_json = json.loads(response_text) + + _LOGGER.error( + "Error when sending DoorbellPress event to Alexa: %s: %s", + response_json["payload"]["code"], + response_json["payload"]["description"], + ) diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index 4fd8bf6f2a9..0fa1961ad61 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -13,7 +13,7 @@ TEST_TOKEN_URL = "https://api.amazon.com/auth/o2/token" class MockConfig(config.AbstractConfig): """Mock Alexa config.""" - entity_config = {} + entity_config = {"binary_sensor.test_doorbell": {"display_categories": "DOORBELL"}} @property def supports_auth(self): diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 139c8c9740b..c50c0748147 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1196,6 +1196,28 @@ async def test_motion_sensor(hass): properties.assert_equal("Alexa.MotionSensor", "detectionState", "DETECTED") +async def test_doorbell_sensor(hass): + """Test doorbell sensor discovery.""" + device = ( + "binary_sensor.test_doorbell", + "off", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "binary_sensor#test_doorbell" + assert appliance["displayCategories"][0] == "DOORBELL" + assert appliance["friendlyName"] == "Test Doorbell Sensor" + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.DoorbellEventSource", "Alexa.EndpointHealth" + ) + + doorbell_capability = get_capability(capabilities, "Alexa.DoorbellEventSource") + assert doorbell_capability is not None + assert doorbell_capability["proactivelyReported"] is True + + async def test_unknown_sensor(hass): """Test sensors of unknown quantities are not discovered.""" device = ( diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 310180ef5d0..2c58d1ed45e 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -137,3 +137,34 @@ async def test_send_delete_message(hass, aioclient_mock): call_json["event"]["payload"]["endpoints"][0]["endpointId"] == "binary_sensor#test_contact" ) + + +async def test_doorbell_event(hass, aioclient_mock): + """Test doorbell press reports.""" + aioclient_mock.post(TEST_URL, text="", status=202) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "off", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ) + + await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) + + hass.states.async_set( + "binary_sensor.test_doorbell", + "on", + {"friendly_name": "Test Doorbell Sensor", "device_class": "occupancy"}, + ) + + # To trigger event listener + await hass.async_block_till_done() + + assert len(aioclient_mock.mock_calls) == 1 + call = aioclient_mock.mock_calls + + call_json = call[0][2] + assert call_json["event"]["header"]["namespace"] == "Alexa.DoorbellEventSource" + assert call_json["event"]["header"]["name"] == "DoorbellPress" + assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION" + assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell" From b6fd191dc427a91aaed81ae72f0e0610e688f145 Mon Sep 17 00:00:00 2001 From: fredericvl <34839323+fredericvl@users.noreply.github.com> Date: Wed, 23 Oct 2019 21:11:04 +0200 Subject: [PATCH 613/639] Add support for SAJ inverters connected via WiFi (#27742) * Add support for SAJ inverters connected via WiFi * Changes after review for saj --- homeassistant/components/saj/manifest.json | 2 +- homeassistant/components/saj/sensor.py | 37 ++++++++++++++++++++-- requirements_all.txt | 2 +- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/saj/manifest.json b/homeassistant/components/saj/manifest.json index e42b37195a4..2dd701e9c7c 100644 --- a/homeassistant/components/saj/manifest.json +++ b/homeassistant/components/saj/manifest.json @@ -3,7 +3,7 @@ "name": "SAJ", "documentation": "https://www.home-assistant.io/integrations/saj", "requirements": [ - "pysaj==0.0.9" + "pysaj==0.0.12" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/saj/sensor.py b/homeassistant/components/saj/sensor.py index fa06b2b9125..5605866908e 100644 --- a/homeassistant/components/saj/sensor.py +++ b/homeassistant/components/saj/sensor.py @@ -9,6 +9,9 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_HOST, + CONF_PASSWORD, + CONF_TYPE, + CONF_USERNAME, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, @@ -31,6 +34,8 @@ MAX_INTERVAL = 300 UNIT_OF_MEASUREMENT_HOURS = "h" +INVERTER_TYPES = ["ethernet", "wifi"] + SAJ_UNIT_MAPPINGS = { "W": POWER_WATT, "kWh": ENERGY_KILO_WATT_HOUR, @@ -40,16 +45,24 @@ SAJ_UNIT_MAPPINGS = { "": None, } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_HOST): cv.string}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_TYPE, default=INVERTER_TYPES[0]): vol.In(INVERTER_TYPES), + vol.Inclusive(CONF_USERNAME, "credentials"): cv.string, + vol.Inclusive(CONF_PASSWORD, "credentials"): cv.string, + } +) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up SAJ sensors.""" remove_interval_update = None + wifi = config[CONF_TYPE] == INVERTER_TYPES[1] # Init all sensors - sensor_def = pysaj.Sensors() + sensor_def = pysaj.Sensors(wifi) # Use all sensors by default hass_sensors = [] @@ -57,7 +70,25 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for sensor in sensor_def: hass_sensors.append(SAJsensor(sensor)) - saj = pysaj.SAJ(config[CONF_HOST]) + kwargs = {} + + if wifi: + kwargs["wifi"] = True + if config.get(CONF_USERNAME) and config.get(CONF_PASSWORD): + kwargs["username"] = config[CONF_USERNAME] + kwargs["password"] = config[CONF_PASSWORD] + + try: + saj = pysaj.SAJ(config[CONF_HOST], **kwargs) + await saj.read(sensor_def) + except pysaj.UnauthorizedException: + _LOGGER.error("Username and/or password is wrong.") + return + except pysaj.UnexpectedResponseException as err: + _LOGGER.error( + "Error in SAJ, please check host/ip address. Original error: %s", err + ) + return async_add_entities(hass_sensors) diff --git a/requirements_all.txt b/requirements_all.txt index 297067f5220..cafdcae9c9b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1420,7 +1420,7 @@ pyrepetier==3.0.5 pysabnzbd==1.1.0 # homeassistant.components.saj -pysaj==0.0.9 +pysaj==0.0.12 # homeassistant.components.sony_projector pysdcp==1 From 1412862f2a29bbc695a515d536b6ca61be799a29 Mon Sep 17 00:00:00 2001 From: On Freund Date: Wed, 23 Oct 2019 22:47:00 +0300 Subject: [PATCH 614/639] Config entry and device for Coolmaster integration (#27925) * Config entry and device for Coolmaster integration * Lint/isort/flake/etc... * Black formatting * Code review fixes * Config flow tests for coolmaster * Add pycoolmaster requirement to test * Remove port selection from Coolmaster config flow * Update config_flow.py * More idoimatic hash concat --- .coveragerc | 2 + .../components/coolmaster/__init__.py | 23 +++- .../components/coolmaster/climate.py | 51 ++++----- .../components/coolmaster/config_flow.py | 64 +++++++++++ homeassistant/components/coolmaster/const.py | 25 +++++ .../components/coolmaster/manifest.json | 1 + .../components/coolmaster/strings.json | 23 ++++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/coolmaster/__init__.py | 1 + .../components/coolmaster/test_config_flow.py | 104 ++++++++++++++++++ 11 files changed, 265 insertions(+), 33 deletions(-) create mode 100644 homeassistant/components/coolmaster/config_flow.py create mode 100644 homeassistant/components/coolmaster/const.py create mode 100644 homeassistant/components/coolmaster/strings.json create mode 100644 tests/components/coolmaster/__init__.py create mode 100644 tests/components/coolmaster/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 83e6971cc6a..3645eb00d33 100644 --- a/.coveragerc +++ b/.coveragerc @@ -126,7 +126,9 @@ omit = homeassistant/components/comfoconnect/* homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/binary_sensor.py + homeassistant/components/coolmaster/__init__.py homeassistant/components/coolmaster/climate.py + homeassistant/components/coolmaster/const.py homeassistant/components/cppm_tracker/device_tracker.py homeassistant/components/cpuspeed/sensor.py homeassistant/components/crimereports/sensor.py diff --git a/homeassistant/components/coolmaster/__init__.py b/homeassistant/components/coolmaster/__init__.py index b27ae5f25b4..530427d33ad 100644 --- a/homeassistant/components/coolmaster/__init__.py +++ b/homeassistant/components/coolmaster/__init__.py @@ -1 +1,22 @@ -"""The coolmaster component.""" +"""The Coolmaster integration.""" + + +async def async_setup(hass, config): + """Set up Coolmaster components.""" + return True + + +async def async_setup_entry(hass, entry): + """Set up Coolmaster from a config entry.""" + hass.async_add_job(hass.config_entries.async_forward_entry_setup(entry, "climate")) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a Coolmaster config entry.""" + await hass.async_add_job( + hass.config_entries.async_forward_entry_unload(entry, "climate") + ) + + return True diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index 71115a9eebb..a52431dd89b 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -3,9 +3,8 @@ import logging from pycoolmasternet import CoolMasterNet -import voluptuous as vol -from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateDevice +from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_DRY, @@ -23,21 +22,11 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) -import homeassistant.helpers.config_validation as cv + +from .const import CONF_SUPPORTED_MODES, DOMAIN SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE -DEFAULT_PORT = 10102 - -AVAILABLE_MODES = [ - HVAC_MODE_OFF, - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_DRY, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_FAN_ONLY, -] - CM_TO_HA_STATE = { "heat": HVAC_MODE_HEAT, "cool": HVAC_MODE_COOL, @@ -50,17 +39,6 @@ HA_STATE_TO_CM = {value: key for key, value in CM_TO_HA_STATE.items()} FAN_MODES = ["low", "med", "high", "auto"] -CONF_SUPPORTED_MODES = "supported_modes" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SUPPORTED_MODES, default=AVAILABLE_MODES): vol.All( - cv.ensure_list, [vol.In(AVAILABLE_MODES)] - ), - } -) - _LOGGER = logging.getLogger(__name__) @@ -69,18 +47,17 @@ def _build_entity(device, supported_modes): return CoolmasterClimate(device, supported_modes) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the CoolMasterNet climate platform.""" - - supported_modes = config.get(CONF_SUPPORTED_MODES) - host = config[CONF_HOST] - port = config[CONF_PORT] + supported_modes = config_entry.data.get(CONF_SUPPORTED_MODES) + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] cool = CoolMasterNet(host, port=port) - devices = cool.devices() + devices = await hass.async_add_executor_job(cool.devices) all_devices = [_build_entity(device, supported_modes) for device in devices] - add_entities(all_devices, True) + async_add_devices(all_devices, True) class CoolmasterClimate(ClimateDevice): @@ -118,6 +95,16 @@ class CoolmasterClimate(ClimateDevice): else: self._unit = TEMP_FAHRENHEIT + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "CoolAutomation", + "model": "CoolMasterNet", + } + @property def unique_id(self): """Return unique ID for this device.""" diff --git a/homeassistant/components/coolmaster/config_flow.py b/homeassistant/components/coolmaster/config_flow.py new file mode 100644 index 00000000000..543b4c239c8 --- /dev/null +++ b/homeassistant/components/coolmaster/config_flow.py @@ -0,0 +1,64 @@ +"""Config flow to configure Coolmaster.""" + +from pycoolmasternet import CoolMasterNet +import voluptuous as vol + +from homeassistant import core, config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +# pylint: disable=unused-import +from .const import AVAILABLE_MODES, CONF_SUPPORTED_MODES, DEFAULT_PORT, DOMAIN + +MODES_SCHEMA = {vol.Required(mode, default=True): bool for mode in AVAILABLE_MODES} + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str, **MODES_SCHEMA}) + + +async def validate_connection(hass: core.HomeAssistant, host): + """Validate that we can connect to the Coolmaster instance.""" + cool = CoolMasterNet(host, port=DEFAULT_PORT) + devices = await hass.async_add_executor_job(cool.devices) + return len(devices) > 0 + + +class CoolmasterConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a Coolmaster config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def _async_get_entry(self, data): + supported_modes = [ + key for (key, value) in data.items() if key in AVAILABLE_MODES and value + ] + return self.async_create_entry( + title=data[CONF_HOST], + data={ + CONF_HOST: data[CONF_HOST], + CONF_PORT: DEFAULT_PORT, + CONF_SUPPORTED_MODES: supported_modes, + }, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is None: + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA) + + errors = {} + + host = user_input[CONF_HOST] + + try: + result = await validate_connection(self.hass, host) + if not result: + errors["base"] = "no_units" + except (ConnectionRefusedError, TimeoutError): + errors["base"] = "connection_error" + + if errors: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + return self._async_get_entry(user_input) diff --git a/homeassistant/components/coolmaster/const.py b/homeassistant/components/coolmaster/const.py new file mode 100644 index 00000000000..d4cfea73820 --- /dev/null +++ b/homeassistant/components/coolmaster/const.py @@ -0,0 +1,25 @@ +"""Constants for the Coolmaster integration.""" + +from homeassistant.components.climate.const import ( + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, +) + +DOMAIN = "coolmaster" + +DEFAULT_PORT = 10102 + +CONF_SUPPORTED_MODES = "supported_modes" + +AVAILABLE_MODES = [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, +] diff --git a/homeassistant/components/coolmaster/manifest.json b/homeassistant/components/coolmaster/manifest.json index 69ab8ee3c4b..124a1e4a5b9 100644 --- a/homeassistant/components/coolmaster/manifest.json +++ b/homeassistant/components/coolmaster/manifest.json @@ -1,6 +1,7 @@ { "domain": "coolmaster", "name": "Coolmaster", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/coolmaster", "requirements": [ "pycoolmasternet==0.0.4" diff --git a/homeassistant/components/coolmaster/strings.json b/homeassistant/components/coolmaster/strings.json new file mode 100644 index 00000000000..d309f8c9c93 --- /dev/null +++ b/homeassistant/components/coolmaster/strings.json @@ -0,0 +1,23 @@ +{ + "config": { + "title": "CoolMasterNet", + "step": { + "user": { + "title": "Setup your CoolMasterNet connection details.", + "data": { + "host": "Host", + "off": "Can be turned off", + "heat": "Support heat mode", + "cool": "Support cool mode", + "heat_cool": "Support automatic heat/cool mode", + "dry": "Support dry mode", + "fan_only": "Support fan only mode" + } + } + }, + "error": { + "connection_error": "Failed to connect to CoolMasterNet instance. Please check your host.", + "no_units": "Could not find any HVAC units in CoolMasterNet host." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index bf63869bc9b..4668528fedb 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -14,6 +14,7 @@ FLOWS = [ "axis", "cast", "cert_expiry", + "coolmaster", "daikin", "deconz", "dialogflow", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e61732e632b..2bdd47cd946 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -398,6 +398,9 @@ pybotvac==0.0.17 # homeassistant.components.cast pychromecast==4.0.1 +# homeassistant.components.coolmaster +pycoolmasternet==0.0.4 + # homeassistant.components.daikin pydaikin==1.6.1 diff --git a/tests/components/coolmaster/__init__.py b/tests/components/coolmaster/__init__.py new file mode 100644 index 00000000000..a7e1bf08c99 --- /dev/null +++ b/tests/components/coolmaster/__init__.py @@ -0,0 +1 @@ +"""Tests for the Coolmaster component.""" diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py new file mode 100644 index 00000000000..d49858fcf05 --- /dev/null +++ b/tests/components/coolmaster/test_config_flow.py @@ -0,0 +1,104 @@ +"""Test the Coolmaster config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.components.coolmaster.const import DOMAIN, AVAILABLE_MODES + +# from homeassistant.components.coolmaster.config_flow import validate_connection + +from tests.common import mock_coro + + +def _flow_data(): + options = {"host": "1.1.1.1"} + for mode in AVAILABLE_MODES: + options[mode] = True + return options + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + return_value=mock_coro(True), + ), patch( + "homeassistant.components.coolmaster.async_setup", return_value=mock_coro(True) + ) as mock_setup, patch( + "homeassistant.components.coolmaster.async_setup_entry", + return_value=mock_coro(True), + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "1.1.1.1" + assert result2["data"] == { + "host": "1.1.1.1", + "port": 10102, + "supported_modes": AVAILABLE_MODES, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_timeout(hass): + """Test we handle a connection timeout.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + side_effect=TimeoutError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_form_connection_refused(hass): + """Test we handle a connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + side_effect=ConnectionRefusedError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "connection_error"} + + +async def test_form_no_units(hass): + """Test we handle no units found.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.coolmaster.config_flow.validate_connection", + return_value=mock_coro(False), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], _flow_data() + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "no_units"} From 062ec8a7c29507798816b964ae02caaf66b29954 Mon Sep 17 00:00:00 2001 From: Villhellm Date: Wed, 23 Oct 2019 13:03:52 -0700 Subject: [PATCH 615/639] changed STATE_OFF to STATE_STANDBY (#28148) --- homeassistant/components/roku/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index d69b0eddb71..12aca141510 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -20,7 +20,7 @@ from homeassistant.const import ( STATE_HOME, STATE_IDLE, STATE_PLAYING, - STATE_OFF, + STATE_STANDBY, ) DEFAULT_PORT = 8060 @@ -98,7 +98,7 @@ class RokuDevice(MediaPlayerDevice): def state(self): """Return the state of the device.""" if self._power_state == "Off": - return STATE_OFF + return STATE_STANDBY if self.current_app is None: return None From 7cb6607b1f6a86469cb79314594a2920141ac224 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Wed, 23 Oct 2019 23:09:11 +0300 Subject: [PATCH 616/639] Allow multiple Transmission clients and add unique_id to entities (#28136) * Allow multiple clients + improvements * remove commented code * fixed test_init.py --- .coveragerc | 1 - .../transmission/.translations/en.json | 37 ++--- .../components/transmission/__init__.py | 153 ++++++++++-------- .../components/transmission/config_flow.py | 46 +++--- .../components/transmission/const.py | 1 - .../components/transmission/sensor.py | 32 ++-- .../components/transmission/strings.json | 13 +- .../components/transmission/switch.py | 37 +++-- .../transmission/test_config_flow.py | 73 +++++---- tests/components/transmission/test_init.py | 123 ++++++++++++++ 10 files changed, 333 insertions(+), 183 deletions(-) create mode 100644 tests/components/transmission/test_init.py diff --git a/.coveragerc b/.coveragerc index 3645eb00d33..f97a7524a21 100644 --- a/.coveragerc +++ b/.coveragerc @@ -703,7 +703,6 @@ omit = homeassistant/components/tradfri/base_class.py homeassistant/components/trafikverket_train/sensor.py homeassistant/components/trafikverket_weatherstation/sensor.py - homeassistant/components/transmission/__init__.py homeassistant/components/transmission/sensor.py homeassistant/components/transmission/switch.py homeassistant/components/transmission/const.py diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index 67461d1a3e8..45c16be36e2 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -1,39 +1,34 @@ { "config": { - "abort": { - "one_instance_allowed": "Only a single instance is necessary." - }, - "error": { - "cannot_connect": "Unable to Connect to host", - "wrong_credentials": "Wrong username or password" - }, + "title": "Transmission", "step": { - "options": { - "data": { - "scan_interval": "Update frequency" - }, - "title": "Configure Options" - }, "user": { + "title": "Setup Transmission Client", "data": { - "host": "Host", "name": "Name", + "host": "Host", + "username": "Username", "password": "Password", - "port": "Port", - "username": "Username" - }, - "title": "Setup Transmission Client" + "port": "Port" + } } }, - "title": "Transmission" + "error": { + "name_exists": "Name already exists", + "wrong_credentials": "Wrong username or password", + "cannot_connect": "Unable to Connect to host" + }, + "abort": { + "already_configured": "Host is already configured." + } }, "options": { "step": { "init": { + "title": "Configure options for Transmission", "data": { "scan_interval": "Update frequency" - }, - "description": "Configure options for Transmission" + } } } } diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index e6ddd87bdf5..6cfd6bf640a 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -19,11 +19,10 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util import slugify from .const import ( ATTR_TORRENT, - DATA_TRANSMISSION, - DATA_UPDATED, DEFAULT_NAME, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, @@ -37,74 +36,77 @@ _LOGGER = logging.getLogger(__name__) SERVICE_ADD_TORRENT_SCHEMA = vol.Schema({vol.Required(ATTR_TORRENT): cv.string}) +TRANS_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_USERNAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + } + ) +) + CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) - }, - extra=vol.ALLOW_EXTRA, + {DOMAIN: vol.All(cv.ensure_list, [TRANS_SCHEMA])}, extra=vol.ALLOW_EXTRA ) async def async_setup(hass, config): """Import the Transmission Component from config.""" - if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + if DOMAIN in config: + for entry in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry + ) ) - ) return True async def async_setup_entry(hass, config_entry): """Set up the Transmission Component.""" - if DOMAIN not in hass.data: - hass.data[DOMAIN] = {} - - if not config_entry.options: - await async_populate_options(hass, config_entry) - client = TransmissionClient(hass, config_entry) - client_id = config_entry.entry_id - hass.data[DOMAIN][client_id] = client + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = client + if not await client.async_setup(): return False return True -async def async_unload_entry(hass, entry): +async def async_unload_entry(hass, config_entry): """Unload Transmission Entry from config_entry.""" - hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) - if hass.data[DOMAIN][entry.entry_id].unsub_timer: - hass.data[DOMAIN][entry.entry_id].unsub_timer() + client = hass.data[DOMAIN][config_entry.entry_id] + hass.services.async_remove(DOMAIN, client.service_name) + if client.unsub_timer: + client.unsub_timer() for component in "sensor", "switch": - await hass.config_entries.async_forward_entry_unload(entry, component) + await hass.config_entries.async_forward_entry_unload(config_entry, component) - del hass.data[DOMAIN] + hass.data[DOMAIN].pop(config_entry.entry_id) return True -async def get_api(hass, host, port, username=None, password=None): +async def get_api(hass, entry): """Get Transmission client.""" + host = entry[CONF_HOST] + port = entry[CONF_PORT] + username = entry.get(CONF_USERNAME) + password = entry.get(CONF_PASSWORD) + try: api = await hass.async_add_executor_job( transmissionrpc.Client, host, port, username, password ) + _LOGGER.debug("Successfully connected to %s", host) return api except TransmissionError as error: @@ -112,20 +114,13 @@ async def get_api(hass, host, port, username=None, password=None): _LOGGER.error("Credentials for Transmission client are not valid") raise AuthenticationError if "111: Connection refused" in str(error): - _LOGGER.error("Connecting to the Transmission client failed") + _LOGGER.error("Connecting to the Transmission client %s failed", host) raise CannotConnect _LOGGER.error(error) raise UnknownError -async def async_populate_options(hass, config_entry): - """Populate default options for Transmission Client.""" - options = {CONF_SCAN_INTERVAL: config_entry.data["options"][CONF_SCAN_INTERVAL]} - - hass.config_entries.async_update_entry(config_entry, options=options) - - class TransmissionClient: """Transmission Client Object.""" @@ -133,33 +128,35 @@ class TransmissionClient: """Initialize the Transmission RPC API.""" self.hass = hass self.config_entry = config_entry - self.scan_interval = self.config_entry.options[CONF_SCAN_INTERVAL] - self.tm_data = None + self._tm_data = None self.unsub_timer = None + @property + def service_name(self): + """Return the service name.""" + return slugify(f"{SERVICE_ADD_TORRENT}_{self.config_entry.data[CONF_NAME]}") + + @property + def api(self): + """Return the tm_data object.""" + return self._tm_data + async def async_setup(self): """Set up the Transmission client.""" - config = { - CONF_HOST: self.config_entry.data[CONF_HOST], - CONF_PORT: self.config_entry.data[CONF_PORT], - CONF_USERNAME: self.config_entry.data.get(CONF_USERNAME), - CONF_PASSWORD: self.config_entry.data.get(CONF_PASSWORD), - } try: - api = await get_api(self.hass, **config) + api = await get_api(self.hass, self.config_entry.data) except CannotConnect: raise ConfigEntryNotReady except (AuthenticationError, UnknownError): return False - self.tm_data = self.hass.data[DOMAIN][DATA_TRANSMISSION] = TransmissionData( - self.hass, self.config_entry, api - ) + self._tm_data = TransmissionData(self.hass, self.config_entry, api) - await self.hass.async_add_executor_job(self.tm_data.init_torrent_list) - await self.hass.async_add_executor_job(self.tm_data.update) - self.set_scan_interval(self.scan_interval) + await self.hass.async_add_executor_job(self._tm_data.init_torrent_list) + await self.hass.async_add_executor_job(self._tm_data.update) + self.add_options() + self.set_scan_interval(self.config_entry.options[CONF_SCAN_INTERVAL]) for platform in ["sensor", "switch"]: self.hass.async_create_task( @@ -181,19 +178,31 @@ class TransmissionClient: ) self.hass.services.async_register( - DOMAIN, SERVICE_ADD_TORRENT, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA + DOMAIN, self.service_name, add_torrent, schema=SERVICE_ADD_TORRENT_SCHEMA ) self.config_entry.add_update_listener(self.async_options_updated) return True + def add_options(self): + """Add options for entry.""" + if not self.config_entry.options: + scan_interval = self.config_entry.data.pop( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ) + options = {CONF_SCAN_INTERVAL: scan_interval} + + self.hass.config_entries.async_update_entry( + self.config_entry, options=options + ) + def set_scan_interval(self, scan_interval): """Update scan interval.""" - def refresh(event_time): + async def refresh(event_time): """Get the latest data from Transmission.""" - self.tm_data.update() + self._tm_data.update() if self.unsub_timer is not None: self.unsub_timer() @@ -215,6 +224,7 @@ class TransmissionData: def __init__(self, hass, config, api): """Initialize the Transmission RPC API.""" self.hass = hass + self.config = config self.data = None self.torrents = None self.session = None @@ -223,6 +233,16 @@ class TransmissionData: self.completed_torrents = [] self.started_torrents = [] + @property + def host(self): + """Return the host name.""" + return self.config.data[CONF_HOST] + + @property + def signal_options_update(self): + """Option update signal per transmission entry.""" + return f"tm-options-{self.host}" + def update(self): """Get the latest data from Transmission instance.""" try: @@ -232,14 +252,13 @@ class TransmissionData: self.check_completed_torrent() self.check_started_torrent() - _LOGGER.debug("Torrent Data Updated") + _LOGGER.debug("Torrent Data for %s Updated", self.host) self.available = True except TransmissionError: self.available = False - _LOGGER.error("Unable to connect to Transmission client") - - dispatcher_send(self.hass, DATA_UPDATED) + _LOGGER.error("Unable to connect to Transmission client %s", self.host) + dispatcher_send(self.hass, self.signal_options_update) def init_torrent_list(self): """Initialize torrent lists.""" diff --git a/homeassistant/components/transmission/config_flow.py b/homeassistant/components/transmission/config_flow.py index 99376f4b6e0..d7b9efb15d8 100644 --- a/homeassistant/components/transmission/config_flow.py +++ b/homeassistant/components/transmission/config_flow.py @@ -29,32 +29,32 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return TransmissionOptionsFlowHandler(config_entry) - def __init__(self): - """Initialize the Transmission flow.""" - self.config = {} - self.errors = {} - async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="one_instance_allowed") + errors = {} if user_input is not None: - self.config[CONF_NAME] = user_input.pop(CONF_NAME) + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == user_input[CONF_HOST]: + return self.async_abort(reason="already_configured") + if entry.data[CONF_NAME] == user_input[CONF_NAME]: + errors[CONF_NAME] = "name_exists" + break + try: - await get_api(self.hass, **user_input) - self.config.update(user_input) - if "options" not in self.config: - self.config["options"] = {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} - return self.async_create_entry( - title=self.config[CONF_NAME], data=self.config - ) + await get_api(self.hass, user_input) + except AuthenticationError: - self.errors[CONF_USERNAME] = "wrong_credentials" - self.errors[CONF_PASSWORD] = "wrong_credentials" + errors[CONF_USERNAME] = "wrong_credentials" + errors[CONF_PASSWORD] = "wrong_credentials" except (CannotConnect, UnknownError): - self.errors["base"] = "cannot_connect" + errors["base"] = "cannot_connect" + + if not errors: + return self.async_create_entry( + title=user_input[CONF_NAME], data=user_input + ) return self.async_show_form( step_id="user", @@ -67,15 +67,12 @@ class TransmissionFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): vol.Required(CONF_PORT, default=DEFAULT_PORT): int, } ), - errors=self.errors, + errors=errors, ) async def async_step_import(self, import_config): """Import from Transmission client config.""" - self.config["options"] = { - CONF_SCAN_INTERVAL: import_config.pop(CONF_SCAN_INTERVAL).seconds - } - + import_config[CONF_SCAN_INTERVAL] = import_config[CONF_SCAN_INTERVAL].seconds return await self.async_step_user(user_input=import_config) @@ -95,8 +92,7 @@ class TransmissionOptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_SCAN_INTERVAL, default=self.config_entry.options.get( - CONF_SCAN_INTERVAL, - self.config_entry.data["options"][CONF_SCAN_INTERVAL], + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), ): int } diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index e4a8b1490c2..472bb32a391 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -21,4 +21,3 @@ ATTR_TORRENT = "torrent" SERVICE_ADD_TORRENT = "add_torrent" DATA_UPDATED = "transmission_data_updated" -DATA_TRANSMISSION = "data_transmission" diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 30dfa4a3cbe..d9fd2b51144 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -6,7 +6,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SENSOR_TYPES +from .const import DOMAIN, SENSOR_TYPES _LOGGER = logging.getLogger(__name__) @@ -19,7 +19,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission sensors.""" - transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + tm_client = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data[CONF_NAME] dev = [] @@ -27,7 +27,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): dev.append( TransmissionSensor( sensor_type, - transmission_api, + tm_client, name, SENSOR_TYPES[sensor_type][0], SENSOR_TYPES[sensor_type][1], @@ -41,17 +41,12 @@ class TransmissionSensor(Entity): """Representation of a Transmission sensor.""" def __init__( - self, - sensor_type, - transmission_api, - client_name, - sensor_name, - unit_of_measurement, + self, sensor_type, tm_client, client_name, sensor_name, unit_of_measurement ): """Initialize the sensor.""" self._name = sensor_name self._state = None - self._transmission_api = transmission_api + self._tm_client = tm_client self._unit_of_measurement = unit_of_measurement self._data = None self.client_name = client_name @@ -62,6 +57,11 @@ class TransmissionSensor(Entity): """Return the name of the sensor.""" return f"{self.client_name} {self._name}" + @property + def unique_id(self): + """Return the unique id of the entity.""" + return f"{self._tm_client.api.host}-{self.name}" + @property def state(self): """Return the state of the sensor.""" @@ -80,12 +80,14 @@ class TransmissionSensor(Entity): @property def available(self): """Could the device be accessed during the last update call.""" - return self._transmission_api.available + return self._tm_client.api.available async def async_added_to_hass(self): """Handle entity which will be added.""" async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update + self.hass, + self._tm_client.api.signal_options_update, + self._schedule_immediate_update, ) @callback @@ -94,12 +96,12 @@ class TransmissionSensor(Entity): def update(self): """Get the latest data from Transmission and updates the state.""" - self._data = self._transmission_api.data + self._data = self._tm_client.api.data if self.type == "completed_torrents": - self._state = self._transmission_api.get_completed_torrent_count() + self._state = self._tm_client.api.get_completed_torrent_count() elif self.type == "started_torrents": - self._state = self._transmission_api.get_started_torrent_count() + self._state = self._tm_client.api.get_started_torrent_count() if self.type == "current_status": if self._data: diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 203ed07adb5..45c16be36e2 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -11,30 +11,25 @@ "password": "Password", "port": "Port" } - }, - "options": { - "title": "Configure Options", - "data": { - "scan_interval": "Update frequency" - } } }, "error": { + "name_exists": "Name already exists", "wrong_credentials": "Wrong username or password", "cannot_connect": "Unable to Connect to host" }, "abort": { - "one_instance_allowed": "Only a single instance is necessary." + "already_configured": "Host is already configured." } }, "options": { "step": { "init": { - "description": "Configure options for Transmission", + "title": "Configure options for Transmission", "data": { "scan_interval": "Update frequency" } } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/transmission/switch.py b/homeassistant/components/transmission/switch.py index 0bb43f715ac..4b93b3f06e2 100644 --- a/homeassistant/components/transmission/switch.py +++ b/homeassistant/components/transmission/switch.py @@ -6,7 +6,7 @@ from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import ToggleEntity -from .const import DATA_TRANSMISSION, DATA_UPDATED, DOMAIN, SWITCH_TYPES +from .const import DOMAIN, SWITCH_TYPES _LOGGING = logging.getLogger(__name__) @@ -19,12 +19,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Transmission switch.""" - transmission_api = hass.data[DOMAIN][DATA_TRANSMISSION] + tm_client = hass.data[DOMAIN][config_entry.entry_id] name = config_entry.data[CONF_NAME] dev = [] for switch_type, switch_name in SWITCH_TYPES.items(): - dev.append(TransmissionSwitch(switch_type, switch_name, transmission_api, name)) + dev.append(TransmissionSwitch(switch_type, switch_name, tm_client, name)) async_add_entities(dev, True) @@ -32,12 +32,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class TransmissionSwitch(ToggleEntity): """Representation of a Transmission switch.""" - def __init__(self, switch_type, switch_name, transmission_api, name): + def __init__(self, switch_type, switch_name, tm_client, name): """Initialize the Transmission switch.""" self._name = switch_name self.client_name = name self.type = switch_type - self._transmission_api = transmission_api + self._tm_client = tm_client self._state = STATE_OFF self._data = None @@ -46,6 +46,11 @@ class TransmissionSwitch(ToggleEntity): """Return the name of the switch.""" return f"{self.client_name} {self._name}" + @property + def unique_id(self): + """Return the unique id of the entity.""" + return f"{self._tm_client.api.host}-{self.name}" + @property def state(self): """Return the state of the device.""" @@ -64,32 +69,34 @@ class TransmissionSwitch(ToggleEntity): @property def available(self): """Could the device be accessed during the last update call.""" - return self._transmission_api.available + return self._tm_client.api.available def turn_on(self, **kwargs): """Turn the device on.""" if self.type == "on_off": _LOGGING.debug("Starting all torrents") - self._transmission_api.start_torrents() + self._tm_client.api.start_torrents() elif self.type == "turtle_mode": _LOGGING.debug("Turning Turtle Mode of Transmission on") - self._transmission_api.set_alt_speed_enabled(True) - self._transmission_api.update() + self._tm_client.api.set_alt_speed_enabled(True) + self._tm_client.api.update() def turn_off(self, **kwargs): """Turn the device off.""" if self.type == "on_off": _LOGGING.debug("Stoping all torrents") - self._transmission_api.stop_torrents() + self._tm_client.api.stop_torrents() if self.type == "turtle_mode": _LOGGING.debug("Turning Turtle Mode of Transmission off") - self._transmission_api.set_alt_speed_enabled(False) - self._transmission_api.update() + self._tm_client.api.set_alt_speed_enabled(False) + self._tm_client.api.update() async def async_added_to_hass(self): """Handle entity which will be added.""" async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update + self.hass, + self._tm_client.api.signal_options_update, + self._schedule_immediate_update, ) @callback @@ -100,12 +107,12 @@ class TransmissionSwitch(ToggleEntity): """Get the latest data from Transmission and updates the state.""" active = None if self.type == "on_off": - self._data = self._transmission_api.data + self._data = self._tm_client.api.data if self._data: active = self._data.activeTorrentCount > 0 elif self.type == "turtle_mode": - active = self._transmission_api.get_alt_speed_enabled() + active = self._tm_client.api.get_alt_speed_enabled() if active is None: return diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index e79f5c8ac96..28fbed9ff42 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -1,4 +1,4 @@ -"""Tests for Met.no config flow.""" +"""Tests for Transmission config flow.""" from datetime import timedelta from unittest.mock import patch @@ -31,6 +31,14 @@ PASSWORD = "password" PORT = 9091 SCAN_INTERVAL = 10 +MOCK_ENTRY = { + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + CONF_PORT: PORT, +} + @pytest.fixture(name="api") def mock_transmission_api(): @@ -90,18 +98,10 @@ async def test_flow_works(hass, api): assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == PORT - assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL # test with all provided - result = await flow.async_step_user( - { - CONF_NAME: NAME, - CONF_HOST: HOST, - CONF_USERNAME: USERNAME, - CONF_PASSWORD: PASSWORD, - CONF_PORT: PORT, - } - ) + result = await flow.async_step_user(MOCK_ENTRY) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == NAME @@ -110,7 +110,7 @@ async def test_flow_works(hass, api): assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD assert result["data"][CONF_PORT] == PORT - assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + # assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL async def test_options(hass): @@ -118,14 +118,7 @@ async def test_options(hass): entry = MockConfigEntry( domain=DOMAIN, title=CONF_NAME, - data={ - "name": DEFAULT_NAME, - "host": HOST, - "username": USERNAME, - "password": PASSWORD, - "port": DEFAULT_PORT, - "options": {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, - }, + data=MOCK_ENTRY, options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) flow = init_config_flow(hass) @@ -157,7 +150,7 @@ async def test_import(hass, api): assert result["data"][CONF_NAME] == DEFAULT_NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_PORT] == DEFAULT_PORT - assert result["data"]["options"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL + assert result["data"][CONF_SCAN_INTERVAL] == DEFAULT_SCAN_INTERVAL # import with all result = await flow.async_step_import( @@ -177,18 +170,40 @@ async def test_import(hass, api): assert result["data"][CONF_USERNAME] == USERNAME assert result["data"][CONF_PASSWORD] == PASSWORD assert result["data"][CONF_PORT] == PORT - assert result["data"]["options"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL + assert result["data"][CONF_SCAN_INTERVAL] == SCAN_INTERVAL -async def test_integration_already_exists(hass, api): - """Test we only allow a single config flow.""" - MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} +async def test_host_already_configured(hass, api): + """Test host is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_ENTRY, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, ) + entry.add_to_hass(hass) + flow = init_config_flow(hass) + result = await flow.async_step_user(MOCK_ENTRY) + assert result["type"] == "abort" - assert result["reason"] == "one_instance_allowed" + assert result["reason"] == "already_configured" + + +async def test_name_already_configured(hass, api): + """Test name is already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_ENTRY, + options={CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL}, + ) + entry.add_to_hass(hass) + + mock_entry = MOCK_ENTRY.copy() + mock_entry[CONF_HOST] = "0.0.0.0" + flow = init_config_flow(hass) + result = await flow.async_step_user(mock_entry) + + assert result["type"] == "form" + assert result["errors"] == {CONF_NAME: "name_exists"} async def test_error_on_wrong_credentials(hass, auth_error): diff --git a/tests/components/transmission/test_init.py b/tests/components/transmission/test_init.py new file mode 100644 index 00000000000..4baa00de7a7 --- /dev/null +++ b/tests/components/transmission/test_init.py @@ -0,0 +1,123 @@ +"""Tests for Transmission init.""" + +from unittest.mock import patch + +import pytest +from transmissionrpc.error import TransmissionError + +from homeassistant.components import transmission +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, mock_coro + +MOCK_ENTRY = MockConfigEntry( + domain=transmission.DOMAIN, + data={ + transmission.CONF_NAME: "Transmission", + transmission.CONF_HOST: "0.0.0.0", + transmission.CONF_USERNAME: "user", + transmission.CONF_PASSWORD: "pass", + transmission.CONF_PORT: 9091, + }, +) + + +@pytest.fixture(name="api") +def mock_transmission_api(): + """Mock an api.""" + with patch("transmissionrpc.Client"): + yield + + +@pytest.fixture(name="auth_error") +def mock_api_authentication_error(): + """Mock an api.""" + with patch( + "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") + ): + yield + + +@pytest.fixture(name="unknown_error") +def mock_api_unknown_error(): + """Mock an api.""" + with patch("transmissionrpc.Client", side_effect=TransmissionError): + yield + + +async def test_setup_with_no_config(hass): + """Test that we do not discover anything or try to set up a Transmission client.""" + assert await async_setup_component(hass, transmission.DOMAIN, {}) is True + assert transmission.DOMAIN not in hass.data + + +async def test_setup_with_config(hass, api): + """Test that we import the config and setup the client.""" + config = { + transmission.DOMAIN: { + transmission.CONF_NAME: "Transmission", + transmission.CONF_HOST: "0.0.0.0", + transmission.CONF_USERNAME: "user", + transmission.CONF_PASSWORD: "pass", + transmission.CONF_PORT: 9091, + }, + transmission.DOMAIN: { + transmission.CONF_NAME: "Transmission2", + transmission.CONF_HOST: "0.0.0.1", + transmission.CONF_USERNAME: "user", + transmission.CONF_PASSWORD: "pass", + transmission.CONF_PORT: 9091, + }, + } + assert await async_setup_component(hass, transmission.DOMAIN, config) is True + + +async def test_successful_config_entry(hass, api): + """Test that configured transmission is configured successfully.""" + + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + assert await transmission.async_setup_entry(hass, entry) is True + assert entry.options == { + transmission.CONF_SCAN_INTERVAL: transmission.DEFAULT_SCAN_INTERVAL + } + + +async def test_setup_failed(hass): + """Test transmission failed due to an error.""" + + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + # test connection error raising ConfigEntryNotReady + with patch( + "transmissionrpc.Client", + side_effect=TransmissionError("111: Connection refused"), + ), pytest.raises(ConfigEntryNotReady): + + await transmission.async_setup_entry(hass, entry) + + # test Authentication error returning false + + with patch( + "transmissionrpc.Client", side_effect=TransmissionError("401: Unauthorized") + ): + + assert await transmission.async_setup_entry(hass, entry) is False + + +async def test_unload_entry(hass, api): + """Test removing transmission client.""" + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + with patch.object( + hass.config_entries, "async_forward_entry_unload", return_value=mock_coro(True) + ) as unload_entry: + assert await transmission.async_setup_entry(hass, entry) + + assert await transmission.async_unload_entry(hass, entry) + assert unload_entry.call_count == 2 + assert entry.entry_id not in hass.data[transmission.DOMAIN] From 6a731a68cdcb61801b995895ad3f3cc614163874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 23 Oct 2019 23:18:41 +0300 Subject: [PATCH 617/639] Parallelize pylint everywhere (#28149) * Run 2 pylint jobs by default * Run pylint with autodetected number of jobs in Travis Gives a ~25% speedup there at the moment. --- .travis.yml | 2 +- azure-pipelines-ci.yml | 2 +- pylintrc | 3 +++ tox.ini | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7b3765716eb..6d5b43c2f03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ matrix: - python: "3.6.1" env: TOXENV=lint - python: "3.6.1" - env: TOXENV=pylint + env: TOXENV=pylint PYLINT_ARGS=--jobs=0 - python: "3.6.1" env: TOXENV=typing - python: "3.6.1" diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 1ca834b6213..f1abf2ff9db 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -167,7 +167,7 @@ stages: displayName: 'Install Home Assistant' - script: | . venv/bin/activate - pylint -j 2 homeassistant + pylint homeassistant displayName: 'Run pylint' - job: 'Mypy' pool: diff --git a/pylintrc b/pylintrc index 3d69800e5c3..4aced384b63 100644 --- a/pylintrc +++ b/pylintrc @@ -1,5 +1,8 @@ [MASTER] ignore=tests +# Use a conservative default here; 2 should speed up most setups and not hurt +# any too bad. Override on command line as appropriate. +jobs=2 [BASIC] good-names=id,i,j,k,ex,Run,_,fp diff --git a/tox.ini b/tox.ini index 0b0c969d781..f6d12fe30f5 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,7 @@ deps = -r{toxinidir}/requirements_test.txt -c{toxinidir}/homeassistant/package_constraints.txt commands = - pylint {posargs} homeassistant + pylint {env:PYLINT_ARGS} {posargs} homeassistant [testenv:lint] deps = From 2b36fe421c9f210b739194251f1013d8dcb4aa00 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 23 Oct 2019 22:22:07 +0200 Subject: [PATCH 618/639] Updated frontend to 20191023.0 (#28150) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 43578420212..ae53b972dca 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191014.0" + "home-assistant-frontend==20191023.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 63b4673b435..d2f10c891a9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191014.0 +home-assistant-frontend==20191023.0 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index cafdcae9c9b..eacaee7d927 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191014.0 +home-assistant-frontend==20191023.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2bdd47cd946..39903b3606a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191014.0 +home-assistant-frontend==20191023.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From 23289459ca3a59b9329d6bd8a48a4bedac466573 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Oct 2019 13:36:38 -0700 Subject: [PATCH 619/639] Update translations --- .../components/adguard/.translations/fr.json | 2 + .../alarm_control_panel/.translations/fr.json | 11 +++++ .../alarm_control_panel/.translations/nl.json | 3 +- .../ambiclimate/.translations/ru.json | 2 +- .../components/axis/.translations/fr.json | 1 + .../components/axis/.translations/ru.json | 4 +- .../cert_expiry/.translations/ca.json | 6 ++- .../cert_expiry/.translations/da.json | 4 +- .../cert_expiry/.translations/en.json | 4 +- .../cert_expiry/.translations/fr.json | 4 +- .../cert_expiry/.translations/nl.json | 4 +- .../cert_expiry/.translations/no.json | 4 +- .../cert_expiry/.translations/ru.json | 6 ++- .../coolmaster/.translations/en.json | 23 ++++++++++ .../components/cover/.translations/fr.json | 10 ++++ .../components/daikin/.translations/ru.json | 2 +- .../components/deconz/.translations/ru.json | 2 +- .../components/glances/.translations/fr.json | 13 ++++-- .../components/glances/.translations/pl.json | 23 ++++++++++ .../components/glances/.translations/ru.json | 2 +- .../components/hangouts/.translations/fr.json | 2 + .../components/heos/.translations/ca.json | 2 +- .../homematicip_cloud/.translations/ru.json | 2 +- .../components/hue/.translations/ru.json | 2 +- .../components/lock/.translations/fr.json | 13 ++++++ .../opentherm_gw/.translations/fr.json | 1 + .../opentherm_gw/.translations/ru.json | 2 +- .../rainmachine/.translations/ru.json | 2 +- .../components/sensor/.translations/nl.json | 2 +- .../components/solarlog/.translations/ca.json | 21 +++++++++ .../components/solarlog/.translations/da.json | 21 +++++++++ .../components/solarlog/.translations/en.json | 38 +++++++-------- .../components/solarlog/.translations/fr.json | 21 +++++++++ .../components/solarlog/.translations/nl.json | 21 +++++++++ .../components/solarlog/.translations/no.json | 21 +++++++++ .../components/solarlog/.translations/ru.json | 21 +++++++++ .../components/tradfri/.translations/ru.json | 2 +- .../transmission/.translations/en.json | 46 +++++++++++-------- .../components/unifi/.translations/fr.json | 6 +++ .../components/upnp/.translations/fr.json | 4 ++ .../components/zha/.translations/fr.json | 8 ++-- 41 files changed, 319 insertions(+), 69 deletions(-) create mode 100644 homeassistant/components/alarm_control_panel/.translations/fr.json create mode 100644 homeassistant/components/coolmaster/.translations/en.json create mode 100644 homeassistant/components/cover/.translations/fr.json create mode 100644 homeassistant/components/glances/.translations/pl.json create mode 100644 homeassistant/components/lock/.translations/fr.json create mode 100644 homeassistant/components/solarlog/.translations/ca.json create mode 100644 homeassistant/components/solarlog/.translations/da.json create mode 100644 homeassistant/components/solarlog/.translations/fr.json create mode 100644 homeassistant/components/solarlog/.translations/nl.json create mode 100644 homeassistant/components/solarlog/.translations/no.json create mode 100644 homeassistant/components/solarlog/.translations/ru.json diff --git a/homeassistant/components/adguard/.translations/fr.json b/homeassistant/components/adguard/.translations/fr.json index 6543ddd50bc..749ba7d9c03 100644 --- a/homeassistant/components/adguard/.translations/fr.json +++ b/homeassistant/components/adguard/.translations/fr.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "adguard_home_addon_outdated": "Cette int\u00e9gration n\u00e9cessite AdGuard Home {minimal_version} ou une version ult\u00e9rieure, vous disposez de {current_version}. Veuillez mettre \u00e0 jour votre compl\u00e9ment Hass.io AdGuard Home.", + "adguard_home_outdated": "Cette int\u00e9gration n\u00e9cessite AdGuard Home {minimal_version} ou une version ult\u00e9rieure, vous disposez de {current_version}.", "existing_instance_updated": "La configuration existante a \u00e9t\u00e9 mise \u00e0 jour.", "single_instance_allowed": "Une seule configuration d'AdGuard Home est autoris\u00e9e." }, diff --git a/homeassistant/components/alarm_control_panel/.translations/fr.json b/homeassistant/components/alarm_control_panel/.translations/fr.json new file mode 100644 index 00000000000..c3ba6db0c62 --- /dev/null +++ b/homeassistant/components/alarm_control_panel/.translations/fr.json @@ -0,0 +1,11 @@ +{ + "device_automation": { + "action_type": { + "arm_away": "Armer {entity_name} mode sortie", + "arm_home": "Armer {entity_name} mode \u00e0 la maison", + "arm_night": "Armer {entity_name} mode nuit", + "disarm": "D\u00e9sarmer {entity_name}", + "trigger": "D\u00e9clencheur {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/nl.json b/homeassistant/components/alarm_control_panel/.translations/nl.json index 9f4e6a4a51c..86cacad9fd6 100644 --- a/homeassistant/components/alarm_control_panel/.translations/nl.json +++ b/homeassistant/components/alarm_control_panel/.translations/nl.json @@ -1,7 +1,8 @@ { "device_automation": { "action_type": { - "disarm": "Uitschakelen {entity_name}" + "disarm": "Uitschakelen {entity_name}", + "trigger": "Trigger {entity_name}" } } } \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/ru.json b/homeassistant/components/ambiclimate/.translations/ru.json index 5a816bce140..ba667ea7b9a 100644 --- a/homeassistant/components/ambiclimate/.translations/ru.json +++ b/homeassistant/components/ambiclimate/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "access_token": "\u041f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043f\u0440\u043e\u0438\u0437\u043e\u0448\u043b\u0430 \u043e\u0448\u0438\u0431\u043a\u0430.", - "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c Ambi Climate \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", + "already_setup": "\u0423\u0447\u0435\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", "no_config": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Ambiclimate \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/components/ambiclimate/)." }, "create_entry": { diff --git a/homeassistant/components/axis/.translations/fr.json b/homeassistant/components/axis/.translations/fr.json index 24afb4a226c..608e12d020a 100644 --- a/homeassistant/components/axis/.translations/fr.json +++ b/homeassistant/components/axis/.translations/fr.json @@ -12,6 +12,7 @@ "device_unavailable": "L'appareil n'est pas disponible", "faulty_credentials": "Mauvaises informations d'identification de l'utilisateur" }, + "flow_title": "Appareil Axis: {name} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index 1128ad30cf5..0345862b865 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis." }, "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "device_unavailable": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e.", "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435." diff --git a/homeassistant/components/cert_expiry/.translations/ca.json b/homeassistant/components/cert_expiry/.translations/ca.json index 25c0b26fafc..f1df9a06be1 100644 --- a/homeassistant/components/cert_expiry/.translations/ca.json +++ b/homeassistant/components/cert_expiry/.translations/ca.json @@ -4,15 +4,17 @@ "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada" }, "error": { + "certificate_error": "El certificat no ha pogut ser validat", "certificate_fetch_failed": "No s'ha pogut obtenir el certificat des d'aquesta combinaci\u00f3 d'amfitri\u00f3 i port", "connection_timeout": "S'ha acabat el temps d'espera durant la connexi\u00f3 amb l'amfitri\u00f3.", "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada", - "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3" + "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3", + "wrong_host": "El certificat no coincideix amb el nom de l'amfitri\u00f3" }, "step": { "user": { "data": { - "host": "Nom d'amfitri\u00f3 del certificat", + "host": "Nom de l'amfitri\u00f3 del certificat", "name": "Nom del certificat", "port": "Port del certificat" }, diff --git a/homeassistant/components/cert_expiry/.translations/da.json b/homeassistant/components/cert_expiry/.translations/da.json index 667ab5fa4e3..c95a56320c9 100644 --- a/homeassistant/components/cert_expiry/.translations/da.json +++ b/homeassistant/components/cert_expiry/.translations/da.json @@ -4,10 +4,12 @@ "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret" }, "error": { + "certificate_error": "Certifikatet kunne ikke valideres", "certificate_fetch_failed": "Kan ikke hente certifikat fra denne v\u00e6rt- og portkombination", "connection_timeout": "Timeout ved tilslutning til denne v\u00e6rt", "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret", - "resolve_failed": "V\u00e6rten kunne ikke findes" + "resolve_failed": "V\u00e6rten kunne ikke findes", + "wrong_host": "Certifikatet stemmer ikke overens med v\u00e6rtsnavnet" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index 873dfee9a92..19e237a6d05 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -4,10 +4,12 @@ "host_port_exists": "This host and port combination is already configured" }, "error": { + "certificate_error": "Certificate could not be validated", "certificate_fetch_failed": "Can not fetch certificate from this host and port combination", "connection_timeout": "Timeout when connecting to this host", "host_port_exists": "This host and port combination is already configured", - "resolve_failed": "This host can not be resolved" + "resolve_failed": "This host can not be resolved", + "wrong_host": "Certificate does not match hostname" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/fr.json b/homeassistant/components/cert_expiry/.translations/fr.json index a3536902c76..9e7df5564a2 100644 --- a/homeassistant/components/cert_expiry/.translations/fr.json +++ b/homeassistant/components/cert_expiry/.translations/fr.json @@ -4,10 +4,12 @@ "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e" }, "error": { + "certificate_error": "Le certificat n'a pas pu \u00eatre valid\u00e9", "certificate_fetch_failed": "Impossible de r\u00e9cup\u00e9rer le certificat de cette combinaison h\u00f4te / port", "connection_timeout": "D\u00e9lai d'attente lors de la connexion \u00e0 cet h\u00f4te", "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e", - "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu" + "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu", + "wrong_host": "Le certificat ne correspond pas au nom d'h\u00f4te" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/nl.json b/homeassistant/components/cert_expiry/.translations/nl.json index 40316f008d5..0544c8c02c1 100644 --- a/homeassistant/components/cert_expiry/.translations/nl.json +++ b/homeassistant/components/cert_expiry/.translations/nl.json @@ -4,10 +4,12 @@ "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd" }, "error": { + "certificate_error": "Certificaat kon niet worden gevalideerd", "certificate_fetch_failed": "Kan certificaat niet ophalen van deze combinatie van host en poort", "connection_timeout": "Time-out bij verbinding maken met deze host", "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd", - "resolve_failed": "Deze host kon niet gevonden worden" + "resolve_failed": "Deze host kon niet gevonden worden", + "wrong_host": "Certificaat komt niet overeen met hostnaam" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/no.json b/homeassistant/components/cert_expiry/.translations/no.json index 73e899106c1..fc2e98b725d 100644 --- a/homeassistant/components/cert_expiry/.translations/no.json +++ b/homeassistant/components/cert_expiry/.translations/no.json @@ -4,10 +4,12 @@ "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert" }, "error": { + "certificate_error": "Sertifikatet kunne ikke valideres", "certificate_fetch_failed": "Kan ikke hente sertifikat fra denne verts- og portkombinasjonen", "connection_timeout": "Tidsavbrudd n\u00e5r du kobler til denne verten", "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert", - "resolve_failed": "Denne verten kan ikke l\u00f8ses" + "resolve_failed": "Denne verten kan ikke l\u00f8ses", + "wrong_host": "Sertifikatet samsvarer ikke med vertsnavn" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json index f9f9e2063be..8c0f230382a 100644 --- a/homeassistant/components/cert_expiry/.translations/ru.json +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -4,15 +4,17 @@ "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430." }, "error": { + "certificate_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442.", "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430.", "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442." + "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442.", + "wrong_host": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u043c\u0443 \u0438\u043c\u0435\u043d\u0438." }, "step": { "user": { "data": { - "host": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430", + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "port": "\u041f\u043e\u0440\u0442" }, diff --git a/homeassistant/components/coolmaster/.translations/en.json b/homeassistant/components/coolmaster/.translations/en.json new file mode 100644 index 00000000000..6c30efc594a --- /dev/null +++ b/homeassistant/components/coolmaster/.translations/en.json @@ -0,0 +1,23 @@ +{ + "config": { + "error": { + "connection_error": "Failed to connect to CoolMasterNet instance. Please check your host.", + "no_units": "Could not find any HVAC units in CoolMasterNet host." + }, + "step": { + "user": { + "data": { + "cool": "Support cool mode", + "dry": "Support dry mode", + "fan_only": "Support fan only mode", + "heat": "Support heat mode", + "heat_cool": "Support automatic heat/cool mode", + "host": "Host", + "off": "Can be turned off" + }, + "title": "Setup your CoolMasterNet connection details." + } + }, + "title": "CoolMasterNet" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/fr.json b/homeassistant/components/cover/.translations/fr.json new file mode 100644 index 00000000000..95978ed0fa5 --- /dev/null +++ b/homeassistant/components/cover/.translations/fr.json @@ -0,0 +1,10 @@ +{ + "device_automation": { + "condition_type": { + "is_closed": "{entity_name} est ferm\u00e9", + "is_closing": "{entity_name} se ferme", + "is_open": "{entity_name} est ouvert", + "is_opening": "{entity_name} est en train de s'ouvrir" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/ru.json b/homeassistant/components/daikin/.translations/ru.json index 98ab98e6b17..00a517f701f 100644 --- a/homeassistant/components/daikin/.translations/ru.json +++ b/homeassistant/components/daikin/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "device_fail": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", "device_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443." }, diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index d3a8781bb4e..2dc3df17aa9 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "no_bridges": "\u0428\u043b\u044e\u0437\u044b deCONZ \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b.", "not_deconz_bridge": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", diff --git a/homeassistant/components/glances/.translations/fr.json b/homeassistant/components/glances/.translations/fr.json index d7b3dc8a448..0391012c4cd 100644 --- a/homeassistant/components/glances/.translations/fr.json +++ b/homeassistant/components/glances/.translations/fr.json @@ -14,18 +14,23 @@ "name": "Nom", "password": "Mot de passe", "port": "Port", + "ssl": "V\u00e9rifier la certification du syst\u00e8me", "username": "Nom d'utilisateur", - "verify_ssl": "V\u00e9rifier la certification du syst\u00e8me" - } + "verify_ssl": "V\u00e9rifier la certification du syst\u00e8me", + "version": "Glances API Version (2 ou 3)" + }, + "title": "Installation de Glances" } - } + }, + "title": "Glances" }, "options": { "step": { "init": { "data": { "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" - } + }, + "description": "Configurer les options pour Glances" } } } diff --git a/homeassistant/components/glances/.translations/pl.json b/homeassistant/components/glances/.translations/pl.json new file mode 100644 index 00000000000..21052c7acdc --- /dev/null +++ b/homeassistant/components/glances/.translations/pl.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Host jest ju\u017c skonfigurowany." + }, + "error": { + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem", + "wrong_version": "Wersja nieobs\u0142ugiwana (tylko 2 lub 3)" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "ssl": "U\u017cyj SSL/TLS, aby po\u0142\u0105czy\u0107 si\u0119 z systemem Glances", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/ru.json b/homeassistant/components/glances/.translations/ru.json index 597a914a88d..8effcc6ab16 100644 --- a/homeassistant/components/glances/.translations/ru.json +++ b/homeassistant/components/glances/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", diff --git a/homeassistant/components/hangouts/.translations/fr.json b/homeassistant/components/hangouts/.translations/fr.json index 0b6dbfcbe44..13142fee513 100644 --- a/homeassistant/components/hangouts/.translations/fr.json +++ b/homeassistant/components/hangouts/.translations/fr.json @@ -14,6 +14,7 @@ "data": { "2fa": "Code PIN d'authentification \u00e0 2 facteurs" }, + "description": "Vide", "title": "Authentification \u00e0 2 facteurs" }, "user": { @@ -22,6 +23,7 @@ "email": "Adresse e-mail", "password": "Mot de passe" }, + "description": "Vide", "title": "Connexion \u00e0 Google Hangouts" } }, diff --git a/homeassistant/components/heos/.translations/ca.json b/homeassistant/components/heos/.translations/ca.json index 60bd780547c..0987e11430b 100644 --- a/homeassistant/components/heos/.translations/ca.json +++ b/homeassistant/components/heos/.translations/ca.json @@ -12,7 +12,7 @@ "access_token": "Amfitri\u00f3", "host": "Amfitri\u00f3" }, - "description": "Introdueix el nom d'amfitri\u00f3 o l'adre\u00e7a IP d'un dispositiu Heos (preferiblement un connectat a la xarxa per cable).", + "description": "Introdueix el nom de l'amfitri\u00f3 o l'adre\u00e7a IP d'un dispositiu Heos (preferiblement un connectat a la xarxa per cable).", "title": "Connexi\u00f3 amb Heos" } }, diff --git a/homeassistant/components/homematicip_cloud/.translations/ru.json b/homeassistant/components/homematicip_cloud/.translations/ru.json index 3170f4bf6cc..57ab265d1c2 100644 --- a/homeassistant/components/homematicip_cloud/.translations/ru.json +++ b/homeassistant/components/homematicip_cloud/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "connection_aborted": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 HMIP.", "unknown": "\u0412\u043e\u0437\u043d\u0438\u043a\u043b\u0430 \u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index 08fda906ea9..c749a498e44 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "all_configured": "\u0412\u0441\u0435 Philips Hue \u0448\u043b\u044e\u0437\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443.", "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d.", diff --git a/homeassistant/components/lock/.translations/fr.json b/homeassistant/components/lock/.translations/fr.json new file mode 100644 index 00000000000..748a1e9290c --- /dev/null +++ b/homeassistant/components/lock/.translations/fr.json @@ -0,0 +1,13 @@ +{ + "device_automation": { + "action_type": { + "lock": "V\u00e9rouiller {entity_name}", + "open": "Ouvre {entity_name}", + "unlock": "D\u00e9verrouiller {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} est verrouill\u00e9", + "is_unlocked": "{entity_name} est d\u00e9verrouill\u00e9" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json index cfdc6b9a738..edde63d62b4 100644 --- a/homeassistant/components/opentherm_gw/.translations/fr.json +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -24,6 +24,7 @@ "step": { "init": { "data": { + "floor_temperature": "Temp\u00e9rature du sol", "precision": "Pr\u00e9cision" }, "description": "Options pour la passerelle OpenTherm" diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json index f38dd669d24..0719857a7d3 100644 --- a/homeassistant/components/opentherm_gw/.translations/ru.json +++ b/homeassistant/components/opentherm_gw/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "id_exists": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u0435\u0442.", "serial_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", "timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." diff --git a/homeassistant/components/rainmachine/.translations/ru.json b/homeassistant/components/rainmachine/.translations/ru.json index df9adf2d989..afaa55424d2 100644 --- a/homeassistant/components/rainmachine/.translations/ru.json +++ b/homeassistant/components/rainmachine/.translations/ru.json @@ -7,7 +7,7 @@ "step": { "user": { "data": { - "ip_address": "\u0418\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "ip_address": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442" }, diff --git a/homeassistant/components/sensor/.translations/nl.json b/homeassistant/components/sensor/.translations/nl.json index f9cd8475a4c..33a7d837d55 100644 --- a/homeassistant/components/sensor/.translations/nl.json +++ b/homeassistant/components/sensor/.translations/nl.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} batterijniveau", + "is_battery_level": "Huidige batterijniveau {entity_name}", "is_humidity": "{entity_name} vochtigheidsgraad", "is_illuminance": "{entity_name} verlichtingssterkte", "is_power": "{entity_name}\nvermogen", diff --git a/homeassistant/components/solarlog/.translations/ca.json b/homeassistant/components/solarlog/.translations/ca.json new file mode 100644 index 00000000000..6a041c7ea4f --- /dev/null +++ b/homeassistant/components/solarlog/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "No s'ha pogut connectar, verifica l'adre\u00e7a de l'amfitri\u00f3" + }, + "step": { + "user": { + "data": { + "host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP del dispositiu Solar-Log", + "name": "Prefix utilitzat pels sensors de Solar-Log" + }, + "title": "Configuraci\u00f3 de la connexi\u00f3 amb Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/da.json b/homeassistant/components/solarlog/.translations/da.json new file mode 100644 index 00000000000..a344832c61c --- /dev/null +++ b/homeassistant/components/solarlog/.translations/da.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheden er allerede konfigureret" + }, + "error": { + "already_configured": "Enheden er allerede konfigureret", + "cannot_connect": "Kunne ikke oprette forbindelse, verificer v\u00e6rtsadressen" + }, + "step": { + "user": { + "data": { + "host": "V\u00e6rtsnavnet eller ip-adressen p\u00e5 din Solar-Log-enhed", + "name": "Pr\u00e6fikset, der skal bruges til dine Solar-Log sensorer" + }, + "title": "Angiv dit Solar-Log forbindelse" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/en.json b/homeassistant/components/solarlog/.translations/en.json index 5399d5176c9..f1396045819 100644 --- a/homeassistant/components/solarlog/.translations/en.json +++ b/homeassistant/components/solarlog/.translations/en.json @@ -1,21 +1,21 @@ { - "config": { - "title": "Solar-Log", - "step": { - "user": { - "title": "Define your Solar-Log connection", - "data": { - "host": "The hostname or ip-address of your Solar-Log device", - "name": "The prefix to be used for your Solar-Log sensors" - } - } - }, - "error": { - "already_configured": "Device is already configured", - "cannot_connect": "Failed to connect, please verify host address" - }, - "abort": { - "already_configured": "Device is already configured" + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect, please verify host address" + }, + "step": { + "user": { + "data": { + "host": "The hostname or ip-address of your Solar-Log device", + "name": "The prefix to be used for your Solar-Log sensors" + }, + "title": "Define your Solar-Log connection" + } + }, + "title": "Solar-Log" } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/fr.json b/homeassistant/components/solarlog/.translations/fr.json new file mode 100644 index 00000000000..0f1b4944ed9 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "cannot_connect": "\u00c9chec de la connexion, veuillez v\u00e9rifier l'adresse de l'h\u00f4te." + }, + "step": { + "user": { + "data": { + "host": "Le nom d'h\u00f4te ou l'adresse IP de votre p\u00e9riph\u00e9rique Solar-Log", + "name": "Le pr\u00e9fixe \u00e0 utiliser pour vos capteurs Solar-Log" + }, + "title": "D\u00e9finissez votre connexion Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/nl.json b/homeassistant/components/solarlog/.translations/nl.json new file mode 100644 index 00000000000..3965f71e992 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/nl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "already_configured": "Apparaat is al geconfigureerd", + "cannot_connect": "Verbinding mislukt, controleer het host-adres" + }, + "step": { + "user": { + "data": { + "host": "De hostnaam of het IP-adres van uw Solar-Log apparaat", + "name": "Het voorvoegsel dat moet worden gebruikt voor uw Solar-Log sensoren" + }, + "title": "Definieer uw Solar-Log verbinding" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/no.json b/homeassistant/components/solarlog/.translations/no.json new file mode 100644 index 00000000000..017e886c817 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Kunne ikke koble til, vennligst bekreft vertsadresse" + }, + "step": { + "user": { + "data": { + "host": "Vertsnavnet eller ip-adressen til din Solar-Log-enhet", + "name": "Prefikset som skal brukes til dine Solar-Log sensorer" + }, + "title": "Definer din Solar-Log tilkobling" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/ru.json b/homeassistant/components/solarlog/.translations/ru.json new file mode 100644 index 00000000000..7f40935e5a5 --- /dev/null +++ b/homeassistant/components/solarlog/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 Solar-Log" + }, + "title": "Solar-Log" + } + }, + "title": "Solar-Log" + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/.translations/ru.json b/homeassistant/components/tradfri/.translations/ru.json index c9121862caf..2e3dc8331be 100644 --- a/homeassistant/components/tradfri/.translations/ru.json +++ b/homeassistant/components/tradfri/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430 \u0443\u0436\u0435 \u043d\u0430\u0447\u0430\u0442\u0430." }, "error": { diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index 45c16be36e2..aa8b99a4914 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -1,34 +1,42 @@ { "config": { - "title": "Transmission", - "step": { - "user": { - "title": "Setup Transmission Client", - "data": { - "name": "Name", - "host": "Host", - "username": "Username", - "password": "Password", - "port": "Port" - } - } + "abort": { + "already_configured": "Host is already configured.", + "one_instance_allowed": "Only a single instance is necessary." }, "error": { + "cannot_connect": "Unable to Connect to host", "name_exists": "Name already exists", - "wrong_credentials": "Wrong username or password", - "cannot_connect": "Unable to Connect to host" + "wrong_credentials": "Wrong username or password" }, - "abort": { - "already_configured": "Host is already configured." - } + "step": { + "options": { + "data": { + "scan_interval": "Update frequency" + }, + "title": "Configure Options" + }, + "user": { + "data": { + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port", + "username": "Username" + }, + "title": "Setup Transmission Client" + } + }, + "title": "Transmission" }, "options": { "step": { "init": { - "title": "Configure options for Transmission", "data": { "scan_interval": "Update frequency" - } + }, + "description": "Configure options for Transmission", + "title": "Configure options for Transmission" } } } diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index c40b7822073..0a100be0a11 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -33,6 +33,12 @@ "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" } }, + "init": { + "data": { + "one": "Vide", + "other": "Vide" + } + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau" diff --git a/homeassistant/components/upnp/.translations/fr.json b/homeassistant/components/upnp/.translations/fr.json index a87ea9ec9c7..6864658b379 100644 --- a/homeassistant/components/upnp/.translations/fr.json +++ b/homeassistant/components/upnp/.translations/fr.json @@ -8,6 +8,10 @@ "no_sensors_or_port_mapping": "Activer au moins les capteurs ou la cartographie des ports", "single_instance_allowed": "Une seule configuration UPnP / IGD est n\u00e9cessaire." }, + "error": { + "one": "Vide", + "other": "Vide" + }, "step": { "confirm": { "description": "Voulez-vous configurer UPnP / IGD?", diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index b4adac8e997..9b1ba025d7c 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -54,14 +54,14 @@ "device_shaken": "Appareil secou\u00e9", "device_slid": "Appareil gliss\u00e9 \"{subtype}\"", "device_tilted": "Dispositif inclin\u00e9", - "remote_button_double_press": "Bouton \"{subtype}\" cliqu\u00e9", + "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9", "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", - "remote_button_quadruple_press": "bouton \" {subtype} \" quadruple clic", - "remote_button_quintuple_press": "bouton \" {subtype} \" quintuple clic", + "remote_button_quadruple_press": "bouton \" {subtype} \" quadruple clics", + "remote_button_quintuple_press": "bouton \" {subtype} \" quintuple clics", "remote_button_short_press": "bouton \" {subtype} \" enfonc\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9", - "remote_button_triple_press": "Bouton\"{sous-type}\" \u00e0 trois clics" + "remote_button_triple_press": "Bouton \"{subtype}\" \u00e0 trois clics" } } } \ No newline at end of file From 3b934166a5300c1ac0cc67fe3b87e6542937d02a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 23 Oct 2019 13:37:01 -0700 Subject: [PATCH 620/639] Bumped version to 0.101.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index cac0386b812..770669b2d82 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 059d2572a28446acc8fd87237d61a9ee0dc314b7 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 24 Oct 2019 12:23:02 -0400 Subject: [PATCH 621/639] Fixes/zha ieee tail (#28160) * Fix ZHA entity_id assignment. * Update tests. --- homeassistant/components/zha/entity.py | 2 +- tests/components/zha/common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 00c3942358e..c11cd405a99 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -40,7 +40,7 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): self._unique_id = unique_id if not skip_entity_id: ieee = zha_device.ieee - ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]]) + ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) self.entity_id = "{}.{}_{}_{}_{}{}".format( self._domain, slugify(zha_device.manufacturer), diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 5f9172749b0..788faaaec73 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -168,7 +168,7 @@ def make_entity_id(domain, device, cluster, use_suffix=True): machine so that we can test state changes. """ ieee = device.ieee - ieeetail = "".join(["%02x" % (o,) for o in ieee[-4:]]) + ieeetail = "".join([f"{o:02x}" for o in ieee[:4]]) entity_id = "{}.{}_{}_{}_{}{}".format( domain, slugify(device.manufacturer), From 8f232f3c692d1042c45ff24faca566bfe519245a Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 24 Oct 2019 20:24:46 +0200 Subject: [PATCH 622/639] Bump aioesphomeapi to 2.4.1 (#28170) * Bump aioesphomeapi to 2.4.1 * Update requirements * Bump to 2.4.2 --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index b2286b8ab67..40691c653f5 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", "requirements": [ - "aioesphomeapi==2.4.0" + "aioesphomeapi==2.4.2" ], "dependencies": [], "zeroconf": ["_esphomelib._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index eacaee7d927..6a6c569b4a1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,7 +139,7 @@ aiobotocore==0.10.2 aiodns==2.0.0 # homeassistant.components.esphome -aioesphomeapi==2.4.0 +aioesphomeapi==2.4.2 # homeassistant.components.freebox aiofreepybox==0.0.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 39903b3606a..51b7ae9d71f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -70,7 +70,7 @@ aioautomatic==0.6.5 aiobotocore==0.10.2 # homeassistant.components.esphome -aioesphomeapi==2.4.0 +aioesphomeapi==2.4.2 # homeassistant.components.emulated_hue # homeassistant.components.http From c64fe19260bedc1eba4e0bae5f8144223d57c34e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 24 Oct 2019 22:36:47 +0200 Subject: [PATCH 623/639] Fix ESPHome stacktraces when removing entity and shutting down (#28185) --- homeassistant/components/esphome/__init__.py | 20 +++++++++++++++++-- .../components/esphome/entry_data.py | 7 +++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index dd4ac699089..a669726ca38 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -95,8 +95,11 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool """Cleanup the socket client on HA stop.""" await _cleanup_instance(hass, entry) + # Use async_listen instead of async_listen_once so that we don't deregister + # the callback twice when shutting down Home Assistant. + # "Unable to remove unknown listener .onetime_listener>" entry_data.cleanup_callbacks.append( - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_stop) + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, on_stop) ) @callback @@ -365,6 +368,7 @@ async def platform_async_setup_entry( """ entry_data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] entry_data.info[component_key] = {} + entry_data.old_info[component_key] = {} entry_data.state[component_key] = {} @callback @@ -390,7 +394,13 @@ async def platform_async_setup_entry( # Remove old entities for info in old_infos.values(): entry_data.async_remove_entity(hass, component_key, info.key) + + # First copy the now-old info into the backup object + entry_data.old_info[component_key] = entry_data.info[component_key] + # Then update the actual info entry_data.info[component_key] = new_infos + + # Add entities to Home Assistant async_add_entities(add_entities) signal = DISPATCHER_ON_LIST.format(entry_id=entry.entry_id) @@ -524,7 +534,13 @@ class EsphomeEntity(Entity): @property def _static_info(self) -> EntityInfo: - return self._entry_data.info[self._component_key][self._key] + # Check if value is in info database. Use a single lookup. + info = self._entry_data.info[self._component_key].get(self._key) + if info is not None: + return info + # This entity is in the removal project and has been removed from .info + # already, look in old_info + return self._entry_data.old_info[self._component_key].get(self._key) @property def _device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index b7f9ad9b347..d916e1a90c8 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -56,6 +56,13 @@ class RuntimeEntryData: reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None) state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + + # A second list of EntityInfo objects + # This is necessary for when an entity is being removed. HA requires + # some static info to be accessible during removal (unique_id, maybe others) + # If an entity can't find anything in the info array, it will look for info here. + old_info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + services = attr.ib(type=Dict[int, "UserService"], factory=dict) available = attr.ib(type=bool, default=False) device_info = attr.ib(type=DeviceInfo, default=None) From 0a5cde7ac3b33a9e20dfdbd8a1e6a80f7ed54ec2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 24 Oct 2019 13:53:30 -0700 Subject: [PATCH 624/639] Bumped version to 0.101.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 770669b2d82..ce00e38bc42 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 05ee15c28c90c22e919f3f621fa198e4ff14a436 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Fri, 25 Oct 2019 11:37:50 -0500 Subject: [PATCH 625/639] Update Plex via websockets (#28158) * Save client identifier from auth for future use * Use websocket events to update Plex * Handle websocket disconnections * Use aiohttp, shut down socket cleanly * Bad rebase fix * Don't connect websocket during config_flow validation, fix tests * Move websocket handling to external library * Close websocket session on HA stop * Use external library, revert unnecessary test change * Async & lint fixes * Clean up websocket stopper on entry unload * Setup websocket in component, pass actual needed object to library --- .coveragerc | 1 + homeassistant/components/plex/__init__.py | 45 ++++++++++++++++----- homeassistant/components/plex/const.py | 3 +- homeassistant/components/plex/manifest.json | 3 +- homeassistant/components/plex/server.py | 7 ++++ requirements_all.txt | 3 ++ requirements_test_all.txt | 3 ++ 7 files changed, 52 insertions(+), 13 deletions(-) diff --git a/.coveragerc b/.coveragerc index f97a7524a21..748ca511dd5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -514,6 +514,7 @@ omit = homeassistant/components/plex/media_player.py homeassistant/components/plex/sensor.py homeassistant/components/plex/server.py + homeassistant/components/plex/websockets.py homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index b6ed3245115..1aaa8a8e3aa 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1,9 +1,9 @@ """Support to embed Plex.""" import asyncio -from datetime import timedelta import logging import plexapi.exceptions +from plexwebsocket import PlexWebsocket import requests.exceptions import voluptuous as vol @@ -16,9 +16,14 @@ from homeassistant.const import ( CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, ) from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .const import ( CONF_USE_EPISODE_ART, @@ -33,8 +38,9 @@ from .const import ( PLATFORMS, PLEX_MEDIA_PLAYER_OPTIONS, PLEX_SERVER_CONFIG, - REFRESH_LISTENERS, + PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, + WEBSOCKETS, ) from .server import PlexServer @@ -67,9 +73,7 @@ _LOGGER = logging.getLogger(__package__) def setup(hass, config): """Set up the Plex component.""" - hass.data.setdefault( - PLEX_DOMAIN, {SERVERS: {}, REFRESH_LISTENERS: {}, DISPATCHERS: {}} - ) + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, DISPATCHERS: {}, WEBSOCKETS: {}}) plex_config = config.get(PLEX_DOMAIN, {}) if plex_config: @@ -136,7 +140,6 @@ async def async_setup_entry(hass, entry): ) server_id = plex_server.machine_identifier hass.data[PLEX_DOMAIN][SERVERS][server_id] = plex_server - hass.data[PLEX_DOMAIN][DISPATCHERS][server_id] = [] for platform in PLATFORMS: hass.async_create_task( @@ -145,9 +148,29 @@ async def async_setup_entry(hass, entry): entry.add_update_listener(async_options_updated) - hass.data[PLEX_DOMAIN][REFRESH_LISTENERS][server_id] = async_track_time_interval( - hass, lambda now: plex_server.update_platforms(), timedelta(seconds=10) + unsub = async_dispatcher_connect( + hass, + PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id), + plex_server.update_platforms, ) + hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) + + def update_plex(): + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + + session = async_get_clientsession(hass) + websocket = PlexWebsocket(plex_server.plex_server, update_plex, session) + hass.loop.create_task(websocket.listen()) + hass.data[PLEX_DOMAIN][WEBSOCKETS][server_id] = websocket + + def close_websocket_session(_): + websocket.close() + + unsub = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, close_websocket_session + ) + hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) return True @@ -156,8 +179,8 @@ async def async_unload_entry(hass, entry): """Unload a config entry.""" server_id = entry.data[CONF_SERVER_IDENTIFIER] - cancel = hass.data[PLEX_DOMAIN][REFRESH_LISTENERS].pop(server_id) - cancel() + websocket = hass.data[PLEX_DOMAIN][WEBSOCKETS].pop(server_id) + websocket.close() dispatchers = hass.data[PLEX_DOMAIN][DISPATCHERS].pop(server_id) for unsub in dispatchers: diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 0d512101e11..d3c79e60bc4 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -10,8 +10,8 @@ DEFAULT_VERIFY_SSL = True DISPATCHERS = "dispatchers" PLATFORMS = ["media_player", "sensor"] -REFRESH_LISTENERS = "refresh_listeners" SERVERS = "servers" +WEBSOCKETS = "websockets" PLEX_CONFIG_FILE = "plex.conf" PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" @@ -19,6 +19,7 @@ PLEX_SERVER_CONFIG = "server_config" PLEX_NEW_MP_SIGNAL = "plex_new_mp_signal.{}" PLEX_UPDATE_MEDIA_PLAYER_SIGNAL = "plex_update_mp_signal.{}" +PLEX_UPDATE_PLATFORMS_SIGNAL = "plex_update_platforms_signal.{}" PLEX_UPDATE_SENSOR_SIGNAL = "plex_update_sensor_signal.{}" CONF_CLIENT_IDENTIFIER = "client_id" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 3c570a0e64c..90ae305148e 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -5,7 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ "plexapi==3.0.6", - "plexauth==0.0.5" + "plexauth==0.0.5", + "plexwebsocket==0.0.1" ], "dependencies": [ "http" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index c0461ee0f54..e6f77a310f1 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -103,6 +103,8 @@ class PlexServer: def update_platforms(self): """Update the platform entities.""" + _LOGGER.debug("Updating devices") + available_clients = {} new_clients = set() @@ -164,6 +166,11 @@ class PlexServer: sessions, ) + @property + def plex_server(self): + """Return the plexapi PlexServer instance.""" + return self._plex_server + @property def friendly_name(self): """Return name of connected Plex server.""" diff --git a/requirements_all.txt b/requirements_all.txt index 6a6c569b4a1..8f5e83a8e67 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -973,6 +973,9 @@ plexapi==3.0.6 # homeassistant.components.plex plexauth==0.0.5 +# homeassistant.components.plex +plexwebsocket==0.0.1 + # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 51b7ae9d71f..0af9b338987 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -345,6 +345,9 @@ plexapi==3.0.6 # homeassistant.components.plex plexauth==0.0.5 +# homeassistant.components.plex +plexwebsocket==0.0.1 + # homeassistant.components.mhz19 # homeassistant.components.serial_pm pmsensor==0.4 From 4df6b3c76a793495f1425395e215dfdec17c6fbd Mon Sep 17 00:00:00 2001 From: SukramJ Date: Fri, 25 Oct 2019 01:41:13 +0200 Subject: [PATCH 626/639] Partially revert tensorflow import move (#28184) * Revert "Refactor imports for tensorflow (#27617)" This reverts commit 5a83a92390e8a3255885198c80622556f886b9b3. * move only some imports to top * fix lint * add comments --- .../components/tensorflow/image_processing.py | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/tensorflow/image_processing.py b/homeassistant/components/tensorflow/image_processing.py index 1f49888cb95..ea73d52fe4a 100644 --- a/homeassistant/components/tensorflow/image_processing.py +++ b/homeassistant/components/tensorflow/image_processing.py @@ -1,23 +1,12 @@ """Support for performing TensorFlow classification on images.""" +import io import logging import os import sys -import io -import voluptuous as vol + from PIL import Image, ImageDraw import numpy as np - -try: - import cv2 -except ImportError: - cv2 = None - -try: - # Verify that the TensorFlow Object Detection API is pre-installed - import tensorflow as tf # noqa - from object_detection.utils import label_map_util # noqa -except ImportError: - label_map_util = None +import voluptuous as vol from homeassistant.components.image_processing import ( CONF_CONFIDENCE, @@ -98,8 +87,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # append custom model path to sys.path sys.path.append(model_dir) - os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" - if label_map_util is None: + try: + # Verify that the TensorFlow Object Detection API is pre-installed + # pylint: disable=unused-import,unused-variable + os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" + # These imports shouldn't be moved to the top, because they depend on code from the model_dir. + # (The model_dir is created during the manual setup process. See integration docs.) + import tensorflow as tf # noqa + from object_detection.utils import label_map_util # noqa + except ImportError: + # pylint: disable=line-too-long _LOGGER.error( "No TensorFlow Object Detection library found! Install or compile " "for your system following instructions here: " @@ -107,7 +104,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ) # noqa return - if cv2 is None: + try: + # Display warning that PIL will be used if no OpenCV is found. + # pylint: disable=unused-import,unused-variable + import cv2 # noqa + except ImportError: _LOGGER.warning( "No OpenCV library found. TensorFlow will process image with " "PIL at reduced resolution" @@ -282,7 +283,13 @@ class TensorFlowImageProcessor(ImageProcessingEntity): def process_image(self, image): """Process the image.""" - if cv2 is None: + try: + import cv2 # pylint: disable=import-error + + img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) + inp = img[:, :, [2, 1, 0]] # BGR->RGB + inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) + except ImportError: img = Image.open(io.BytesIO(bytearray(image))).convert("RGB") img.thumbnail((460, 460), Image.ANTIALIAS) img_width, img_height = img.size @@ -292,10 +299,6 @@ class TensorFlowImageProcessor(ImageProcessingEntity): .astype(np.uint8) ) inp_expanded = np.expand_dims(inp, axis=0) - else: - img = cv2.imdecode(np.asarray(bytearray(image)), cv2.IMREAD_UNCHANGED) - inp = img[:, :, [2, 1, 0]] # BGR->RGB - inp_expanded = inp.reshape(1, inp.shape[0], inp.shape[1], 3) image_tensor = self._graph.get_tensor_by_name("image_tensor:0") boxes = self._graph.get_tensor_by_name("detection_boxes:0") From 637a16799f61f71231dde1e85c8df3b1a8aa2d96 Mon Sep 17 00:00:00 2001 From: gngj Date: Fri, 25 Oct 2019 20:42:23 +0300 Subject: [PATCH 627/639] Fix microsoft tts (#28199) * Update pycsspeechtts From 1.0.2 to 1.0.3 as the old one is using an api that doesn't work * Give a option to choose region Api is now region dependent, so gave it a config --- homeassistant/components/microsoft/manifest.json | 2 +- homeassistant/components/microsoft/tts.py | 11 +++++++++-- requirements_all.txt | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/microsoft/manifest.json b/homeassistant/components/microsoft/manifest.json index 16ae94c212e..5834897ee90 100644 --- a/homeassistant/components/microsoft/manifest.json +++ b/homeassistant/components/microsoft/manifest.json @@ -3,7 +3,7 @@ "name": "Microsoft", "documentation": "https://www.home-assistant.io/integrations/microsoft", "requirements": [ - "pycsspeechtts==1.0.2" + "pycsspeechtts==1.0.3" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/microsoft/tts.py b/homeassistant/components/microsoft/tts.py index 3536c788bb9..d214f6648dd 100644 --- a/homeassistant/components/microsoft/tts.py +++ b/homeassistant/components/microsoft/tts.py @@ -14,6 +14,7 @@ CONF_RATE = "rate" CONF_VOLUME = "volume" CONF_PITCH = "pitch" CONF_CONTOUR = "contour" +CONF_REGION = "region" _LOGGER = logging.getLogger(__name__) @@ -72,6 +73,7 @@ DEFAULT_RATE = 0 DEFAULT_VOLUME = 0 DEFAULT_PITCH = "default" DEFAULT_CONTOUR = "" +DEFAULT_REGION = "eastus" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -87,6 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ), vol.Optional(CONF_PITCH, default=DEFAULT_PITCH): cv.string, vol.Optional(CONF_CONTOUR, default=DEFAULT_CONTOUR): cv.string, + vol.Optional(CONF_REGION, default=DEFAULT_REGION): cv.string, } ) @@ -102,13 +105,16 @@ def get_engine(hass, config): config[CONF_VOLUME], config[CONF_PITCH], config[CONF_CONTOUR], + config[CONF_REGION], ) class MicrosoftProvider(Provider): """The Microsoft speech API provider.""" - def __init__(self, apikey, lang, gender, ttype, rate, volume, pitch, contour): + def __init__( + self, apikey, lang, gender, ttype, rate, volume, pitch, contour, region + ): """Init Microsoft TTS service.""" self._apikey = apikey self._lang = lang @@ -119,6 +125,7 @@ class MicrosoftProvider(Provider): self._volume = f"{volume}%" self._pitch = pitch self._contour = contour + self._region = region self.name = "Microsoft" @property @@ -138,7 +145,7 @@ class MicrosoftProvider(Provider): from pycsspeechtts import pycsspeechtts try: - trans = pycsspeechtts.TTSTranslator(self._apikey) + trans = pycsspeechtts.TTSTranslator(self._apikey, self._region) data = trans.speak( language=language, gender=self._gender, diff --git a/requirements_all.txt b/requirements_all.txt index 8f5e83a8e67..9b73d46dfbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1132,7 +1132,7 @@ pycomfoconnect==0.3 pycoolmasternet==0.0.4 # homeassistant.components.microsoft -pycsspeechtts==1.0.2 +pycsspeechtts==1.0.3 # homeassistant.components.cups # pycups==1.9.73 From 524f5a7264d11a75ec19e439d1671d0e65a05500 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 25 Oct 2019 19:20:42 +0200 Subject: [PATCH 628/639] Updated frontend to 20191025.0 (#28208) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index ae53b972dca..b23d40605dd 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191023.0" + "home-assistant-frontend==20191025.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d2f10c891a9..682d9b1e1db 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 9b73d46dfbf..9508423927e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0af9b338987..1783f192b24 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191023.0 +home-assistant-frontend==20191025.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From c456b725fdebd5d32bc29f4367d3e7747c1c8b17 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 25 Oct 2019 10:49:42 -0700 Subject: [PATCH 629/639] Bumped version to 0.101.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ce00e38bc42..4d858deec87 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 82ed84ba43a6ce9ac950495d1877d7cbb4d1cb3f Mon Sep 17 00:00:00 2001 From: michaeldavie Date: Sat, 26 Oct 2019 16:27:21 -0400 Subject: [PATCH 630/639] Bump env_canada to 0.0.27 (#28239) --- homeassistant/components/environment_canada/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index c62e1e356b6..4d1a8094663 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -3,7 +3,7 @@ "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", "requirements": [ - "env_canada==0.0.25" + "env_canada==0.0.27" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 9508423927e..cd4a6fceb85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -456,7 +456,7 @@ enocean==0.50 enturclient==0.2.0 # homeassistant.components.environment_canada -env_canada==0.0.25 +env_canada==0.0.27 # homeassistant.components.envirophat # envirophat==0.0.6 From 3cedee3feaced5752093b8f48aaf90bf1f027d24 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Oct 2019 06:55:42 +0800 Subject: [PATCH 631/639] Add above and below to sensor condition extra_fields (#27364) * Add above and below to sensor condition extra_fields * Change unit_of_measurement to suffix in extra_fields * Check if sensor has unit when getting capabilities * Improve tests --- .../components/device_automation/__init__.py | 6 +- .../components/sensor/device_condition.py | 27 +++++++ .../components/sensor/device_trigger.py | 8 +- .../sensor/test_device_condition.py | 81 +++++++++++++++++++ .../components/sensor/test_device_trigger.py | 35 ++++++++ 5 files changed, 155 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/device_automation/__init__.py b/homeassistant/components/device_automation/__init__.py index 0be1c3eb1dd..80e64033295 100644 --- a/homeassistant/components/device_automation/__init__.py +++ b/homeassistant/components/device_automation/__init__.py @@ -156,7 +156,11 @@ async def _async_get_device_automation_capabilities(hass, automation_type, autom # The device automation has no capabilities return {} - capabilities = await getattr(platform, function_name)(hass, automation) + try: + capabilities = await getattr(platform, function_name)(hass, automation) + except InvalidDeviceAutomationConfig: + return {} + capabilities = capabilities.copy() extra_fields = capabilities.get("extra_fields") diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index 26479807991..259fb5dbab9 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -2,6 +2,9 @@ from typing import Dict, List import voluptuous as vol +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.core import HomeAssistant from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -141,3 +144,27 @@ def async_condition_from_config( numeric_state_config[condition.CONF_BELOW] = config[CONF_BELOW] return condition.async_numeric_state_from_config(numeric_state_config) + + +async def async_get_condition_capabilities(hass, config): + """List condition capabilities.""" + state = hass.states.get(config[CONF_ENTITY_ID]) + unit_of_measurement = ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None + ) + + if not state or not unit_of_measurement: + raise InvalidDeviceAutomationConfig + + return { + "extra_fields": vol.Schema( + { + vol.Optional( + CONF_ABOVE, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + vol.Optional( + CONF_BELOW, description={"suffix": unit_of_measurement} + ): vol.Coerce(float), + } + ) + } diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index b462124165a..73e55340da9 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -3,6 +3,9 @@ import voluptuous as vol import homeassistant.components.automation.numeric_state as numeric_state_automation from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, @@ -146,9 +149,12 @@ async def async_get_trigger_capabilities(hass, config): """List trigger capabilities.""" state = hass.states.get(config[CONF_ENTITY_ID]) unit_of_measurement = ( - state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else "" + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) if state else None ) + if not state or not unit_of_measurement: + raise InvalidDeviceAutomationConfig + return { "extra_fields": vol.Schema( { diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index e28e487f4ef..f3ff15c3ad9 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -14,6 +14,7 @@ from tests.common import ( mock_device_registry, mock_registry, async_get_device_automations, + async_get_device_automation_capabilities, ) from tests.testing_config.custom_components.test.sensor import DEVICE_CLASSES @@ -73,6 +74,86 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions +async def test_get_condition_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES["battery"].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_capabilities = { + "extra_fields": [ + { + "description": {"suffix": "%"}, + "name": "above", + "optional": True, + "type": "float", + }, + { + "description": {"suffix": "%"}, + "name": "below", + "optional": True, + "type": "float", + }, + ] + } + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert len(conditions) == 1 + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + +async def test_get_condition_capabilities_none(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor condition.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + conditions = [ + { + "condition": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": "sensor.beer", + "type": "is_battery_level", + }, + { + "condition": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": platform.ENTITIES["none"].entity_id, + "type": "is_battery_level", + }, + ] + + expected_capabilities = {} + for condition in conditions: + capabilities = await async_get_device_automation_capabilities( + hass, "condition", condition + ) + assert capabilities == expected_capabilities + + async def test_if_state_not_above_below(hass, calls, caplog): """Test for bad value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index a21839fcebc..b7a921fff18 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -124,6 +124,41 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities +async def test_get_trigger_capabilities_none(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a sensor trigger.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + triggers = [ + { + "platform": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": "sensor.beer", + "type": "is_battery_level", + }, + { + "platform": "device", + "device_id": "8770c43885354d5fa27604db6817f63f", + "domain": "sensor", + "entity_id": platform.ENTITIES["none"].entity_id, + "type": "is_battery_level", + }, + ] + + expected_capabilities = {} + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + async def test_if_fires_not_on_above_below(hass, calls, caplog): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") From 4a25bab1b30fcdecc125762ea63548ea608266a9 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 26 Oct 2019 04:40:05 +0800 Subject: [PATCH 632/639] Fix broken deconz trigger (#28211) --- homeassistant/components/deconz/device_trigger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 27ff6fcd590..2d097d30c0b 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -235,6 +235,7 @@ async def async_attach_trigger(hass, config, action, automation_info): event_id = deconz_event.serial event_config = { + event.CONF_PLATFORM: "event", event.CONF_EVENT_TYPE: CONF_DECONZ_EVENT, event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: event_id, CONF_EVENT: trigger}, } From 0e2b55e60e7fb65bf4a1854b6f1e4acd3a20e883 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 28 Oct 2019 12:39:37 -0500 Subject: [PATCH 633/639] Bump library to 0.0.3 (#28294) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 90ae305148e..8edccda75e0 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "plexapi==3.0.6", "plexauth==0.0.5", - "plexwebsocket==0.0.1" + "plexwebsocket==0.0.3" ], "dependencies": [ "http" diff --git a/requirements_all.txt b/requirements_all.txt index cd4a6fceb85..869c6ea90c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -974,7 +974,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.1 +plexwebsocket==0.0.3 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1783f192b24..72aa75a4676 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -346,7 +346,7 @@ plexapi==3.0.6 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.1 +plexwebsocket==0.0.3 # homeassistant.components.mhz19 # homeassistant.components.serial_pm From 070790ccc9f613eb1596897031d09cfe4a444b3a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 28 Oct 2019 11:28:45 -0700 Subject: [PATCH 634/639] Bumped version to 0.101.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4d858deec87..2aa5e97fa94 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From f021e5832a0b7f01892e0281e1072a3bf877c847 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 29 Oct 2019 12:05:05 +0100 Subject: [PATCH 635/639] Cleanup not needed websocket flags for ingress (#28295) --- homeassistant/components/hassio/ingress.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py index 4ecb9a8419f..53235f80dca 100644 --- a/homeassistant/components/hassio/ingress.py +++ b/homeassistant/components/hassio/ingress.py @@ -167,7 +167,14 @@ def _init_header( # filter flags for name, value in request.headers.items(): - if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_ENCODING): + if name in ( + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_ENCODING, + hdrs.SEC_WEBSOCKET_EXTENSIONS, + hdrs.SEC_WEBSOCKET_PROTOCOL, + hdrs.SEC_WEBSOCKET_VERSION, + hdrs.SEC_WEBSOCKET_KEY, + ): continue headers[name] = value From c104efc18d28acc765fbac6c6b50354dbfed57c4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 29 Oct 2019 16:30:33 +0100 Subject: [PATCH 636/639] Updated frontend to 20191025.1 (#28327) --- homeassistant/components/frontend/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b23d40605dd..aa7ad8b18f9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -3,7 +3,7 @@ "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", "requirements": [ - "home-assistant-frontend==20191025.0" + "home-assistant-frontend==20191025.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 682d9b1e1db..85bb00ce6eb 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,7 +11,7 @@ contextvars==2.4;python_version<"3.7" cryptography==2.8 distro==1.4.0 hass-nabucasa==0.22 -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 importlib-metadata==0.23 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 869c6ea90c3..df9cafe7aa0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -646,7 +646,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72aa75a4676..36f423860d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -242,7 +242,7 @@ hole==0.5.0 holidays==0.9.11 # homeassistant.components.frontend -home-assistant-frontend==20191025.0 +home-assistant-frontend==20191025.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 From cf6d11db8d6ab2be890ac06e03765bc28dd085fa Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 29 Oct 2019 16:17:34 -0700 Subject: [PATCH 637/639] Bumped version to 0.101.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2aa5e97fa94..7367d9723f6 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0b4" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1) From 8aee92347f12fcb677fbce734ef41b49e3bb13ad Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 30 Oct 2019 07:57:40 +0100 Subject: [PATCH 638/639] Fix KeyError in decora setup (#28279) * Imported homeassistant.util and slugified address if no name is specified * Added a custom validator function in case name is not set in config * Removed logger.debug line only used for testing --- homeassistant/components/decora/light.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/decora/light.py b/homeassistant/components/decora/light.py index 4d2d10ccbd5..6ca427f2476 100644 --- a/homeassistant/components/decora/light.py +++ b/homeassistant/components/decora/light.py @@ -1,4 +1,5 @@ """Support for Decora dimmers.""" +import copy from functools import wraps import logging import time @@ -15,17 +16,34 @@ from homeassistant.components.light import ( ) from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME import homeassistant.helpers.config_validation as cv +import homeassistant.util as util _LOGGER = logging.getLogger(__name__) SUPPORT_DECORA_LED = SUPPORT_BRIGHTNESS + +def _name_validator(config): + """Validate the name.""" + config = copy.deepcopy(config) + for address, device_config in config[CONF_DEVICES].items(): + if CONF_NAME not in device_config: + device_config[CONF_NAME] = util.slugify(address) + + return config + + DEVICE_SCHEMA = vol.Schema( {vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_API_KEY): cv.string} ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} +PLATFORM_SCHEMA = vol.Schema( + vol.All( + PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_DEVICES, default={}): {cv.string: DEVICE_SCHEMA}} + ), + _name_validator, + ) ) From 7eceedea108399a738b81eb57ddf9304630a6296 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 30 Oct 2019 19:50:48 +0000 Subject: [PATCH 639/639] Bump version 0.101.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 7367d9723f6..f6f1a4f2de2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 101 -PATCH_VERSION = "0b4" +PATCH_VERSION = "0" __short_version__ = "{}.{}".format(MAJOR_VERSION, MINOR_VERSION) __version__ = "{}.{}".format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 6, 1)