From bd7c0e87d52407cb5fb0a9d1cdae9082340f8183 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 10 Jul 2019 20:49:56 -0700 Subject: [PATCH 01/74] Version bump to 0.96.0b0 --- homeassistant/const.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 79aa735bfe2..090a9f26d2f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,8 +1,8 @@ # coding: utf-8 """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 95 -PATCH_VERSION = '4' +MINOR_VERSION = 96 +PATCH_VERSION = '0b0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 8041339052bee2e1208d025a937b813f979e8681 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 11 Jul 2019 09:15:14 +0200 Subject: [PATCH 02/74] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 0a6618718a2..77091315c6a 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -19,7 +19,6 @@ stages: - stage: 'Validate' jobs: - job: 'VersionValidate' - condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') pool: vmImage: 'ubuntu-latest' steps: @@ -54,7 +53,7 @@ stages: - stage: 'Build' jobs: - job: 'ReleasePython' - condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate')) + condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded()) dependsOn: - 'VersionValidate' pool: @@ -75,7 +74,7 @@ stages: twine upload dist/* --skip-existing displayName: 'Upload pypi' - job: 'ReleaseDocker' - condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('VersionValidate')) + condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded()) dependsOn: - 'VersionValidate' timeoutInMinutes: 240 @@ -127,7 +126,7 @@ stages: - stage: 'Publish' jobs: - job: 'ReleaseHassio' - condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded('ReleaseDocker')) + condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded()) dependsOn: - 'ReleaseDocker' pool: From fcb1783f56700528c1cd29cf2e4251d7ac273b5d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 11 Jul 2019 09:21:58 +0200 Subject: [PATCH 03/74] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 77091315c6a..83e906f3684 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -53,9 +53,6 @@ stages: - stage: 'Build' jobs: - job: 'ReleasePython' - condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded()) - dependsOn: - - 'VersionValidate' pool: vmImage: 'ubuntu-latest' steps: @@ -74,9 +71,6 @@ stages: twine upload dist/* --skip-existing displayName: 'Upload pypi' - job: 'ReleaseDocker' - condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded()) - dependsOn: - - 'VersionValidate' timeoutInMinutes: 240 pool: vmImage: 'ubuntu-latest' @@ -126,9 +120,7 @@ stages: - stage: 'Publish' jobs: - job: 'ReleaseHassio' - condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags'), succeeded()) - dependsOn: - - 'ReleaseDocker' + condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags') pool: vmImage: 'ubuntu-latest' steps: From 3b6b42115206e8eaac0d50863e9c4798d9987ba7 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 11 Jul 2019 09:24:15 +0200 Subject: [PATCH 04/74] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 83e906f3684..79e7381fedb 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -120,7 +120,6 @@ stages: - stage: 'Publish' jobs: - job: 'ReleaseHassio' - condition: and(startsWith(variables['Build.SourceBranch'], 'refs/tags') pool: vmImage: 'ubuntu-latest' steps: From cc7b65a6c8d874b3b84911ba455725d67392e4b3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 12 Jul 2019 09:16:14 +0200 Subject: [PATCH 05/74] Support hass-release inside devcontainer (#25090) --- .devcontainer/Dockerfile | 17 +++++++++++++++-- .gitignore | 4 ++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 423f93f7ec9..8abf28cddff 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -2,11 +2,24 @@ FROM python:3.7 RUN apt-get update \ && apt-get install -y --no-install-recommends \ - libudev-dev libavformat-dev libavcodec-dev libavdevice-dev \ - libavutil-dev libswscale-dev libswresample-dev libavfilter-dev \ + libudev-dev \ + libavformat-dev \ + libavcodec-dev \ + libavdevice-dev \ + libavutil-dev \ + libswscale-dev \ + libswresample-dev \ + libavfilter-dev \ + git \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* +WORKDIR /usr/src + +RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ + && cd hass-release \ + && pip3 install -e . + WORKDIR /workspace # Install Python dependencies from requirements.txt if it exists diff --git a/.gitignore b/.gitignore index 4ab6ebd4a48..9c3afdd9091 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ config2/* tests/testing_config/deps tests/testing_config/home-assistant.log +# hass-release +data/ +.token + # Hide sublime text stuff *.sublime-project *.sublime-workspace From 2b62ea1f0ede043e1a068861b221b0cd2f361c3f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 10 Jul 2019 23:33:38 -0700 Subject: [PATCH 06/74] Do not reverse open/close calls (#24879) --- homeassistant/components/tahoma/cover.py | 26 +++++++++--------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/tahoma/cover.py b/homeassistant/components/tahoma/cover.py index fdeb77dd990..333531c579d 100644 --- a/homeassistant/components/tahoma/cover.py +++ b/homeassistant/components/tahoma/cover.py @@ -19,9 +19,11 @@ ATTR_LOCK_END_TS = 'lock_end_ts' ATTR_LOCK_LEVEL = 'lock_level' ATTR_LOCK_ORIG = 'lock_originator' +HORIZONTAL_AWNING = 'io:HorizontalAwningIOComponent' + TAHOMA_DEVICE_CLASSES = { 'io:ExteriorVenetianBlindIOComponent': DEVICE_CLASS_BLIND, - 'io:HorizontalAwningIOComponent': DEVICE_CLASS_AWNING, + HORIZONTAL_AWNING: DEVICE_CLASS_AWNING, 'io:RollerShutterGenericIOComponent': DEVICE_CLASS_SHUTTER, 'io:RollerShutterUnoIOComponent': DEVICE_CLASS_SHUTTER, 'io:RollerShutterVeluxIOComponent': DEVICE_CLASS_SHUTTER, @@ -130,18 +132,16 @@ class TahomaCover(TahomaDevice, CoverDevice): # _position: 0 is closed, 100 is fully open. # 'core:ClosureState': 100 is closed, 0 is fully open. if self._closure is not None: - if self.tahoma_device.type == 'io:HorizontalAwningIOComponent': + 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 - if self.tahoma_device.type == 'io:HorizontalAwningIOComponent': - self._closed = self._position == 0 - else: - self._closed = self._position == 100 else: self._position = None if 'core:OpenClosedState' in self.tahoma_device.active_states: @@ -160,7 +160,7 @@ class TahomaCover(TahomaDevice, CoverDevice): def set_cover_position(self, **kwargs): """Move the cover to a specific position.""" - if self.tahoma_device.type == 'io:HorizontalAwningIOComponent': + if self.tahoma_device.type == HORIZONTAL_AWNING: self.apply_action('setPosition', kwargs.get(ATTR_POSITION, 0)) else: self.apply_action('setPosition', @@ -206,17 +206,11 @@ class TahomaCover(TahomaDevice, CoverDevice): def open_cover(self, **kwargs): """Open the cover.""" - if self.tahoma_device.type == 'io:HorizontalAwningIOComponent': - self.apply_action('close') - else: - self.apply_action('open') + self.apply_action('open') def close_cover(self, **kwargs): """Close the cover.""" - if self.tahoma_device.type == 'io:HorizontalAwningIOComponent': - self.apply_action('open') - else: - self.apply_action('close') + self.apply_action('close') def stop_cover(self, **kwargs): """Stop the cover.""" @@ -232,7 +226,7 @@ class TahomaCover(TahomaDevice, CoverDevice): 'rts:BlindRTSComponent'): self.apply_action('my') elif self.tahoma_device.type in \ - ('io:HorizontalAwningIOComponent', + (HORIZONTAL_AWNING, 'io:RollerShutterGenericIOComponent', 'io:VerticalExteriorAwningIOComponent'): self.apply_action('stop') From a0e45cce7916d45a7f8c99a2722b0605f9af9bd5 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 12 Jul 2019 00:28:11 +0200 Subject: [PATCH 07/74] Add support for on/off climate (#25026) * Add support for on/off climate * address comments * Add test for sync overwrite * Add more tests --- homeassistant/components/climate/__init__.py | 114 ++++++++++-------- .../components/climate/services.yaml | 14 +++ tests/components/climate/common.py | 25 +++- tests/components/climate/test_init.py | 26 +++- tests/components/demo/test_climate.py | 30 ++++- 5 files changed, 153 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index 369ef6fc838..ba6f15567d9 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -2,13 +2,13 @@ from datetime import timedelta import functools as ft import logging -from typing import Any, Awaitable, Dict, List, Optional +from typing import Any, Dict, List, Optional import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, PRECISION_TENTHS, PRECISION_WHOLE, - STATE_OFF, STATE_ON, TEMP_CELSIUS) + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON, TEMP_CELSIUS) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) @@ -25,7 +25,8 @@ from .const import ( ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP, ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_PRESET_MODES, ATTR_SWING_MODE, ATTR_SWING_MODES, ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_STEP, DOMAIN, HVAC_MODES, + ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_STEP, DOMAIN, HVAC_MODE_COOL, + HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, HVAC_MODES, SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, @@ -49,6 +50,9 @@ CONVERTIBLE_ATTRIBUTE = [ _LOGGER = logging.getLogger(__name__) +TURN_ON_OFF_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, +}) SET_AUX_HEAT_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, vol.Required(ATTR_AUX_HEAT): cv.boolean, @@ -92,6 +96,14 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) + component.async_register_entity_service( + SERVICE_TURN_ON, TURN_ON_OFF_SCHEMA, + 'async_turn_on' + ) + component.async_register_entity_service( + SERVICE_TURN_OFF, TURN_ON_OFF_SCHEMA, + 'async_turn_off' + ) component.async_register_entity_service( SERVICE_SET_HVAC_MODE, SET_HVAC_MODE_SCHEMA, 'async_set_hvac_mode' @@ -338,90 +350,92 @@ class ClimateDevice(Entity): """Set new target temperature.""" raise NotImplementedError() - def async_set_temperature(self, **kwargs) -> Awaitable[None]: - """Set new target temperature. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job( + async def async_set_temperature(self, **kwargs) -> None: + """Set new target temperature.""" + await self.hass.async_add_executor_job( ft.partial(self.set_temperature, **kwargs)) def set_humidity(self, humidity: int) -> None: """Set new target humidity.""" raise NotImplementedError() - def async_set_humidity(self, humidity: int) -> Awaitable[None]: - """Set new target humidity. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_humidity, humidity) + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + await self.hass.async_add_executor_job(self.set_humidity, humidity) def set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" raise NotImplementedError() - def async_set_fan_mode(self, fan_mode: str) -> Awaitable[None]: - """Set new target fan mode. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_fan_mode, fan_mode) + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set new target fan mode.""" + await self.hass.async_add_executor_job(self.set_fan_mode, fan_mode) def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" raise NotImplementedError() - def async_set_hvac_mode(self, hvac_mode: str) -> Awaitable[None]: - """Set new target hvac mode. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_hvac_mode, hvac_mode) + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + await self.hass.async_add_executor_job(self.set_hvac_mode, hvac_mode) def set_swing_mode(self, swing_mode: str) -> None: """Set new target swing operation.""" raise NotImplementedError() - def async_set_swing_mode(self, swing_mode: str) -> Awaitable[None]: - """Set new target swing operation. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_swing_mode, swing_mode) + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new target swing operation.""" + await self.hass.async_add_executor_job(self.set_swing_mode, swing_mode) def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" raise NotImplementedError() - def async_set_preset_mode(self, preset_mode: str) -> Awaitable[None]: - """Set new preset mode. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.set_preset_mode, preset_mode) + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + await self.hass.async_add_executor_job( + self.set_preset_mode, preset_mode) def turn_aux_heat_on(self) -> None: """Turn auxiliary heater on.""" raise NotImplementedError() - def async_turn_aux_heat_on(self) -> Awaitable[None]: - """Turn auxiliary heater on. - - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.turn_aux_heat_on) + async def async_turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_on) def turn_aux_heat_off(self) -> None: """Turn auxiliary heater off.""" raise NotImplementedError() - def async_turn_aux_heat_off(self) -> Awaitable[None]: - """Turn auxiliary heater off. + async def async_turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + await self.hass.async_add_executor_job(self.turn_aux_heat_off) - This method must be run in the event loop and returns a coroutine. - """ - return self.hass.async_add_job(self.turn_aux_heat_off) + async def async_turn_on(self) -> None: + """Turn the entity on.""" + if hasattr(self, 'turn_on'): + # pylint: disable=no-member + await self.hass.async_add_executor_job(self.turn_on) + return + + # Fake turn on + for mode in (HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, HVAC_MODE_COOL): + if mode not in self.hvac_modes: + continue + await self.async_set_hvac_mode(mode) + break + + async def async_turn_off(self) -> None: + """Turn the entity off.""" + if hasattr(self, 'turn_off'): + # pylint: disable=no-member + await self.hass.async_add_executor_job(self.turn_off) + return + + # Fake turn off + if HVAC_MODE_OFF in self.hvac_modes: + await self.async_set_hvac_mode(HVAC_MODE_OFF) @property def supported_features(self) -> int: diff --git a/homeassistant/components/climate/services.yaml b/homeassistant/components/climate/services.yaml index 8969f60cd89..4e9a4a3a4f4 100644 --- a/homeassistant/components/climate/services.yaml +++ b/homeassistant/components/climate/services.yaml @@ -114,3 +114,17 @@ nuheat_resume_program: entity_id: description: Name(s) of entities to change. example: 'climate.kitchen' + +turn_on: + description: Turn climate device on. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' + +turn_off: + description: Turn climate device off. + fields: + entity_id: + description: Name(s) of entities to change. + example: 'climate.kitchen' diff --git a/tests/components/climate/common.py b/tests/components/climate/common.py index 33c42ee1eed..0279f356058 100644 --- a/tests/components/climate/common.py +++ b/tests/components/climate/common.py @@ -10,7 +10,8 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, DOMAIN, SERVICE_SET_AUX_HEAT, SERVICE_SET_FAN_MODE, SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE) -from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_TEMPERATURE, SERVICE_TURN_OFF, SERVICE_TURN_ON) from homeassistant.loader import bind_hass @@ -188,3 +189,25 @@ def set_swing_mode(hass, swing_mode, entity_id=None): data[ATTR_ENTITY_ID] = entity_id hass.services.call(DOMAIN, SERVICE_SET_SWING_MODE, data) + + +async def async_turn_on(hass, entity_id=None): + """Turn on device.""" + data = {} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, data, blocking=True) + + +async def async_turn_off(hass, entity_id=None): + """Turn off device.""" + data = {} + + if entity_id is not None: + data[ATTR_ENTITY_ID] = entity_id + + await hass.services.async_call( + DOMAIN, SERVICE_TURN_OFF, data, blocking=True) diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 744e579a5bc..0c1b7f1ecc0 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -1,9 +1,11 @@ """The tests for the climate component.""" +from unittest.mock import MagicMock import pytest import voluptuous as vol -from homeassistant.components.climate import SET_TEMPERATURE_SCHEMA +from homeassistant.components.climate import ( + SET_TEMPERATURE_SCHEMA, ClimateDevice) from tests.common import async_mock_service @@ -37,3 +39,25 @@ async def test_set_temp_schema(hass, caplog): assert len(calls) == 1 assert calls[-1].data == data + + +async def test_sync_turn_on(hass): + """Test if adding turn_on work.""" + climate = ClimateDevice() + climate.hass = hass + + climate.turn_on = MagicMock() + await climate.async_turn_on() + + assert climate.turn_on.called + + +async def test_sync_turn_off(hass): + """Test if adding turn_on work.""" + climate = ClimateDevice() + climate.hass = hass + + climate.turn_off = MagicMock() + await climate.async_turn_off() + + assert climate.turn_off.called diff --git a/tests/components/demo/test_climate.py b/tests/components/demo/test_climate.py index 44637fa9245..628c9e417b3 100644 --- a/tests/components/demo/test_climate.py +++ b/tests/components/demo/test_climate.py @@ -4,12 +4,12 @@ import pytest import voluptuous as vol from homeassistant.components.climate.const import ( - ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_HVAC_ACTIONS, - ATTR_CURRENT_TEMPERATURE, ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_MODES, + ATTR_AUX_HEAT, ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, ATTR_HUMIDITY, ATTR_HVAC_ACTIONS, ATTR_HVAC_MODES, ATTR_MAX_HUMIDITY, ATTR_MAX_TEMP, ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP, ATTR_PRESET_MODE, ATTR_SWING_MODE, ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, DOMAIN, - HVAC_MODE_COOL, HVAC_MODE_HEAT, PRESET_AWAY, PRESET_ECO) + ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, DOMAIN, HVAC_MODE_COOL, + HVAC_MODE_HEAT, HVAC_MODE_OFF, PRESET_AWAY, PRESET_ECO) from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM @@ -279,3 +279,25 @@ async def test_set_aux_heat_off(hass): state = hass.states.get(ENTITY_CLIMATE) assert state.attributes.get(ATTR_AUX_HEAT) == STATE_OFF + + +async def test_turn_on(hass): + """Test turn on device.""" + await common.async_set_hvac_mode(hass, HVAC_MODE_OFF, ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == HVAC_MODE_OFF + + await common.async_turn_on(hass, ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == HVAC_MODE_HEAT + + +async def test_turn_off(hass): + """Test turn on device.""" + await common.async_set_hvac_mode(hass, HVAC_MODE_HEAT, ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == HVAC_MODE_HEAT + + await common.async_turn_off(hass, ENTITY_CLIMATE) + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == HVAC_MODE_OFF From 53a701b12c19a2dc75463b508b8039b02878e474 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 10 Jul 2019 23:31:03 -0600 Subject: [PATCH 08/74] Change unique_id formula for Notion entities (#25076) * Change unique_id formula for Notion entities * Don't use name --- homeassistant/components/notion/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index afa08def4df..06f2404ec12 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -267,7 +267,8 @@ class NotionEntity(Entity): @property def unique_id(self): """Return a unique, unchanging string that represents this sensor.""" - return self._task_id + task = self._notion.tasks[self._task_id] + return '{0}_{1}'.format(self._sensor_id, task['task_type']) async def _update_bridge_id(self): """Update the entity's bridge ID if it has changed. From 53111f6426cc3433fe1083904059ce319ed9a799 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 11 Jul 2019 17:35:46 +0200 Subject: [PATCH 09/74] Fix powercontrol media player alexa (#25080) --- homeassistant/components/alexa/entities.py | 6 +-- homeassistant/components/alexa/handlers.py | 59 ++++++++++------------ tests/components/alexa/test_smart_home.py | 11 ++++ 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 7caec1b541d..2a6498fdcaf 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -339,16 +339,12 @@ class MediaPlayerCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" yield AlexaEndpointHealth(self.hass, self.entity) + yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if supported & media_player.const.SUPPORT_VOLUME_SET: yield AlexaSpeaker(self.entity) - power_features = (media_player.SUPPORT_TURN_ON | - media_player.SUPPORT_TURN_OFF) - if supported & power_features: - yield AlexaPowerController(self.entity) - step_volume_features = (media_player.const.SUPPORT_VOLUME_MUTE | media_player.const.SUPPORT_VOLUME_STEP) if supported & step_volume_features: diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 3cdd4e741af..b66fbf82c0f 100644 --- a/homeassistant/components/alexa/handlers.py +++ b/homeassistant/components/alexa/handlers.py @@ -4,45 +4,26 @@ import logging import math from homeassistant import core as ha -from homeassistant.util.decorator import Registry -import homeassistant.util.color as color_util -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_TEMPERATURE, - SERVICE_LOCK, - SERVICE_MEDIA_NEXT_TRACK, - SERVICE_MEDIA_PAUSE, - SERVICE_MEDIA_PLAY, - SERVICE_MEDIA_PREVIOUS_TRACK, - SERVICE_MEDIA_STOP, - SERVICE_SET_COVER_POSITION, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - SERVICE_UNLOCK, - SERVICE_VOLUME_DOWN, - SERVICE_VOLUME_MUTE, - SERVICE_VOLUME_SET, - SERVICE_VOLUME_UP, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -from homeassistant.components.climate import const as climate from homeassistant.components import cover, fan, group, light, media_player +from homeassistant.components.climate import const as climate +from homeassistant.const import ( + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, SERVICE_LOCK, + SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_MEDIA_STOP, + SERVICE_SET_COVER_POSITION, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SERVICE_UNLOCK, SERVICE_VOLUME_DOWN, SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, SERVICE_VOLUME_UP, TEMP_CELSIUS, TEMP_FAHRENHEIT) +import homeassistant.util.color as color_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, -) + API_TEMP_UNITS, API_THERMOSTAT_MODES, API_THERMOSTAT_PRESETS, Cause) from .entities import async_get_entities -from .state_report import async_enable_proactive_mode from .errors import ( - AlexaInvalidValueError, - AlexaTempRangeError, - AlexaUnsupportedThermostatModeError, -) + AlexaInvalidValueError, AlexaTempRangeError, + AlexaUnsupportedThermostatModeError) +from .state_report import async_enable_proactive_mode _LOGGER = logging.getLogger(__name__) HANDLERS = Registry() @@ -99,6 +80,12 @@ async def async_api_turn_on(hass, config, directive, context): service = SERVICE_TURN_ON if domain == cover.DOMAIN: service = cover.SERVICE_OPEN_COVER + elif domain == media_player.DOMAIN: + supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + power_features = (media_player.SUPPORT_TURN_ON | + media_player.SUPPORT_TURN_OFF) + if not supported & power_features: + service = media_player.SERVICE_MEDIA_PLAY await hass.services.async_call(domain, service, { ATTR_ENTITY_ID: entity.entity_id @@ -118,6 +105,12 @@ async def async_api_turn_off(hass, config, directive, context): service = SERVICE_TURN_OFF if entity.domain == cover.DOMAIN: service = cover.SERVICE_CLOSE_COVER + elif domain == media_player.DOMAIN: + supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + power_features = (media_player.SUPPORT_TURN_ON | + media_player.SUPPORT_TURN_OFF) + if not supported & power_features: + service = media_player.SERVICE_MEDIA_STOP await hass.services.async_call(domain, service, { ATTR_ENTITY_ID: entity.entity_id diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 5f751a10039..62dbbfdc693 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -558,12 +558,23 @@ async def test_media_player_power(hass): assert_endpoint_capabilities( appliance, 'Alexa.InputController', + 'Alexa.PowerController', 'Alexa.Speaker', 'Alexa.StepSpeaker', 'Alexa.PlaybackController', 'Alexa.EndpointHealth', ) + await assert_request_calls_service( + 'Alexa.PowerController', 'TurnOn', 'media_player#test', + 'media_player.media_play', + hass) + + await assert_request_calls_service( + 'Alexa.PowerController', 'TurnOff', 'media_player#test', + 'media_player.media_stop', + hass) + async def test_alert(hass): """Test alert discovery.""" From afade4e997f4b1d3a7baad4b17c381cc556a9a92 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Fri, 12 Jul 2019 07:08:57 +0200 Subject: [PATCH 10/74] Support podcast episodes as Sonos favorites (#25087) --- homeassistant/components/sonos/manifest.json | 2 +- homeassistant/components/sonos/media_player.py | 12 +++++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 854e4ef5706..68e363d3635 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/components/sonos", "requirements": [ - "pysonos==0.0.19" + "pysonos==0.0.20" ], "dependencies": [], "ssdp": { diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 66888c59e00..6e69181e72f 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -371,9 +371,15 @@ class SonosEntity(MediaPlayerDevice): def _set_favorites(self): """Set available favorites.""" - favorites = self.soco.music_library.get_sonos_favorites() - # Exclude favorites that are non-playable due to no linked resources - self._favorites = [f for f in favorites if f.reference.resources] + self._favorites = [] + for fav in self.soco.music_library.get_sonos_favorites(): + try: + # Exclude non-playable favorites with no linked resources + if fav.reference.resources: + self._favorites.append(fav) + except SoCoException as ex: + # Skip unknown types + _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) def _radio_artwork(self, url): """Return the private URL with artwork for a radio stream.""" diff --git a/requirements_all.txt b/requirements_all.txt index c9a4e829ec6..6dd705d431f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1378,7 +1378,7 @@ pysmarty==0.8 pysnmp==4.4.9 # homeassistant.components.sonos -pysonos==0.0.19 +pysonos==0.0.20 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3458e091b0..6898490fea4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -298,7 +298,7 @@ pysmartapp==0.3.2 pysmartthings==0.6.9 # homeassistant.components.sonos -pysonos==0.0.19 +pysonos==0.0.20 # homeassistant.components.spc pyspcwebgw==0.4.0 From 155c75c54a45a2588bcdcb582886c2b5cce524fd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 11 Jul 2019 00:38:58 -0700 Subject: [PATCH 11/74] Guard module being None (#25077) --- homeassistant/helpers/config_validation.py | 8 +++++++- tests/helpers/test_config_validation.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index bd5d85230c5..40b06447a2f 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -549,7 +549,13 @@ def deprecated(key: str, - Once the invalidation_version is crossed, raises vol.Invalid if key is detected """ - module_name = inspect.getmodule(inspect.stack()[1][0]).__name__ + module = inspect.getmodule(inspect.stack()[1][0]) + if module is not None: + module_name = module.__name__ + else: + # Unclear when it is None, but it happens, so let's guard. + # https://github.com/home-assistant/home-assistant/issues/24982 + module_name = __name__ if replacement_key and invalidation_version: warning = ("The '{key}' option (with value '{value}') is" diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 6124699d88e..a4513dbab19 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -833,6 +833,16 @@ def test_deprecated_with_replacement_key_invalidation_version_default( "invalid in version 0.1.0") == str(exc_info.value) +def test_deprecated_cant_find_module(): + """Test if the current module cannot be inspected.""" + with patch('inspect.getmodule', return_value=None): + # This used to raise. + cv.deprecated( + 'mars', replacement_key='jupiter', invalidation_version='1.0.0', + default=False + ) + + def test_key_dependency(): """Test key_dependency validator.""" schema = vol.Schema(cv.key_dependency('beer', 'soda')) From 7fc8ff982bbaf1e593f9cf7d99f01e1f4b4fab37 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 12 Jul 2019 07:24:30 +0000 Subject: [PATCH 12/74] Version bump to 0.96.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 090a9f26d2f..33f22cfbf3e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 96 -PATCH_VERSION = '0b0' +PATCH_VERSION = '0b1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 9181660497cfa019adc8c51819adafc20610e65e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Jul 2019 14:58:50 -0700 Subject: [PATCH 13/74] Updated frontend to 20190712.0 --- 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 56a8da57249..4bfe8627909 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/components/frontend", "requirements": [ - "home-assistant-frontend==20190710.0" + "home-assistant-frontend==20190712.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 8bb94522c24..eee82ae87b2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 hass-nabucasa==0.15 -home-assistant-frontend==20190710.0 +home-assistant-frontend==20190712.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6dd705d431f..3e919ed0eee 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,7 +610,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190710.0 +home-assistant-frontend==20190712.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6898490fea4..f8b5f8c9411 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -165,7 +165,7 @@ hdate==0.8.8 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190710.0 +home-assistant-frontend==20190712.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From 1d784bdc05e92572eedc4774b7cfce192e6ffde7 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 12 Jul 2019 20:29:45 +0100 Subject: [PATCH 14/74] [climate] Add water_heater to evohome (#25035) * initial commit * refactor for sync * minor tweak * refactor convert code * fix regression * remove bad await * de-lint * de-lint 2 * address edge case - invalid tokens * address edge case - delint * handle no schedule * improve support for RoundThermostat * tweak logging * delint * refactor for greatness * use time_zone: for state attributes * small tweak * small tweak 2 * have datetime state attributes as UTC * have datetime state attributes as UTC - delint * have datetime state attributes as UTC - tweak * missed this - remove * de-lint type hint * use parse_datetime instead of datetime.strptime) * remove debug code * state atrribute datetimes are UTC now * revert * de-lint (again) * tweak type hints * de-lint (again, again) * tweak type hints * Convert datetime closer to sending it out --- homeassistant/components/evohome/__init__.py | 95 +++++++++++-------- homeassistant/components/evohome/climate.py | 49 +++++----- .../components/evohome/water_heater.py | 92 ++++++++++++++++++ 3 files changed, 175 insertions(+), 61 deletions(-) create mode 100644 homeassistant/components/evohome/water_heater.py diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 8b1b934fa00..1445154d267 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -2,11 +2,11 @@ Such systems include evohome (multi-zone), and Round Thermostat (single zone). """ +import asyncio from datetime import datetime, timedelta import logging -from typing import Any, Dict, Tuple +from typing import Any, Dict, Optional, Tuple -from dateutil.tz import tzlocal import requests.exceptions import voluptuous as vol import evohomeclient2 @@ -21,10 +21,10 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import ( - async_track_point_in_utc_time, async_track_time_interval) -from homeassistant.util.dt import as_utc, parse_datetime, utcnow + async_track_point_in_utc_time, track_time_interval) +from homeassistant.util.dt import parse_datetime, utcnow -from .const import DOMAIN, EVO_STRFTIME, STORAGE_VERSION, STORAGE_KEY, GWS, TCS +from .const import DOMAIN, STORAGE_VERSION, STORAGE_KEY, GWS, TCS _LOGGER = logging.getLogger(__name__) @@ -47,11 +47,20 @@ CONFIG_SCHEMA = vol.Schema({ def _local_dt_to_utc(dt_naive: datetime) -> datetime: - dt_aware = as_utc(dt_naive.replace(microsecond=0, tzinfo=tzlocal())) - return dt_aware.replace(tzinfo=None) + dt_aware = utcnow() + (dt_naive - datetime.now()) + if dt_aware.microsecond >= 500000: + dt_aware += timedelta(seconds=1) + return dt_aware.replace(microsecond=0) -def _handle_exception(err): +def _utc_to_local_dt(dt_aware: datetime) -> datetime: + dt_naive = datetime.now() + (dt_aware - utcnow()) + if dt_naive.microsecond >= 500000: + dt_naive += timedelta(seconds=1) + return dt_naive.replace(microsecond=0) + + +def _handle_exception(err) -> bool: try: raise err @@ -92,18 +101,17 @@ def _handle_exception(err): raise # we don't expect/handle any other HTTPErrors -async def async_setup(hass, hass_config): +def setup(hass, hass_config) -> bool: """Create a (EMEA/EU-based) Honeywell evohome system.""" broker = EvoBroker(hass, hass_config[DOMAIN]) - if not await broker.init_client(): + if not broker.init_client(): return False load_platform(hass, 'climate', DOMAIN, {}, hass_config) if broker.tcs.hotwater: - _LOGGER.warning("DHW controller detected, however this integration " - "does not currently support DHW controllers.") + load_platform(hass, 'water_heater', DOMAIN, {}, hass_config) - async_track_time_interval( + track_time_interval( hass, broker.update, hass_config[DOMAIN][CONF_SCAN_INTERVAL] ) @@ -126,23 +134,26 @@ class EvoBroker: hass.data[DOMAIN] = {} hass.data[DOMAIN]['broker'] = self - async def init_client(self) -> bool: + 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() + asyncio.run_coroutine_threadsafe( + self._load_auth_tokens(), self.hass.loop).result() + + # evohomeclient2 uses local datetimes + if access_token_expires is not None: + access_token_expires = _utc_to_local_dt(access_token_expires) try: - client = self.client = await self.hass.async_add_executor_job( - evohomeclient2.EvohomeClient, + client = self.client = evohomeclient2.EvohomeClient( self.params[CONF_USERNAME], self.params[CONF_PASSWORD], - False, - refresh_token, - access_token, - access_token_expires + refresh_token=refresh_token, + access_token=access_token, + access_token_expires=access_token_expires ) except (requests.exceptions.RequestException, @@ -150,13 +161,11 @@ class EvoBroker: if not _handle_exception(err): return False - else: - if access_token != self.client.access_token: - await self._save_auth_tokens() - 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] @@ -170,15 +179,19 @@ class EvoBroker: ) return False - else: - self.tcs = \ - client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa: E501; pylint: disable=protected-access + self.tcs = client.locations[loc_idx]._gateways[0]._control_systems[0] # noqa: E501; pylint: disable=protected-access _LOGGER.debug("Config = %s", self.config) + if _LOGGER.isEnabledFor(logging.DEBUG): + # don't do an I/O unless required + _LOGGER.debug( + "Status = %s", + client.locations[loc_idx].status()[GWS][0][TCS][0]) return True - async def _load_auth_tokens(self) -> Tuple[str, str, datetime]: + 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() @@ -187,9 +200,7 @@ class EvoBroker: 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 = as_utc(parse_datetime(at_expires_str)) - at_expires_dt = at_expires_dt.astimezone(tzlocal()) - at_expires_dt = at_expires_dt.replace(tzinfo=None) + at_expires_dt = parse_datetime(at_expires_str) else: at_expires_dt = None @@ -198,14 +209,15 @@ class EvoBroker: return (None, None, None) # account switched: so tokens wont be valid async def _save_auth_tokens(self, *args) -> None: - access_token_expires_utc = _local_dt_to_utc( + # evohomeclient2 uses local datetimes + access_token_expires = _local_dt_to_utc( 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_utc.isoformat() + access_token_expires.isoformat() store = self.hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) await store.async_save(self._app_storage) @@ -213,7 +225,7 @@ class EvoBroker: async_track_point_in_utc_time( self.hass, self._save_auth_tokens, - access_token_expires_utc + access_token_expires + self.params[CONF_SCAN_INTERVAL] ) def update(self, *args, **kwargs) -> None: @@ -262,7 +274,7 @@ class EvoDevice(Entity): if packet['signal'] == 'refresh': self.async_schedule_update_ha_state(force_refresh=True) - def get_setpoints(self) -> Dict[str, Any]: + def get_setpoints(self) -> Optional[Dict[str, Any]]: """Return the current/next scheduled switchpoints. Only Zones & DHW controllers (but not the TCS) have schedules. @@ -270,6 +282,9 @@ class EvoDevice(Entity): switchpoints = {} schedule = self._evo_device.schedule() + if not schedule['DailySchedules']: + return None + day_time = datetime.now() day_of_week = int(day_time.strftime('%w')) # 0 is Sunday @@ -300,9 +315,11 @@ class EvoDevice(Entity): '{}T{}'.format(sp_date, switchpoint['TimeOfDay']), '%Y-%m-%dT%H:%M:%S') - spt['target_temp'] = switchpoint['heatSetpoint'] - spt['from_datetime'] = \ - _local_dt_to_utc(dt_naive).strftime(EVO_STRFTIME) + spt['from'] = _local_dt_to_utc(dt_naive).isoformat() + try: + spt['temperature'] = switchpoint['heatSetpoint'] + except KeyError: + spt['state'] = switchpoint['DhwState'] return switchpoints diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index efa9c3cc8fa..c9391f16045 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -11,11 +11,11 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF, PRESET_AWAY, PRESET_ECO, PRESET_HOME, SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE) +from homeassistant.util.dt import parse_datetime from . import CONF_LOCATION_IDX, _handle_exception, EvoDevice from .const import ( - DOMAIN, EVO_STRFTIME, - EVO_RESET, EVO_AUTO, EVO_AUTOECO, EVO_AWAY, EVO_DAYOFF, EVO_CUSTOM, + DOMAIN, EVO_RESET, EVO_AUTO, EVO_AUTOECO, EVO_AWAY, EVO_DAYOFF, EVO_CUSTOM, EVO_HEATOFF, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER) _LOGGER = logging.getLogger(__name__) @@ -43,8 +43,8 @@ HA_PRESET_TO_EVO = { EVO_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_EVO.items()} -async def async_setup_platform(hass, hass_config, async_add_entities, - discovery_info=None) -> None: +def setup_platform(hass, hass_config, add_entities, + discovery_info=None) -> None: """Create the evohome Controller, and its Zones, if any.""" broker = hass.data[DOMAIN]['broker'] loc_idx = broker.params[CONF_LOCATION_IDX] @@ -60,13 +60,14 @@ async def async_setup_platform(hass, hass_config, async_add_entities, for zone_idx in broker.tcs.zones: evo_zone = broker.tcs.zones[zone_idx] _LOGGER.debug( - "Found Zone, id=%s [%s], name=%s", - evo_zone.zoneId, evo_zone.zone_type, evo_zone.name) + "Found %s, id=%s [%s], name=%s", + evo_zone.zoneType, evo_zone.zoneId, evo_zone.modelType, + evo_zone.name) zones.append(EvoZone(broker, evo_zone)) entities = [controller] + zones - async_add_entities(entities, update_before_add=True) + add_entities(entities, update_before_add=True) class EvoClimateDevice(EvoDevice, ClimateDevice): @@ -141,7 +142,7 @@ class EvoZone(EvoClimateDevice): if self._evo_device.temperatureStatus['isAvailable'] else None) @property - def target_temperature(self) -> Optional[float]: + def target_temperature(self) -> float: """Return the target temperature of the evohome Zone.""" if self._evo_tcs.systemModeStatus['mode'] == EVO_HEATOFF: return self._evo_device.setpointCapabilities['minHeatSetpoint'] @@ -172,7 +173,7 @@ class EvoZone(EvoClimateDevice): return self._evo_device.setpointCapabilities['maxHeatSetpoint'] def _set_temperature(self, temperature: float, - until: Optional[datetime] = None): + until: Optional[datetime] = None) -> None: """Set a new target temperature for the Zone. until == None means indefinitely (i.e. PermanentOverride) @@ -187,11 +188,11 @@ class EvoZone(EvoClimateDevice): """Set a new target temperature for an hour.""" until = kwargs.get('until') if until: - until = datetime.strptime(until, EVO_STRFTIME) + until = parse_datetime(until) self._set_temperature(kwargs['temperature'], until) - def _set_operation_mode(self, op_mode) -> None: + def _set_operation_mode(self, op_mode: str) -> None: """Set the Zone to one of its native EVO_* operating modes.""" if op_mode == EVO_FOLLOW: try: @@ -201,14 +202,13 @@ class EvoZone(EvoClimateDevice): _handle_exception(err) return - self._setpoints = self.get_setpoints() temperature = self._evo_device.setpointStatus['targetHeatTemperature'] + until = None # EVO_PERMOVER if op_mode == EVO_TEMPOVER: - until = self._setpoints['next']['from_datetime'] - until = datetime.strptime(until, EVO_STRFTIME) - else: # EVO_PERMOVER: - until = None + self._setpoints = self.get_setpoints() + if self._setpoints: + until = parse_datetime(self._setpoints['next']['from']) self._set_temperature(temperature, until=until) @@ -220,7 +220,7 @@ class EvoZone(EvoClimateDevice): else: # HVAC_MODE_HEAT self._set_operation_mode(EVO_FOLLOW) - def set_preset_mode(self, preset_mode: str) -> None: + def set_preset_mode(self, preset_mode: Optional[str]) -> None: """Set a new preset mode. If preset_mode is None, then revert to following the schedule. @@ -244,14 +244,19 @@ class EvoController(EvoClimateDevice): self._icon = 'mdi:thermostat' self._precision = None - self._state_attributes = [ - 'activeFaults', 'systemModeStatus'] + self._state_attributes = ['activeFaults', 'systemModeStatus'] self._supported_features = SUPPORT_PRESET_MODE self._hvac_modes = list(HA_HVAC_TO_TCS) - self._preset_modes = list(HA_PRESET_TO_TCS) self._config = dict(evo_broker.config) + + # special case of RoundThermostat + if self._config['zones'][0]['modelType'] == 'RoundModulation': + self._preset_modes = [PRESET_AWAY, PRESET_ECO] + else: + self._preset_modes = list(HA_PRESET_TO_TCS) + self._config['zones'] = '...' if 'dhw' in self._config: self._config['dhw'] = '...' @@ -307,7 +312,7 @@ class EvoController(EvoClimateDevice): for z in self._evo_device._zones] # noqa: E501; pylint: disable=protected-access return max(temps) if temps else 35 - def _set_operation_mode(self, op_mode) -> None: + def _set_operation_mode(self, op_mode: str) -> None: """Set the Controller to any of its native EVO_* operating modes.""" try: self._evo_device._set_status(op_mode) # noqa: E501; pylint: disable=protected-access @@ -319,7 +324,7 @@ class EvoController(EvoClimateDevice): """Set an operating mode for the Controller.""" self._set_operation_mode(HA_HVAC_TO_TCS.get(hvac_mode)) - def set_preset_mode(self, preset_mode: str) -> None: + def set_preset_mode(self, preset_mode: Optional[str]) -> None: """Set a new preset mode. If preset_mode is None, then revert to 'Auto' mode. diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py new file mode 100644 index 00000000000..6e851741489 --- /dev/null +++ b/homeassistant/components/evohome/water_heater.py @@ -0,0 +1,92 @@ +"""Support for WaterHeater devices of (EMEA/EU) Honeywell TCC systems.""" +import logging +from typing import List + +import requests.exceptions +import evohomeclient2 + +from homeassistant.components.water_heater import ( + SUPPORT_OPERATION_MODE, WaterHeaterDevice) +from homeassistant.const import PRECISION_WHOLE, STATE_OFF, STATE_ON +from homeassistant.util.dt import parse_datetime + +from . import _handle_exception, EvoDevice +from .const import DOMAIN, EVO_STRFTIME, EVO_FOLLOW, EVO_TEMPOVER, EVO_PERMOVER + +_LOGGER = logging.getLogger(__name__) + +HA_STATE_TO_EVO = {STATE_ON: 'On', STATE_OFF: 'Off'} +EVO_STATE_TO_HA = {v: k for k, v in HA_STATE_TO_EVO.items()} + +HA_OPMODE_TO_DHW = {STATE_ON: EVO_FOLLOW, STATE_OFF: EVO_PERMOVER} + + +def setup_platform(hass, hass_config, add_entities, + discovery_info=None) -> None: + """Create the DHW controller.""" + broker = hass.data[DOMAIN]['broker'] + + _LOGGER.debug( + "Found DHW device, id: %s [%s]", + broker.tcs.hotwater.zoneId, broker.tcs.hotwater.zone_type) + + evo_dhw = EvoDHW(broker, broker.tcs.hotwater) + + add_entities([evo_dhw], update_before_add=True) + + +class EvoDHW(EvoDevice, WaterHeaterDevice): + """Base for a Honeywell evohome DHW controller (aka boiler).""" + + def __init__(self, evo_broker, evo_device) -> None: + """Initialize the evohome DHW controller.""" + super().__init__(evo_broker, evo_device) + + self._id = evo_device.dhwId + self._name = 'DHW controller' + self._icon = 'mdi:thermometer-lines' + + self._precision = PRECISION_WHOLE + self._state_attributes = [ + 'activeFaults', 'stateStatus', 'temperatureStatus', 'setpoints'] + + self._supported_features = SUPPORT_OPERATION_MODE + self._operation_list = list(HA_OPMODE_TO_DHW) + + self._config = evo_broker.config['dhw'] + + @property + def current_operation(self) -> str: + """Return the current operating mode (On, or Off).""" + return EVO_STATE_TO_HA[self._evo_device.stateStatus['state']] + + @property + def operation_list(self) -> List[str]: + """Return the list of available operations.""" + return self._operation_list + + @property + def current_temperature(self) -> float: + """Return the current temperature.""" + return self._evo_device.temperatureStatus['temperature'] + + def set_operation_mode(self, operation_mode: str) -> None: + """Set new operation mode for a DHW controller.""" + op_mode = HA_OPMODE_TO_DHW[operation_mode] + + state = '' if op_mode == EVO_FOLLOW else HA_STATE_TO_EVO[STATE_OFF] + until = None # EVO_FOLLOW, EVO_PERMOVER + + if op_mode == EVO_TEMPOVER: + self._setpoints = self.get_setpoints() + if self._setpoints: + until = parse_datetime(self._setpoints['next']['from']) + until = until.strftime(EVO_STRFTIME) + + data = {'Mode': op_mode, 'State': state, 'UntilTime': until} + + try: + self._evo_device._set_dhw(data) # pylint: disable=protected-access + except (requests.exceptions.RequestException, + evohomeclient2.AuthenticationError) as err: + _handle_exception(err) From 4e69b5b45ff9a0354c885988715fc48dc0d77b36 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 12 Jul 2019 17:43:18 +0200 Subject: [PATCH 15/74] Fix Netatmo climate issue when device out of reach (#25096) * Fix valve/thermostat out of reach * Fix boost for valves * Set netatmo default max temp to 30 * Remove unnecessary get * Remove unnecessary default value * Readd get --- homeassistant/components/netatmo/climate.py | 108 ++++++++++++-------- 1 file changed, 67 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 852b5f58ac2..03a898ba87e 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -42,6 +42,7 @@ STATE_NETATMO_MANUAL = 'manual' PRESET_MAP_NETATMO = { PRESET_FROST_GUARD: STATE_NETATMO_HG, PRESET_BOOST: STATE_NETATMO_MAX, + STATE_NETATMO_MAX: STATE_NETATMO_MAX, PRESET_SCHEDULE: STATE_NETATMO_SCHEDULE, PRESET_AWAY: STATE_NETATMO_AWAY, STATE_NETATMO_OFF: STATE_NETATMO_OFF @@ -75,6 +76,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_HOMES): vol.All(cv.ensure_list, [HOME_CONFIG_SCHEMA]) }) +DEFAULT_MAX_TEMP = 30 + NA_THERM = 'NATherm1' NA_VALVE = 'NRV' @@ -141,7 +144,7 @@ class NetatmoThermostat(ClimateDevice): self._hvac_mode = None self.update_without_throttle = False self._module_type = \ - self._data.room_status[room_id].get('module_type', NA_VALVE) + self._data.room_status.get(room_id, {}).get('module_type') if self._module_type == NA_THERM: self._operation_list.append(HVAC_MODE_OFF) @@ -192,8 +195,10 @@ class NetatmoThermostat(ClimateDevice): if self._module_type == NA_THERM: return CURRENT_HVAC_MAP_NETATMO[self._data.boilerstatus] # Maybe it is a valve - if self._data.room_status[self._room_id]['heating_power_request'] > 0: - return CURRENT_HVAC_HEAT + if self._room_id in self._data.room_status: + if (self._data.room_status[self._room_id] + .get('heating_power_request', 0) > 0): + return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE def set_hvac_mode(self, hvac_mode: str) -> None: @@ -219,7 +224,20 @@ class NetatmoThermostat(ClimateDevice): DEFAULT_MIN_TEMP ) - if preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX, STATE_NETATMO_OFF]: + if ( + preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] + and self._module_type == NA_VALVE + ): + self._data.homestatus.setroomThermpoint( + self._data.home_id, + self._room_id, + STATE_NETATMO_MANUAL, + DEFAULT_MAX_TEMP + ) + elif ( + preset_mode + in [PRESET_BOOST, STATE_NETATMO_MAX, STATE_NETATMO_OFF] + ): self._data.homestatus.setroomThermpoint( self._data.home_id, self._room_id, @@ -269,18 +287,21 @@ class NetatmoThermostat(ClimateDevice): "got exception.") return try: + if self._module_type is None: + self._module_type = \ + self._data.room_status[self._room_id]['module_type'] self._current_temperature = \ self._data.room_status[self._room_id]['current_temperature'] self._target_temperature = \ self._data.room_status[self._room_id]['target_temperature'] self._preset = \ self._data.room_status[self._room_id]["setpoint_mode"] + self._hvac_mode = HVAC_MAP_NETATMO[self._preset] except KeyError: _LOGGER.error( "The thermostat in room %s seems to be out of reach.", self._room_id ) - self._hvac_mode = HVAC_MAP_NETATMO[self._preset] self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] @@ -303,8 +324,10 @@ class HomeData: if self.homedata is None: return [] for home in self.homedata.homes: - if 'therm_schedules' in self.homedata.homes[home] and 'modules' \ - in self.homedata.homes[home]: + if ( + 'therm_schedules' in self.homedata.homes[home] + and 'modules' in self.homedata.homes[home] + ): self.home_names.append(self.homedata.homes[home]['name']) return self.home_names @@ -381,44 +404,47 @@ class ThermostatData: roomstatus = {} homestatus_room = self.homestatus.rooms[room] homedata_room = self.homedata.rooms[self.home][room] + roomstatus["roomID"] = homestatus_room["id"] - roomstatus["roomname"] = homedata_room["name"] - roomstatus["target_temperature"] = homestatus_room[ - "therm_setpoint_temperature" - ] - roomstatus["setpoint_mode"] = homestatus_room[ - "therm_setpoint_mode" - ] - roomstatus["current_temperature"] = homestatus_room[ - "therm_measured_temperature" - ] - roomstatus["module_type"] = self.homestatus.thermostatType( - self.home, room - ) - roomstatus["module_id"] = None - roomstatus["heating_status"] = None - roomstatus["heating_power_request"] = None - for module_id in homedata_room["module_ids"]: - if (self.homedata.modules[self.home][module_id]["type"] - == NA_THERM - or roomstatus["module_id"] is None): - roomstatus["module_id"] = module_id - if roomstatus["module_type"] == NA_THERM: - self.boilerstatus = self.homestatus.boilerStatus( - rid=roomstatus["module_id"] - ) - roomstatus["heating_status"] = self.boilerstatus - elif roomstatus["module_type"] == NA_VALVE: - roomstatus["heating_power_request"] = homestatus_room[ - "heating_power_request" + if homestatus_room["reachable"]: + roomstatus["roomname"] = homedata_room["name"] + roomstatus["target_temperature"] = homestatus_room[ + "therm_setpoint_temperature" ] - roomstatus["heating_status"] = ( - roomstatus["heating_power_request"] > 0 + roomstatus["setpoint_mode"] = homestatus_room[ + "therm_setpoint_mode" + ] + roomstatus["current_temperature"] = homestatus_room[ + "therm_measured_temperature" + ] + roomstatus["module_type"] = self.homestatus.thermostatType( + self.home, room ) - if self.boilerstatus is not None: - roomstatus["heating_status"] = ( - self.boilerstatus and roomstatus["heating_status"] + roomstatus["module_id"] = None + roomstatus["heating_status"] = None + roomstatus["heating_power_request"] = None + for module_id in homedata_room["module_ids"]: + if (self.homedata.modules[self.home][module_id]["type"] + == NA_THERM + or roomstatus["module_id"] is None): + roomstatus["module_id"] = module_id + if roomstatus["module_type"] == NA_THERM: + self.boilerstatus = self.homestatus.boilerStatus( + rid=roomstatus["module_id"] ) + roomstatus["heating_status"] = self.boilerstatus + elif roomstatus["module_type"] == NA_VALVE: + roomstatus["heating_power_request"] = homestatus_room[ + "heating_power_request" + ] + roomstatus["heating_status"] = ( + roomstatus["heating_power_request"] > 0 + ) + if self.boilerstatus is not None: + roomstatus["heating_status"] = ( + self.boilerstatus + and roomstatus["heating_status"] + ) self.room_status[room] = roomstatus except KeyError as err: _LOGGER.error("Update of room %s failed. Error: %s", room, err) From 60c2e5e2e23d1b3e1f3076e190f1f7de2a605461 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 12 Jul 2019 18:40:29 +0300 Subject: [PATCH 16/74] Add turn on/off to coolmaster (#25097) --- homeassistant/components/coolmaster/climate.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/coolmaster/climate.py b/homeassistant/components/coolmaster/climate.py index c5430472cb7..378a1c0c281 100644 --- a/homeassistant/components/coolmaster/climate.py +++ b/homeassistant/components/coolmaster/climate.py @@ -169,7 +169,17 @@ class CoolmasterClimate(ClimateDevice): hvac_mode) if hvac_mode == HVAC_MODE_OFF: - self._device.turn_off() + self.turn_off() else: self._device.set_mode(HA_STATE_TO_CM[hvac_mode]) - self._device.turn_on() + self.turn_on() + + def turn_on(self): + """Turn on.""" + _LOGGER.debug("Turning %s on", self.unique_id) + self._device.turn_on() + + def turn_off(self): + """Turn off.""" + _LOGGER.debug("Turning %s off", self.unique_id) + self._device.turn_off() From 5eb7268ae70f76faaffa3d58e869fbe9a2937df0 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 12 Jul 2019 09:41:47 -0600 Subject: [PATCH 17/74] Fix window exception in WWLLN (#25100) * Beta fix: handle window exception in WWLLN * Fixed test * Fix bug * Member comments * Removed unused import --- homeassistant/components/wwlln/config_flow.py | 13 ++++++++----- tests/components/wwlln/test_config_flow.py | 10 +++++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/wwlln/config_flow.py b/homeassistant/components/wwlln/config_flow.py index 35b6ce6c8a0..81992794d2a 100644 --- a/homeassistant/components/wwlln/config_flow.py +++ b/homeassistant/components/wwlln/config_flow.py @@ -60,11 +60,14 @@ class WWLLNFlowHandler(config_entries.ConfigFlow): else: user_input[CONF_UNIT_SYSTEM] = CONF_UNIT_SYSTEM_METRIC - # To simplify things, we don't allow users of the config flow to - # input a window; instead, we make a sane assumption to use the - # default (stored as seconds, since timedelta's aren't - # JSON-serializable): - if CONF_WINDOW not in user_input: + # When importing from `configuration.yaml`, we give the user + # flexibility by allowing the `window` parameter to be any type + # of time period. This will always return a timedelta; unfortunately, + # timedeltas aren't JSON-serializable, so we can't store them in a + # config entry as-is; instead, we save the total seconds as an int: + if CONF_WINDOW in user_input: + user_input[CONF_WINDOW] = user_input[CONF_WINDOW].total_seconds() + else: user_input[CONF_WINDOW] = DEFAULT_WINDOW.total_seconds() return self.async_create_entry(title=identifier, data=user_input) diff --git a/tests/components/wwlln/test_config_flow.py b/tests/components/wwlln/test_config_flow.py index 9751f5d5c9c..349dc19dce4 100644 --- a/tests/components/wwlln/test_config_flow.py +++ b/tests/components/wwlln/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the WWLLN config flow.""" +from datetime import timedelta + from homeassistant import data_entry_flow from homeassistant.components.wwlln import CONF_WINDOW, DOMAIN, config_flow from homeassistant.const import ( @@ -36,12 +38,14 @@ async def test_show_form(hass): async def test_step_import(hass): """Test that the import step works.""" + # `configuration.yaml` will always return a timedelta for the `window` + # parameter, FYI: conf = { CONF_LATITUDE: 39.128712, CONF_LONGITUDE: -104.9812612, CONF_RADIUS: 25, CONF_UNIT_SYSTEM: 'metric', - CONF_WINDOW: 600.0, + CONF_WINDOW: timedelta(minutes=10) } flow = config_flow.WWLLNFlowHandler() @@ -88,7 +92,7 @@ async def test_custom_window(hass): CONF_LATITUDE: 39.128712, CONF_LONGITUDE: -104.9812612, CONF_RADIUS: 25, - CONF_WINDOW: 300 + CONF_WINDOW: timedelta(hours=1) } flow = config_flow.WWLLNFlowHandler() @@ -102,5 +106,5 @@ async def test_custom_window(hass): CONF_LONGITUDE: -104.9812612, CONF_RADIUS: 25, CONF_UNIT_SYSTEM: 'metric', - CONF_WINDOW: 300, + CONF_WINDOW: 3600, } From d0af73efe13c8292af99dfd4053b31f85c61e545 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 12 Jul 2019 09:59:04 -0600 Subject: [PATCH 18/74] Fix missing sensor unit in RainMachine (#25101) --- homeassistant/components/rainmachine/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 97694b1431a..de3afb5797c 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -73,7 +73,7 @@ SENSORS = { TYPE_FLOW_SENSOR_CONSUMED_LITERS: ( 'Flow Sensor Consumed Liters', 'mdi:water-pump', 'liter', None), TYPE_FLOW_SENSOR_START_INDEX: ( - 'Flow Sensor Start Index', 'mdi:water-pump', None), + 'Flow Sensor Start Index', 'mdi:water-pump', 'index', None), TYPE_FLOW_SENSOR_WATERING_CLICKS: ( 'Flow Sensor Clicks', 'mdi:water-pump', 'clicks', None), TYPE_FREEZE_TEMP: ( From c884f9edbcbed84002dc93339a41f8b965ea5bec Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 12 Jul 2019 15:09:02 -0700 Subject: [PATCH 19/74] Bumped version to 0.96.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 33f22cfbf3e..81ad2f2fe50 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 96 -PATCH_VERSION = '0b1' +PATCH_VERSION = '0b2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From d444ba397b9f88afa820e155199c31d8e14a160c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 15 Jul 2019 15:22:41 +0200 Subject: [PATCH 20/74] Update azure-pipelines-wheels.yml for Azure Pipelines --- azure-pipelines-wheels.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index c49c7ee0358..89e45fc31da 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -11,19 +11,18 @@ trigger: pr: none variables: - name: versionWheels - value: '0.7' + value: '1.0-3.7-alpine3.10' - group: wheels jobs: - job: 'Wheels' - condition: or(eq(variables['Build.SourceBranchName'], 'dev'), eq(variables['Build.SourceBranchName'], 'master')) timeoutInMinutes: 360 pool: vmImage: 'ubuntu-latest' strategy: - maxParallel: 3 + maxParallel: 5 matrix: amd64: buildArch: 'amd64' From 9548345ed09b4e898f51dda09307826d57f09109 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 15 Jul 2019 15:23:08 +0200 Subject: [PATCH 21/74] Update azure-pipelines-wheels.yml for Azure Pipelines --- azure-pipelines-wheels.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index c49c7ee0358..89e45fc31da 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -11,19 +11,18 @@ trigger: pr: none variables: - name: versionWheels - value: '0.7' + value: '1.0-3.7-alpine3.10' - group: wheels jobs: - job: 'Wheels' - condition: or(eq(variables['Build.SourceBranchName'], 'dev'), eq(variables['Build.SourceBranchName'], 'master')) timeoutInMinutes: 360 pool: vmImage: 'ubuntu-latest' strategy: - maxParallel: 3 + maxParallel: 5 matrix: amd64: buildArch: 'amd64' From 4fc302b67a4fb3628a85657e7a5e05ff4403ddfa Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 15 Jul 2019 22:38:48 +0200 Subject: [PATCH 22/74] Update azure-pipelines-release.yml for Azure Pipelines --- azure-pipelines-release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 79e7381fedb..b75d5b6bee8 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -8,7 +8,7 @@ trigger: pr: none variables: - name: versionBuilder - value: '5.1' + value: '5.2' - group: docker - group: github - group: twine @@ -88,10 +88,10 @@ stages: buildMachine: 'qemuarm,raspberrypi' armv7: buildArch: 'armv7' - buildMachine: 'raspberrypi2,raspberrypi3,odroid-xu,tinker' + buildMachine: 'raspberrypi2,raspberrypi3,raspberrypi4,odroid-xu,tinker' aarch64: buildArch: 'aarch64' - buildMachine: 'qemuarm-64,raspberrypi3-64,odroid-c2,orangepi-prime' + buildMachine: 'qemuarm-64,raspberrypi3-64,raspberrypi4-64,odroid-c2,orangepi-prime' steps: - script: sudo docker login -u $(dockerUser) -p $(dockerPassword) displayName: 'Docker hub login' From 82d9488ec8b38b964922c84399cd0a5cc5de5ae6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 15 Jul 2019 13:54:30 -0700 Subject: [PATCH 23/74] Updated frontend to 20190715.0 --- 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 4bfe8627909..5bb9e2e40fb 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/components/frontend", "requirements": [ - "home-assistant-frontend==20190712.0" + "home-assistant-frontend==20190715.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index eee82ae87b2..9aead28ff94 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 hass-nabucasa==0.15 -home-assistant-frontend==20190712.0 +home-assistant-frontend==20190715.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3e919ed0eee..e868cace4c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,7 +610,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190712.0 +home-assistant-frontend==20190715.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8b5f8c9411..7d7ebc5076a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -165,7 +165,7 @@ hdate==0.8.8 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190712.0 +home-assistant-frontend==20190715.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From e85d434f4ed64f382679a7bb27cc3b26e7f40d96 Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Thu, 11 Jul 2019 15:14:06 +0200 Subject: [PATCH 24/74] Add climate related services to Homematic IP Cloud (#25079) * add hmip climate services * Rename accesspoint_id to hapid to comply with config * Revert "Rename accesspoint_id to hapid" This reverts commit 4a3cd14e1482fb508273c728ad8020945b02e426. --- .../components/homematicip_cloud/__init__.py | 139 ++++++++++++++++++ .../homematicip_cloud/services.yaml | 49 ++++++ 2 files changed, 188 insertions(+) create mode 100644 homeassistant/components/homematicip_cloud/services.yaml diff --git a/homeassistant/components/homematicip_cloud/__init__.py b/homeassistant/components/homematicip_cloud/__init__.py index 550ba43950b..f73ce5a9d21 100644 --- a/homeassistant/components/homematicip_cloud/__init__.py +++ b/homeassistant/components/homematicip_cloud/__init__.py @@ -20,6 +20,17 @@ from .hap import HomematicipAuth, HomematicipHAP # noqa: F401 _LOGGER = logging.getLogger(__name__) +ATTR_DURATION = 'duration' +ATTR_ENDTIME = 'endtime' +ATTR_TEMPERATURE = 'temperature' +ATTR_ACCESSPOINT_ID = 'accesspoint_id' + +SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION = 'activate_eco_mode_with_duration' +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' + CONFIG_SCHEMA = vol.Schema({ vol.Optional(DOMAIN, default=[]): vol.All(cv.ensure_list, [vol.Schema({ vol.Optional(CONF_NAME, default=''): vol.Any(cv.string), @@ -28,6 +39,36 @@ CONFIG_SCHEMA = vol.Schema({ })]), }, extra=vol.ALLOW_EXTRA) +SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION = vol.Schema({ + vol.Required(ATTR_DURATION): cv.positive_int, + vol.Optional(ATTR_ACCESSPOINT_ID): + vol.All(str, vol.Length(min=24, max=24)), +}) + +SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD = vol.Schema({ + vol.Required(ATTR_ENDTIME): cv.datetime, + vol.Optional(ATTR_ACCESSPOINT_ID): + vol.All(str, vol.Length(min=24, max=24)), +}) + +SCHEMA_ACTIVATE_VACATION = vol.Schema({ + vol.Required(ATTR_ENDTIME): cv.datetime, + vol.Required(ATTR_TEMPERATURE, default=18.0): + vol.All(vol.Coerce(float), vol.Range(min=0, max=55)), + vol.Optional(ATTR_ACCESSPOINT_ID): + vol.All(str, vol.Length(min=24, max=24)), +}) + +SCHEMA_DEACTIVATE_ECO_MODE = vol.Schema({ + vol.Optional(ATTR_ACCESSPOINT_ID): + vol.All(str, vol.Length(min=24, max=24)), +}) + +SCHEMA_DEACTIVATE_VACATION = vol.Schema({ + vol.Optional(ATTR_ACCESSPOINT_ID): + vol.All(str, vol.Length(min=24, max=24)), +}) + async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the HomematicIP Cloud component.""" @@ -46,6 +87,104 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: } )) + async def _async_activate_eco_mode_with_duration(service): + """Service to activate eco mode with duration.""" + duration = service.data[ATTR_DURATION] + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hapid) + 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) + + hass.services.async_register( + DOMAIN, SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION, + _async_activate_eco_mode_with_duration, + schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_DURATION) + + async def _async_activate_eco_mode_with_period(service): + """Service to activate eco mode with period.""" + endtime = service.data[ATTR_ENDTIME] + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hapid) + 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) + + hass.services.async_register( + DOMAIN, SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD, + _async_activate_eco_mode_with_period, + schema=SCHEMA_ACTIVATE_ECO_MODE_WITH_PERIOD) + + async def _async_activate_vacation(service): + """Service to activate vacation.""" + endtime = service.data[ATTR_ENDTIME] + temperature = service.data[ATTR_TEMPERATURE] + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hapid) + 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) + + hass.services.async_register( + DOMAIN, SERVICE_ACTIVATE_VACATION, _async_activate_vacation, + schema=SCHEMA_ACTIVATE_VACATION) + + async def _async_deactivate_eco_mode(service): + """Service to deactivate eco mode.""" + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hapid) + if home: + await home.deactivate_absence() + else: + for hapid in hass.data[DOMAIN]: + home = hass.data[DOMAIN][hapid].home + await home.deactivate_absence() + + hass.services.async_register( + DOMAIN, SERVICE_DEACTIVATE_ECO_MODE, _async_deactivate_eco_mode, + schema=SCHEMA_DEACTIVATE_ECO_MODE) + + async def _async_deactivate_vacation(service): + """Service to deactivate vacation.""" + hapid = service.data.get(ATTR_ACCESSPOINT_ID) + + if hapid: + home = _get_home(hapid) + if home: + await home.deactivate_vacation() + else: + for hapid in hass.data[DOMAIN]: + home = hass.data[DOMAIN][hapid].home + await home.deactivate_vacation() + + hass.services.async_register( + DOMAIN, SERVICE_DEACTIVATE_VACATION, _async_deactivate_vacation, + schema=SCHEMA_DEACTIVATE_VACATION) + + def _get_home(hapid: str): + """Return a HmIP home.""" + hap = hass.data[DOMAIN][hapid] + if hap: + return hap.home + return None + return True diff --git a/homeassistant/components/homematicip_cloud/services.yaml b/homeassistant/components/homematicip_cloud/services.yaml new file mode 100644 index 00000000000..cf93b3065ee --- /dev/null +++ b/homeassistant/components/homematicip_cloud/services.yaml @@ -0,0 +1,49 @@ +# Describes the format for available component services + +activate_eco_mode_with_duration: + description: Activate eco mode with period. + fields: + duration: + description: The duration of eco mode in minutes. + example: 60 + accesspoint_id: + description: The ID of the Homematic IP Access Point + example: 3014xxxxxxxxxxxxxxxxxxxx + +activate_eco_mode_with_period: + description: Activate eco mode with period. + fields: + endtime: + 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 + example: 3014xxxxxxxxxxxxxxxxxxxx + +activate_vacation: + description: Activates the vacation mode until the given time. + fields: + endtime: + description: The time when the vacation mode should automatically be disabled. + example: 2019-09-17 14:00 + temperature: + description: the set temperature during the vacation mode. + example: 18.5 + accesspoint_id: + description: The ID of the Homematic IP Access Point + example: 3014xxxxxxxxxxxxxxxxxxxx + +deactivate_eco_mode: + description: Deactivates the eco mode immediately. + fields: + accesspoint_id: + description: The ID of the Homematic IP Access Point + example: 3014xxxxxxxxxxxxxxxxxxxx + +deactivate_vacation: + description: Deactivates the vacation mode immediately. + fields: + accesspoint_id: + description: The ID of the Homematic IP Access Point + example: 3014xxxxxxxxxxxxxxxxxxxx + From 8996e330b8a84d268343e521cf9772ba124b2bdb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 13 Jul 2019 01:27:50 -0700 Subject: [PATCH 25/74] Simplify Alexa/Google for new climate turn_on/off (#25115) --- homeassistant/components/alexa/entities.py | 4 +- .../components/google_assistant/trait.py | 50 ++++++++----------- tests/components/alexa/test_smart_home.py | 1 + .../components/google_assistant/test_trait.py | 12 ++--- 4 files changed, 30 insertions(+), 37 deletions(-) diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 2a6498fdcaf..c7f4fd9b7ea 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -249,8 +249,8 @@ class ClimateCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" # If we support two modes, one being off, we allow turning on too. - if len([v for v in self.entity.attributes[climate.ATTR_HVAC_MODES] - if v != climate.HVAC_MODE_OFF]) == 1: + if (climate.HVAC_MODE_OFF in + self.entity.attributes[climate.ATTR_HVAC_MODES]): yield AlexaPowerController(self.entity) yield AlexaThermostatController(self.hass, self.entity) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 1d36f6f53b4..2d7b7edd6ba 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -580,16 +580,6 @@ class TemperatureSettingTrait(_Trait): return modes - @property - def climate_on_mode(self): - """Return the mode that should be considered on.""" - modes = [m for m in self.climate_google_modes if m != 'off'] - - if len(modes) == 1: - return modes[0] - - return None - def sync_attributes(self): """Return temperature point and modes attributes for a sync request.""" response = {} @@ -605,8 +595,8 @@ class TemperatureSettingTrait(_Trait): elif domain == climate.DOMAIN: modes = self.climate_google_modes - on_mode = self.climate_on_mode - if on_mode is not None: + if 'off' in modes and any(mode in modes for mode + in ('heatcool', 'heat', 'cool')): modes.append('on') response['availableThermostatModes'] = ','.join(modes) @@ -761,6 +751,26 @@ class TemperatureSettingTrait(_Trait): target_mode = params['thermostatMode'] supported = self.state.attributes.get(ATTR_SUPPORTED_FEATURES) + if target_mode == 'on': + await self.hass.services.async_call( + climate.DOMAIN, SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: self.state.entity_id + }, + blocking=True, context=data.context + ) + return + + if target_mode == 'off': + await self.hass.services.async_call( + climate.DOMAIN, SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: self.state.entity_id + }, + blocking=True, context=data.context + ) + return + if target_mode in self.google_to_preset: await self.hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_PRESET_MODE, @@ -773,22 +783,6 @@ class TemperatureSettingTrait(_Trait): ) return - if target_mode == 'on': - # When targetting 'on', we're going to try best effort. - modes = [m for m in self.climate_google_modes - if m != climate.HVAC_MODE_OFF] - - if len(modes) == 1: - target_mode = modes[0] - elif 'auto' in modes: - target_mode = 'auto' - elif 'heatcool' in modes: - target_mode = 'heatcool' - else: - raise SmartHomeError( - ERR_FUNCTION_NOT_SUPPORTED, - "Unable to translate 'on' to a HVAC mode.") - await self.hass.services.async_call( climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE, { ATTR_ENTITY_ID: self.state.entity_id, diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 62dbbfdc693..59a5a5e858e 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -855,6 +855,7 @@ async def test_thermostat(hass): assert_endpoint_capabilities( appliance, + 'Alexa.PowerController', 'Alexa.ThermostatController', 'Alexa.TemperatureSensor', 'Alexa.EndpointHealth', diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 1cbece2b057..5fa71632da9 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -627,24 +627,22 @@ async def test_temperature_setting_climate_onoff(hass): climate.ATTR_MAX_TEMP: None, }), BASIC_CONFIG) assert trt.sync_attributes() == { - 'availableThermostatModes': 'off,cool,heat,heatcool', + 'availableThermostatModes': 'off,cool,heat,heatcool,on', 'thermostatTemperatureUnit': 'F', } assert trt.can_execute(trait.COMMAND_THERMOSTAT_SET_MODE, {}) - calls = async_mock_service( - hass, climate.DOMAIN, climate.SERVICE_SET_HVAC_MODE) + calls = async_mock_service(hass, climate.DOMAIN, SERVICE_TURN_ON) await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, { 'thermostatMode': 'on', }, {}) assert len(calls) == 1 - assert calls[0].data[climate.ATTR_HVAC_MODE] == climate.HVAC_MODE_HEAT_COOL + calls = async_mock_service(hass, climate.DOMAIN, SERVICE_TURN_OFF) await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, { 'thermostatMode': 'off', }, {}) - assert len(calls) == 2 - assert calls[1].data[climate.ATTR_HVAC_MODE] == climate.HVAC_MODE_OFF + assert len(calls) == 1 async def test_temperature_setting_climate_range(hass): @@ -671,7 +669,7 @@ async def test_temperature_setting_climate_range(hass): climate.ATTR_MAX_TEMP: 80 }), BASIC_CONFIG) assert trt.sync_attributes() == { - 'availableThermostatModes': 'off,cool,heat,auto', + 'availableThermostatModes': 'off,cool,heat,auto,on', 'thermostatTemperatureUnit': 'F', } assert trt.query_attributes() == { From 65593e36b1860188c6896e7dab22c1c1116bb11d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 13 Jul 2019 00:33:31 -0700 Subject: [PATCH 26/74] Verify cloud user exists during boot (#25119) --- homeassistant/components/cloud/__init__.py | 8 +++++- tests/components/cloud/test_init.py | 30 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index bb539a270ac..3e17dd70841 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -157,7 +157,13 @@ async def async_setup(hass, config): await prefs.async_initialize() # Cloud user - if not prefs.cloud_user: + user = None + if prefs.cloud_user: + # Fetch the user. It can happen that the user no longer exists if + # an image was restored without restoring the cloud prefs. + user = await hass.auth.async_get_user(prefs.cloud_user) + + if user is None: user = await hass.auth.async_create_system_user( 'Home Assistant Cloud', [GROUP_ID_ADMIN]) await prefs.async_update(cloud_user=user.id) diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index c938a404964..c8f6a852181 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -127,6 +127,36 @@ async def test_setup_existing_cloud_user(hass, hass_storage): assert hass_storage[STORAGE_KEY]['data']['cloud_user'] == user.id +async def test_setup_invalid_cloud_user(hass, hass_storage): + """Test setup with API push default data.""" + hass_storage[STORAGE_KEY] = { + 'version': 1, + 'data': { + 'cloud_user': 'non-existing' + } + } + with patch('hass_nabucasa.Cloud.start', return_value=mock_coro()): + result = await async_setup_component(hass, 'cloud', { + 'http': {}, + 'cloud': { + cloud.CONF_MODE: cloud.MODE_DEV, + 'cognito_client_id': 'test-cognito_client_id', + 'user_pool_id': 'test-user_pool_id', + 'region': 'test-region', + 'relayer': 'test-relayer', + } + }) + assert result + + assert hass_storage[STORAGE_KEY]['data']['cloud_user'] != 'non-existing' + cloud_user = await hass.auth.async_get_user( + hass_storage[STORAGE_KEY]['data']['cloud_user'] + ) + + assert cloud_user + assert cloud_user.groups[0].id == GROUP_ID_ADMIN + + async def test_setup_setup_cloud_user(hass, hass_storage): """Test setup with API push default data.""" hass_storage[STORAGE_KEY] = { From 7dedf173ad1cade6f8e64fa7629cd1c0ee90d11e Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 15 Jul 2019 11:31:53 -0700 Subject: [PATCH 27/74] Allow area ID in service call schemas (#25121) * Allow area ID in service call schemas * Remove ATTR_ENTITY_ID from service light turn off schcema --- homeassistant/components/light/__init__.py | 8 +++----- homeassistant/components/switch/__init__.py | 16 +++++----------- homeassistant/helpers/config_validation.py | 8 +++++++- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index d5fc087888e..680ccb76f17 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -16,7 +16,7 @@ from homeassistant.const import ( from homeassistant.exceptions import UnknownUser, Unauthorized import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import ( # noqa - PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ENTITY_SERVICE_SCHEMA) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers import intent @@ -84,8 +84,7 @@ VALID_TRANSITION = vol.All(vol.Coerce(float), vol.Clamp(min=0, max=6553)) VALID_BRIGHTNESS = vol.All(vol.Coerce(int), vol.Clamp(min=0, max=255)) VALID_BRIGHTNESS_PCT = vol.All(vol.Coerce(float), vol.Range(min=0, max=100)) -LIGHT_TURN_ON_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.comp_entity_ids, +LIGHT_TURN_ON_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({ vol.Exclusive(ATTR_PROFILE, COLOR_GROUP): cv.string, ATTR_TRANSITION: VALID_TRANSITION, ATTR_BRIGHTNESS: VALID_BRIGHTNESS, @@ -111,8 +110,7 @@ LIGHT_TURN_ON_SCHEMA = vol.Schema({ ATTR_EFFECT: cv.string, }) -LIGHT_TURN_OFF_SCHEMA = vol.Schema({ - ATTR_ENTITY_ID: cv.comp_entity_ids, +LIGHT_TURN_OFF_SCHEMA = ENTITY_SERVICE_SCHEMA.extend({ ATTR_TRANSITION: VALID_TRANSITION, ATTR_FLASH: vol.In([FLASH_SHORT, FLASH_LONG]), }) diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index e3f756abf53..db178c9fe7e 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -8,11 +8,9 @@ from homeassistant.loader import bind_hass from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.config_validation import ( # noqa - PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE) -import homeassistant.helpers.config_validation as cv + PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ENTITY_SERVICE_SCHEMA) from homeassistant.const import ( - STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE, - ATTR_ENTITY_ID) + STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, SERVICE_TOGGLE) from homeassistant.components import group DOMAIN = 'switch' @@ -43,10 +41,6 @@ DEVICE_CLASSES = [ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) -SWITCH_SERVICE_SCHEMA = vol.Schema({ - vol.Optional(ATTR_ENTITY_ID): cv.comp_entity_ids, -}) - _LOGGER = logging.getLogger(__name__) @@ -67,17 +61,17 @@ async def async_setup(hass, config): await component.async_setup(config) component.async_register_entity_service( - SERVICE_TURN_OFF, SWITCH_SERVICE_SCHEMA, + SERVICE_TURN_OFF, ENTITY_SERVICE_SCHEMA, 'async_turn_off' ) component.async_register_entity_service( - SERVICE_TURN_ON, SWITCH_SERVICE_SCHEMA, + SERVICE_TURN_ON, ENTITY_SERVICE_SCHEMA, 'async_turn_on' ) component.async_register_entity_service( - SERVICE_TOGGLE, SWITCH_SERVICE_SCHEMA, + SERVICE_TOGGLE, ENTITY_SERVICE_SCHEMA, 'async_toggle' ) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 40b06447a2f..60457a9963c 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -20,7 +20,8 @@ from homeassistant.const import ( CONF_ENTITY_NAMESPACE, CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC, CONF_VALUE_TEMPLATE, CONF_TIMEOUT, ENTITY_MATCH_ALL, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, - TEMP_CELSIUS, TEMP_FAHRENHEIT, WEEKDAYS, __version__) + TEMP_CELSIUS, TEMP_FAHRENHEIT, WEEKDAYS, __version__, ATTR_AREA_ID, + ATTR_ENTITY_ID) from homeassistant.core import valid_entity_id, split_entity_id from homeassistant.exceptions import TemplateError from homeassistant.helpers.logging import KeywordStyleAdapter @@ -642,6 +643,11 @@ PLATFORM_SCHEMA = vol.Schema({ PLATFORM_SCHEMA_BASE = PLATFORM_SCHEMA.extend({ }, extra=vol.ALLOW_EXTRA) +ENTITY_SERVICE_SCHEMA = vol.Schema({ + vol.Optional(ATTR_ENTITY_ID): comp_entity_ids, + vol.Optional(ATTR_AREA_ID): vol.All(ensure_list, [str]), +}) + EVENT_SCHEMA = vol.Schema({ vol.Optional(CONF_ALIAS): string, vol.Required('event'): string, From c8d7e1346c3dc0ad2ac1581d13bcf1dc76a62e77 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 14 Jul 2019 23:13:37 +0200 Subject: [PATCH 28/74] Load requirements for platforms (#25133) Fixes #25124 and fixes #25126 --- homeassistant/config.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index 056c99aed81..ab7632b6605 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -705,8 +705,17 @@ async def async_process_component_config( try: p_integration = await async_get_integration(hass, p_name) + except IntegrationNotFound: + continue + + if (not hass.config.skip_pip and p_integration.requirements and + not await async_process_requirements( + hass, p_integration.domain, p_integration.requirements)): + continue + + try: platform = p_integration.get_platform(domain) - except (IntegrationNotFound, ImportError): + except ImportError: continue # Validate platform specific schema From 2643bbc2289e3be3f08c42e8d6b846a83f3c75d3 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 14 Jul 2019 23:36:05 +0200 Subject: [PATCH 29/74] Handle Sonos connection errors during setup (#25135) --- .../components/sonos/media_player.py | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 6e69181e72f..6b6e35be453 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -342,12 +342,21 @@ class SonosEntity(MediaPlayerDevice): self._seen_timer = self.hass.helpers.event.async_call_later( 2.5*DISCOVERY_INTERVAL, self.async_unseen) - if not was_available: - await self.hass.async_add_executor_job(self._attach_player) - self.async_schedule_update_ha_state() + if was_available: + return + + self._poll_timer = self.hass.helpers.event.async_track_time_interval( + self.update, datetime.timedelta(seconds=SCAN_INTERVAL)) + + done = await self.hass.async_add_executor_job(self._attach_player) + if not done: + self._seen_timer() + self.async_unseen() + + self.async_schedule_update_ha_state() @callback - def async_unseen(self, now): + def async_unseen(self, now=None): """Make this player unavailable when it was not seen recently.""" self._seen_timer = None @@ -396,29 +405,31 @@ class SonosEntity(MediaPlayerDevice): def _attach_player(self): """Get basic information and add event subscriptions.""" - self._shuffle = self.soco.shuffle - self.update_volume() - self._set_favorites() + try: + self._shuffle = self.soco.shuffle + self.update_volume() + self._set_favorites() - self._poll_timer = self.hass.helpers.event.track_time_interval( - self.update, datetime.timedelta(seconds=SCAN_INTERVAL)) + # New player available, build the current group topology + for entity in self.hass.data[DATA_SONOS].entities: + entity.update_groups() - # New player available, build the current group topology - for entity in self.hass.data[DATA_SONOS].entities: - entity.update_groups() + player = self.soco - player = self.soco + def subscribe(service, action): + """Add a subscription to a pysonos service.""" + queue = _ProcessSonosEventQueue(action) + sub = service.subscribe(auto_renew=True, event_queue=queue) + self._subscriptions.append(sub) - def subscribe(service, action): - """Add a subscription to a pysonos service.""" - queue = _ProcessSonosEventQueue(action) - sub = service.subscribe(auto_renew=True, event_queue=queue) - self._subscriptions.append(sub) - - subscribe(player.avTransport, self.update_media) - subscribe(player.renderingControl, self.update_volume) - subscribe(player.zoneGroupTopology, self.update_groups) - subscribe(player.contentDirectory, self.update_content) + subscribe(player.avTransport, self.update_media) + subscribe(player.renderingControl, self.update_volume) + subscribe(player.zoneGroupTopology, self.update_groups) + subscribe(player.contentDirectory, self.update_content) + return True + except SoCoException as ex: + _LOGGER.warning("Could not connect %s: %s", self.entity_id, ex) + return False @property def should_poll(self): @@ -656,6 +667,11 @@ class SonosEntity(MediaPlayerDevice): async def _async_handle_group_event(event): """Get async lock and handle event.""" + if event and self._poll_timer: + # Cancel poll timer since we do receive events + self._poll_timer() + self._poll_timer = None + async with self.hass.data[DATA_SONOS].topology_condition: group = await _async_extract_group(event) @@ -664,14 +680,8 @@ class SonosEntity(MediaPlayerDevice): self.hass.data[DATA_SONOS].topology_condition.notify_all() - if event: - # Cancel poll timer since we do receive events - if self._poll_timer: - self._poll_timer() - self._poll_timer = None - - if not hasattr(event, 'zone_player_uui_ds_in_group'): - return + if event and not hasattr(event, 'zone_player_uui_ds_in_group'): + return self.hass.add_job(_async_handle_group_event(event)) From 02e8ee137f827f147c712aae1d12009cf2276b53 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Sun, 14 Jul 2019 17:40:06 +0100 Subject: [PATCH 30/74] [climate-1.0] Bugfix evohome showstopper (#25139) * initial commit * small tweak --- homeassistant/components/evohome/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index 1445154d267..d7892be6949 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -129,7 +129,7 @@ class EvoBroker: self.config = self.status = self.timers = {} self.client = self.tcs = None - self._app_storage = None + self._app_storage = {} hass.data[DOMAIN] = {} hass.data[DOMAIN]['broker'] = self @@ -195,6 +195,9 @@ class EvoBroker: 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) From 97ca0d81e728b4890d3a2df3752a628e6d4ca3e4 Mon Sep 17 00:00:00 2001 From: Markus Jankowski Date: Sun, 14 Jul 2019 23:31:32 +0200 Subject: [PATCH 31/74] remove comfort mode (#25140) --- homeassistant/components/homematicip_cloud/climate.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/climate.py b/homeassistant/components/homematicip_cloud/climate.py index 26ec6e9b50e..56cab03396e 100644 --- a/homeassistant/components/homematicip_cloud/climate.py +++ b/homeassistant/components/homematicip_cloud/climate.py @@ -9,7 +9,7 @@ from homematicip.aio.home import AsyncHome from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, HVAC_MODE_HEAT, PRESET_BOOST, PRESET_COMFORT, PRESET_ECO, + HVAC_MODE_AUTO, HVAC_MODE_HEAT, PRESET_BOOST, PRESET_ECO, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS @@ -110,8 +110,6 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): """ if self._device.boostMode: return PRESET_BOOST - if self._device.controlMode == HMIP_AUTOMATIC_CM: - return PRESET_COMFORT if self._device.controlMode == HMIP_ECO_CM: return PRESET_ECO @@ -123,7 +121,7 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): Requires SUPPORT_PRESET_MODE. """ - return [PRESET_BOOST, PRESET_COMFORT] + return [PRESET_BOOST] @property def min_temp(self) -> float: @@ -155,8 +153,6 @@ class HomematicipHeatingGroup(HomematicipGenericDevice, ClimateDevice): await self._device.set_boost(False) if preset_mode == PRESET_BOOST: await self._device.set_boost() - elif preset_mode == PRESET_COMFORT: - await self._device.set_control_mode(HMIP_AUTOMATIC_CM) def _get_first_heating_thermostat(heating_group: AsyncHeatingGroup): From 78a0d72a5cb6c3efa8054e6f11d335b094da84b1 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 15 Jul 2019 04:14:24 +0100 Subject: [PATCH 32/74] [climate-1.0] Add RoundThermostat to evohome (#25141) * initial commit * improve enumeration of zone(s) * remove unused self._config * remove unused self._config 2 * remove unused self._id * clean up device_state_attributes * remove some pylint: disable=protected-access * remove LOGGER.warn( * refactor for RoundThermostat * ready for review * small tweak * small tweak 2 * fix regression, tweak * tidy up docstring * simplify code --- homeassistant/components/evohome/__init__.py | 4 +- homeassistant/components/evohome/climate.py | 258 +++++++++++------- .../components/evohome/water_heater.py | 10 +- 3 files changed, 162 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index d7892be6949..49ddbdde156 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -143,7 +143,7 @@ class EvoBroker: asyncio.run_coroutine_threadsafe( self._load_auth_tokens(), self.hass.loop).result() - # evohomeclient2 uses local datetimes + # evohomeclient2 uses naive/local datetimes if access_token_expires is not None: access_token_expires = _utc_to_local_dt(access_token_expires) @@ -212,7 +212,7 @@ class EvoBroker: return (None, None, None) # account switched: so tokens wont be valid async def _save_auth_tokens(self, *args) -> None: - # evohomeclient2 uses local datetimes + # evohomeclient2 uses naive/local datetimes access_token_expires = _local_dt_to_utc( self.client.access_token_expires) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index c9391f16045..e31a71b19f2 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -1,7 +1,7 @@ """Support for Climate devices of (EMEA/EU-based) Honeywell TCC systems.""" from datetime import datetime import logging -from typing import Optional, List +from typing import Any, Dict, Optional, List import requests.exceptions import evohomeclient2 @@ -11,6 +11,7 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF, PRESET_AWAY, PRESET_ECO, PRESET_HOME, SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE) +from homeassistant.const import PRECISION_TENTHS from homeassistant.util.dt import parse_datetime from . import CONF_LOCATION_IDX, _handle_exception, EvoDevice @@ -27,6 +28,7 @@ HA_HVAC_TO_TCS = { HVAC_MODE_OFF: EVO_HEATOFF, HVAC_MODE_HEAT: EVO_AUTO, } + HA_PRESET_TO_TCS = { PRESET_AWAY: EVO_AWAY, PRESET_CUSTOM: EVO_CUSTOM, @@ -36,11 +38,13 @@ HA_PRESET_TO_TCS = { } TCS_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_TCS.items()} -HA_PRESET_TO_EVO = { - 'temporary': EVO_TEMPOVER, - 'permanent': EVO_PERMOVER, +EVO_PRESET_TO_HA = { + EVO_FOLLOW: None, + EVO_TEMPOVER: 'temporary', + EVO_PERMOVER: 'permanent', } -EVO_PRESET_TO_HA = {v: k for k, v in HA_PRESET_TO_EVO.items()} +HA_PRESET_TO_EVO = {v: k for k, v in EVO_PRESET_TO_HA.items() + if v is not None} def setup_platform(hass, hass_config, add_entities, @@ -50,24 +54,30 @@ def setup_platform(hass, hass_config, add_entities, loc_idx = broker.params[CONF_LOCATION_IDX] _LOGGER.debug( - "Found Controller, id=%s [%s], name=%s (location_idx=%s)", + "Found Location/Controller, id=%s [%s], name=%s (location_idx=%s)", broker.tcs.systemId, broker.tcs.modelType, broker.tcs.location.name, loc_idx) + # special case of RoundThermostat (is single zone) + if broker.config['zones'][0]['modelType'] == 'RoundModulation': + zone = list(broker.tcs.zones.values())[0] + _LOGGER.debug( + "Found %s, id=%s [%s], name=%s", + zone.zoneType, zone.zoneId, zone.modelType, zone.name) + + add_entities([EvoThermostat(broker, zone)], update_before_add=True) + return + controller = EvoController(broker, broker.tcs) zones = [] - for zone_idx in broker.tcs.zones: - evo_zone = broker.tcs.zones[zone_idx] + for zone in broker.tcs.zones.values(): _LOGGER.debug( "Found %s, id=%s [%s], name=%s", - evo_zone.zoneType, evo_zone.zoneId, evo_zone.modelType, - evo_zone.name) - zones.append(EvoZone(broker, evo_zone)) + zone.zoneType, zone.zoneId, zone.modelType, zone.name) + zones.append(EvoZone(broker, zone)) - entities = [controller] + zones - - add_entities(entities, update_before_add=True) + add_entities([controller] + zones, update_before_add=True) class EvoClimateDevice(EvoDevice, ClimateDevice): @@ -77,12 +87,67 @@ class EvoClimateDevice(EvoDevice, ClimateDevice): """Initialize the evohome Climate device.""" super().__init__(evo_broker, evo_device) - self._hvac_modes = self._preset_modes = None + self._preset_modes = None + + def _set_temperature(self, temperature: float, + until: Optional[datetime] = None) -> None: + """Set a new target temperature for the Zone. + + until == None means indefinitely (i.e. PermanentOverride) + """ + try: + self._evo_device.set_temperature(temperature, until) + except (requests.exceptions.RequestException, + evohomeclient2.AuthenticationError) as err: + _handle_exception(err) + + def _set_zone_mode(self, op_mode: str) -> None: + """Set the Zone to one of its native EVO_* operating modes. + + NB: evohome Zones 'inherit' their operating mode from the Controller. + + Usually, Zones are in 'FollowSchedule' mode, where their setpoints are + a function of their schedule, and the Controller's operating_mode, e.g. + Economy mode is their scheduled setpoint less (usually) 3C. + + However, Zones can override these setpoints, either for a specified + period of time, 'TemporaryOverride', after which they will revert back + to 'FollowSchedule' mode, or indefinitely, 'PermanentOverride'. + + Some of the Controller's operating_mode are 'forced' upon the Zone, + regardless of its override state, e.g. 'HeatingOff' (Zones to min_temp) + and 'Away' (Zones to 12C). + """ + if op_mode == EVO_FOLLOW: + try: + self._evo_device.cancel_temp_override() + except (requests.exceptions.RequestException, + evohomeclient2.AuthenticationError) as err: + _handle_exception(err) + return + + temperature = self._evo_device.setpointStatus['targetHeatTemperature'] + until = None # EVO_PERMOVER + + if op_mode == EVO_TEMPOVER: + self._setpoints = self.get_setpoints() + if self._setpoints: + until = parse_datetime(self._setpoints['next']['from']) + + self._set_temperature(temperature, until=until) + + def _set_tcs_mode(self, op_mode: str) -> None: + """Set the Controller to any of its native EVO_* operating modes.""" + try: + self._evo_tcs._set_status(op_mode) # noqa: E501; pylint: disable=protected-access + except (requests.exceptions.RequestException, + evohomeclient2.AuthenticationError) as err: + _handle_exception(err) @property def hvac_modes(self) -> List[str]: """Return the list of available hvac operation modes.""" - return self._hvac_modes + return [HVAC_MODE_OFF, HVAC_MODE_HEAT] @property def preset_modes(self) -> Optional[List[str]]: @@ -97,39 +162,22 @@ class EvoZone(EvoClimateDevice): """Initialize the evohome Zone.""" super().__init__(evo_broker, evo_device) - self._id = evo_device.zoneId self._name = evo_device.name self._icon = 'mdi:radiator' self._precision = \ self._evo_device.setpointCapabilities['valueResolution'] self._state_attributes = [ - 'activeFaults', 'setpointStatus', 'temperatureStatus', 'setpoints'] + 'zoneId', 'activeFaults', 'setpointStatus', 'temperatureStatus', + 'setpoints'] self._supported_features = SUPPORT_PRESET_MODE | \ SUPPORT_TARGET_TEMPERATURE - self._hvac_modes = [HVAC_MODE_OFF, HVAC_MODE_HEAT] self._preset_modes = list(HA_PRESET_TO_EVO) - for _zone in evo_broker.config['zones']: - if _zone['zoneId'] == self._id: - self._config = _zone - break - @property def hvac_mode(self) -> str: - """Return the current operating mode of the evohome Zone. - - NB: evohome Zones 'inherit' their operating mode from the controller. - - Usually, Zones are in 'FollowSchedule' mode, where their setpoints are - a function of their schedule, and the Controller's operating_mode, e.g. - Economy mode is their scheduled setpoint less (usually) 3C. - - However, Zones can override these setpoints, either for a specified - period of time, 'TemporaryOverride', after which they will revert back - to 'FollowSchedule' mode, or indefinitely, 'PermanentOverride'. - """ + """Return the current operating mode of the evohome Zone.""" if self._evo_tcs.systemModeStatus['mode'] in [EVO_AWAY, EVO_HEATOFF]: return HVAC_MODE_AUTO is_off = self.target_temperature <= self.min_temp @@ -152,7 +200,7 @@ class EvoZone(EvoClimateDevice): def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" if self._evo_tcs.systemModeStatus['mode'] in [EVO_AWAY, EVO_HEATOFF]: - return None + return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus['mode']) return EVO_PRESET_TO_HA.get( self._evo_device.setpointStatus['setpointMode'], 'follow') @@ -172,18 +220,6 @@ class EvoZone(EvoClimateDevice): """ return self._evo_device.setpointCapabilities['maxHeatSetpoint'] - def _set_temperature(self, temperature: float, - until: Optional[datetime] = None) -> None: - """Set a new target temperature for the Zone. - - until == None means indefinitely (i.e. PermanentOverride) - """ - try: - self._evo_device.set_temperature(temperature, until) - except (requests.exceptions.RequestException, - evohomeclient2.AuthenticationError) as err: - _handle_exception(err) - def set_temperature(self, **kwargs) -> None: """Set a new target temperature for an hour.""" until = kwargs.get('until') @@ -192,40 +228,20 @@ class EvoZone(EvoClimateDevice): self._set_temperature(kwargs['temperature'], until) - def _set_operation_mode(self, op_mode: str) -> None: - """Set the Zone to one of its native EVO_* operating modes.""" - if op_mode == EVO_FOLLOW: - try: - self._evo_device.cancel_temp_override() - except (requests.exceptions.RequestException, - evohomeclient2.AuthenticationError) as err: - _handle_exception(err) - return - - temperature = self._evo_device.setpointStatus['targetHeatTemperature'] - until = None # EVO_PERMOVER - - if op_mode == EVO_TEMPOVER: - self._setpoints = self.get_setpoints() - if self._setpoints: - until = parse_datetime(self._setpoints['next']['from']) - - self._set_temperature(temperature, until=until) - def set_hvac_mode(self, hvac_mode: str) -> None: """Set an operating mode for the Zone.""" if hvac_mode == HVAC_MODE_OFF: self._set_temperature(self.min_temp, until=None) else: # HVAC_MODE_HEAT - self._set_operation_mode(EVO_FOLLOW) + self._set_zone_mode(EVO_FOLLOW) def set_preset_mode(self, preset_mode: Optional[str]) -> None: """Set a new preset mode. If preset_mode is None, then revert to following the schedule. """ - self._set_operation_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) + self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) class EvoController(EvoClimateDevice): @@ -239,27 +255,15 @@ class EvoController(EvoClimateDevice): """Initialize the evohome Controller (hub).""" super().__init__(evo_broker, evo_device) - self._id = evo_device.systemId self._name = evo_device.location.name self._icon = 'mdi:thermostat' - self._precision = None - self._state_attributes = ['activeFaults', 'systemModeStatus'] + self._precision = PRECISION_TENTHS + self._state_attributes = [ + 'systemId', 'activeFaults', 'systemModeStatus'] self._supported_features = SUPPORT_PRESET_MODE - self._hvac_modes = list(HA_HVAC_TO_TCS) - - self._config = dict(evo_broker.config) - - # special case of RoundThermostat - if self._config['zones'][0]['modelType'] == 'RoundModulation': - self._preset_modes = [PRESET_AWAY, PRESET_ECO] - else: - self._preset_modes = list(HA_PRESET_TO_TCS) - - self._config['zones'] = '...' - if 'dhw' in self._config: - self._config['dhw'] = '...' + self._preset_modes = list(HA_PRESET_TO_TCS) @property def hvac_mode(self) -> str: @@ -273,8 +277,9 @@ class EvoController(EvoClimateDevice): Controllers do not have a current temp, but one is expected by HA. """ - temps = [z.temperatureStatus['temperature'] for z in - self._evo_device._zones if z.temperatureStatus['isAvailable']] # noqa: E501; pylint: disable=protected-access + temps = [z.temperatureStatus['temperature'] + for z in self._evo_device.zones.values() + if z.temperatureStatus['isAvailable']] return round(sum(temps) / len(temps), 1) if temps else None @property @@ -284,7 +289,7 @@ class EvoController(EvoClimateDevice): Controllers do not have a target temp, but one is expected by HA. """ temps = [z.setpointStatus['targetHeatTemperature'] - for z in self._evo_device._zones] # noqa: E501; pylint: disable=protected-access + for z in self._evo_device.zones.values()] return round(sum(temps) / len(temps), 1) if temps else None @property @@ -299,7 +304,7 @@ class EvoController(EvoClimateDevice): Controllers do not have a min target temp, but one is required by HA. """ temps = [z.setpointCapabilities['minHeatSetpoint'] - for z in self._evo_device._zones] # noqa: E501; pylint: disable=protected-access + for z in self._evo_device.zones.values()] return min(temps) if temps else 5 @property @@ -309,28 +314,77 @@ class EvoController(EvoClimateDevice): Controllers do not have a max target temp, but one is required by HA. """ temps = [z.setpointCapabilities['maxHeatSetpoint'] - for z in self._evo_device._zones] # noqa: E501; pylint: disable=protected-access + for z in self._evo_device.zones.values()] return max(temps) if temps else 35 - def _set_operation_mode(self, op_mode: str) -> None: - """Set the Controller to any of its native EVO_* operating modes.""" - try: - self._evo_device._set_status(op_mode) # noqa: E501; pylint: disable=protected-access - except (requests.exceptions.RequestException, - evohomeclient2.AuthenticationError) as err: - _handle_exception(err) - def set_hvac_mode(self, hvac_mode: str) -> None: """Set an operating mode for the Controller.""" - self._set_operation_mode(HA_HVAC_TO_TCS.get(hvac_mode)) + self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) def set_preset_mode(self, preset_mode: Optional[str]) -> None: """Set a new preset mode. If preset_mode is None, then revert to 'Auto' mode. """ - self._set_operation_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) + self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode, EVO_AUTO)) def update(self) -> None: """Get the latest state data.""" pass + + +class EvoThermostat(EvoZone): + """Base for a Honeywell Round Thermostat. + + Implemented as a combined Controller/Zone. + """ + + def __init__(self, evo_broker, evo_device) -> None: + """Initialize the Round Thermostat.""" + super().__init__(evo_broker, evo_device) + + self._name = evo_broker.tcs.location.name + self._icon = 'mdi:radiator' + + self._preset_modes = [PRESET_AWAY, PRESET_ECO] + + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return the device-specific state attributes.""" + status = super().device_state_attributes['status'] + + status['systemModeStatus'] = getattr(self._evo_tcs, 'systemModeStatus') + status['activeFaults'] += getattr(self._evo_tcs, 'activeFaults') + + return {'status': status} + + @property + def hvac_mode(self) -> str: + """Return the current operating mode.""" + if self._evo_tcs.systemModeStatus['mode'] == EVO_HEATOFF: + return HVAC_MODE_OFF + + return super().hvac_mode + + @property + def preset_mode(self) -> Optional[str]: + """Return the current preset mode, e.g., home, away, temp.""" + if self._evo_tcs.systemModeStatus['mode'] == EVO_AUTOECO: + if self._evo_device.setpointStatus['setpointMode'] == EVO_FOLLOW: + return PRESET_ECO + + return super().preset_mode + + def set_hvac_mode(self, hvac_mode: str) -> None: + """Set an operating mode.""" + self._set_tcs_mode(HA_HVAC_TO_TCS.get(hvac_mode)) + + def set_preset_mode(self, preset_mode: Optional[str]) -> None: + """Set a new preset mode. + + If preset_mode is None, then revert to following the schedule. + """ + if preset_mode in list(HA_PRESET_TO_TCS): + self._set_tcs_mode(HA_PRESET_TO_TCS.get(preset_mode)) + else: + self._set_zone_mode(HA_PRESET_TO_EVO.get(preset_mode, EVO_FOLLOW)) diff --git a/homeassistant/components/evohome/water_heater.py b/homeassistant/components/evohome/water_heater.py index 6e851741489..4706269e1cf 100644 --- a/homeassistant/components/evohome/water_heater.py +++ b/homeassistant/components/evohome/water_heater.py @@ -27,8 +27,8 @@ def setup_platform(hass, hass_config, add_entities, broker = hass.data[DOMAIN]['broker'] _LOGGER.debug( - "Found DHW device, id: %s [%s]", - broker.tcs.hotwater.zoneId, broker.tcs.hotwater.zone_type) + "Found %s, id: %s", + broker.tcs.hotwater.zone_type, broker.tcs.hotwater.zoneId) evo_dhw = EvoDHW(broker, broker.tcs.hotwater) @@ -42,19 +42,17 @@ class EvoDHW(EvoDevice, WaterHeaterDevice): """Initialize the evohome DHW controller.""" super().__init__(evo_broker, evo_device) - self._id = evo_device.dhwId self._name = 'DHW controller' self._icon = 'mdi:thermometer-lines' self._precision = PRECISION_WHOLE self._state_attributes = [ - 'activeFaults', 'stateStatus', 'temperatureStatus', 'setpoints'] + 'dhwId', 'activeFaults', 'stateStatus', 'temperatureStatus', + 'setpoints'] self._supported_features = SUPPORT_OPERATION_MODE self._operation_list = list(HA_OPMODE_TO_DHW) - self._config = evo_broker.config['dhw'] - @property def current_operation(self) -> str: """Return the current operating mode (On, or Off).""" From 50b145cf05fce6718f1250406de26efeef8c9f13 Mon Sep 17 00:00:00 2001 From: Khole Date: Sun, 14 Jul 2019 22:54:07 +0100 Subject: [PATCH 33/74] [Climate] Hive Add water heater Component post the refresh of the climate component. (#25148) * climate_water_heater * updated names * Update water_heater * Update requirements * Updated reqirements * Version update * updated Versiojn * Update device list * Removed unused Attributes --- homeassistant/components/hive/__init__.py | 1 + homeassistant/components/hive/climate.py | 9 +- homeassistant/components/hive/manifest.json | 4 +- homeassistant/components/hive/water_heater.py | 112 ++++++++++++++++++ requirements_all.txt | 2 +- 5 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/hive/water_heater.py diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 3afb628bb2d..7ad1cc002f9 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -16,6 +16,7 @@ DATA_HIVE = 'data_hive' DEVICETYPES = { 'binary_sensor': 'device_list_binary_sensor', 'climate': 'device_list_climate', + 'water_heater': 'device_list_water_heater', 'light': 'device_list_light', 'switch': 'device_list_plug', 'sensor': 'device_list_sensor', diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index ef8ae85f529..811f6fe4da4 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -45,11 +45,14 @@ class HiveClimateEntity(ClimateDevice): """Initialize the Climate device.""" self.node_id = hivedevice["Hive_NodeID"] self.node_name = hivedevice["Hive_NodeName"] + self.device_type = hivedevice["HA_DeviceType"] self.thermostat_node_id = hivedevice["Thermostat_NodeID"] self.session = hivesession self.attributes = {} - self.data_updatesource = 'Heating.{}'.format(self.node_id) - self._unique_id = '{}-Heating'.format(self.node_id) + self.data_updatesource = '{}.{}'.format( + self.device_type, self.node_id) + self._unique_id = '{}-{}'.format(self.node_id, self.device_type) + self.session.entities.append(self) @property def unique_id(self): @@ -73,7 +76,7 @@ class HiveClimateEntity(ClimateDevice): def handle_update(self, updatesource): """Handle the new update request.""" - if 'Heating.{}'.format(self.node_id) not in updatesource: + if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: self.schedule_update_ha_state() @property diff --git a/homeassistant/components/hive/manifest.json b/homeassistant/components/hive/manifest.json index 76403f293ac..886d6841ebb 100644 --- a/homeassistant/components/hive/manifest.json +++ b/homeassistant/components/hive/manifest.json @@ -3,11 +3,11 @@ "name": "Hive", "documentation": "https://www.home-assistant.io/components/hive", "requirements": [ - "pyhiveapi==0.2.17" + "pyhiveapi==0.2.18.1" ], "dependencies": [], "codeowners": [ "@Rendili", "@KJonline" ] -} +} \ No newline at end of file diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py new file mode 100644 index 00000000000..f0ec2de08b3 --- /dev/null +++ b/homeassistant/components/hive/water_heater.py @@ -0,0 +1,112 @@ +"""Support for hive water heaters.""" +from homeassistant.const import TEMP_CELSIUS + +from homeassistant.components.water_heater import ( + STATE_ECO, STATE_ON, STATE_OFF, SUPPORT_OPERATION_MODE, WaterHeaterDevice) + +from . import DATA_HIVE, DOMAIN + +SUPPORT_FLAGS_HEATER = (SUPPORT_OPERATION_MODE) + +HIVE_TO_HASS_STATE = { + 'SCHEDULE': STATE_ECO, + 'ON': STATE_ON, + 'OFF': STATE_OFF, +} + +HASS_TO_HIVE_STATE = { + STATE_ECO: 'SCHEDULE', + STATE_ON: 'ON', + STATE_OFF: 'OFF', +} + +SUPPORT_WATER_HEATER = [STATE_ECO, STATE_ON, STATE_OFF] + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Wink water heater devices.""" + if discovery_info is None: + return + if discovery_info["HA_DeviceType"] != "HotWater": + return + + session = hass.data.get(DATA_HIVE) + water_heater = HiveWaterHeater(session, discovery_info) + + add_entities([water_heater]) + session.entities.append(water_heater) + + +class HiveWaterHeater(WaterHeaterDevice): + """Hive Water Heater Device.""" + + def __init__(self, hivesession, hivedevice): + """Initialize the Water Heater device.""" + self.node_id = hivedevice["Hive_NodeID"] + self.node_name = hivedevice["Hive_NodeName"] + self.device_type = hivedevice["HA_DeviceType"] + self.session = hivesession + self.data_updatesource = '{}.{}'.format( + self.device_type, self.node_id) + self._unique_id = '{}-{}'.format(self.node_id, self.device_type) + self._unit_of_measurement = TEMP_CELSIUS + self.session.entities.append(self) + + @property + def unique_id(self): + """Return unique ID of entity.""" + return self._unique_id + + @property + def device_info(self): + """Return device information.""" + return { + 'identifiers': { + (DOMAIN, self.unique_id) + }, + 'name': self.name + } + + @property + def supported_features(self): + """Return the list of supported features.""" + return SUPPORT_FLAGS_HEATER + + def handle_update(self, updatesource): + """Handle the new update request.""" + if '{}.{}'.format(self.device_type, self.node_id) not in updatesource: + self.schedule_update_ha_state() + + @property + def name(self): + """Return the name of the water heater """ + if self.node_name is None: + self.node_name = "Hot Water" + return self.node_name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def current_operation(self): + """ Return current operation. """ + return HIVE_TO_HASS_STATE[self.session.hotwater.get_mode(self.node_id)] + + @property + def operation_list(self): + """List of available operation modes.""" + return SUPPORT_WATER_HEATER + + def set_operation_mode(self, operation_mode): + """Set operation mode.""" + new_mode = HASS_TO_HIVE_STATE[operation_mode] + self.session.hotwater.set_mode(self.node_id, new_mode) + + for entity in self.session.entities: + entity.handle_update(self.data_updatesource) + + def update(self): + """Update all Node data from Hive.""" + self.session.core.update_data(self.node_id) diff --git a/requirements_all.txt b/requirements_all.txt index e868cace4c7..364a7021179 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1166,7 +1166,7 @@ pyheos==0.5.2 pyhik==0.2.3 # homeassistant.components.hive -pyhiveapi==0.2.17 +pyhiveapi==0.2.18.1 # homeassistant.components.homematic pyhomematic==0.1.59 From 842c1a2274e3294d7a9db2c228c58a3414f97467 Mon Sep 17 00:00:00 2001 From: Josh Anderson Date: Sun, 14 Jul 2019 22:38:57 +0100 Subject: [PATCH 34/74] Remove check and restore temp/mode changes (#25149) --- homeassistant/components/tado/climate.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 6720b3c87bb..1659a4bba12 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -361,9 +361,6 @@ class TadoClimate(ClimateDevice): _LOGGER.info("Obtained current and target temperature. " "Tado thermostat active") - if not self._active or self._current_operation == self._overlay_mode: - return - if self._current_operation == CONST_MODE_SMART_SCHEDULE: _LOGGER.info("Switching mytado.com to SCHEDULE (default) " "for zone %s", self.zone_name) From c8b495f22402a3c3d76f3a20a26ab916d0215f66 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Mon, 15 Jul 2019 05:21:37 +0200 Subject: [PATCH 35/74] Update pyhomematic to 0.1.60 (#25152) --- 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 ea012ceeb27..3c350e75730 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/components/homematic", "requirements": [ - "pyhomematic==0.1.59" + "pyhomematic==0.1.60" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 364a7021179..4be4c42d8e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1169,7 +1169,7 @@ pyhik==0.2.3 pyhiveapi==0.2.18.1 # homeassistant.components.homematic -pyhomematic==0.1.59 +pyhomematic==0.1.60 # homeassistant.components.homeworks pyhomeworks==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7d7ebc5076a..91d77005238 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -260,7 +260,7 @@ pydispatcher==2.0.5 pyheos==0.5.2 # homeassistant.components.homematic -pyhomematic==0.1.59 +pyhomematic==0.1.60 # homeassistant.components.iqvia pyiqvia==0.2.1 From ff79e437d2ec097ea27bff098dd2d53381038579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Mon, 15 Jul 2019 19:38:21 +0200 Subject: [PATCH 36/74] Version sensor update (#25162) * component -> integration * Bump pyhaversion to 3.0.2 * Update requirements * Formating --- homeassistant/components/version/__init__.py | 2 +- .../components/version/manifest.json | 2 +- homeassistant/components/version/sensor.py | 44 +++++++++++-------- requirements_all.txt | 2 +- 4 files changed, 29 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/version/__init__.py b/homeassistant/components/version/__init__.py index eb257007f7c..64b04fd7d71 100644 --- a/homeassistant/components/version/__init__.py +++ b/homeassistant/components/version/__init__.py @@ -1 +1 @@ -"""The version component.""" +"""The version integration.""" diff --git a/homeassistant/components/version/manifest.json b/homeassistant/components/version/manifest.json index 16d11e913f7..2a48f91a6f8 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -3,7 +3,7 @@ "name": "Version", "documentation": "https://www.home-assistant.io/components/version", "requirements": [ - "pyhaversion==2.2.1" + "pyhaversion==3.0.2" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index 6aed6da17f7..1cdf67a480e 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -45,18 +45,38 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): """Set up the Version sensor platform.""" - from pyhaversion import Version + from pyhaversion import ( + LocalVersion, DockerVersion, HassioVersion, PyPiVersion) beta = config.get(CONF_BETA) image = config.get(CONF_IMAGE) name = config.get(CONF_NAME) source = config.get(CONF_SOURCE) session = async_get_clientsession(hass) + if beta: branch = 'beta' else: branch = 'stable' - haversion = VersionData(Version(hass.loop, session, branch, image), source) + + if source == 'pypi': + haversion = VersionData( + PyPiVersion(hass.loop, session, branch)) + elif source == 'hassio': + haversion = VersionData( + HassioVersion(hass.loop, session, branch, image)) + elif source == 'docker': + haversion = VersionData( + DockerVersion(hass.loop, session, branch, image)) + else: + haversion = VersionData( + LocalVersion(hass.loop, session)) + + if not name: + if source == DEFAULT_SOURCE: + name = DEFAULT_NAME_LOCAL + else: + name = DEFAULT_NAME_LATEST async_add_entities([VersionSensor(haversion, name)], True) @@ -64,7 +84,7 @@ async def async_setup_platform( class VersionSensor(Entity): """Representation of a Home Assistant version sensor.""" - def __init__(self, haversion, name=''): + def __init__(self, haversion, name): """Initialize the Version sensor.""" self.haversion = haversion self._name = name @@ -77,11 +97,7 @@ class VersionSensor(Entity): @property def name(self): """Return the name of the sensor.""" - if self._name: - return self._name - if self.haversion.source == DEFAULT_SOURCE: - return DEFAULT_NAME_LOCAL - return DEFAULT_NAME_LATEST + return self._name @property def state(self): @@ -102,19 +118,11 @@ class VersionSensor(Entity): class VersionData: """Get the latest data and update the states.""" - def __init__(self, api, source): + def __init__(self, api): """Initialize the data object.""" self.api = api - self.source = source @Throttle(TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest version information.""" - if self.source == 'pypi': - await self.api.get_pypi_version() - elif self.source == 'hassio': - await self.api.get_hassio_version() - elif self.source == 'docker': - await self.api.get_docker_version() - else: - await self.api.get_local_version() + await self.api.get_version() diff --git a/requirements_all.txt b/requirements_all.txt index 4be4c42d8e6..1883355ac00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1157,7 +1157,7 @@ pygtfs==0.1.5 pygtt==1.1.2 # homeassistant.components.version -pyhaversion==2.2.1 +pyhaversion==3.0.2 # homeassistant.components.heos pyheos==0.5.2 From c04049d6f60ad50fe70f4c57fb89e2c3eeefc084 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 15 Jul 2019 13:39:04 -0700 Subject: [PATCH 37/74] Make dev tools titlte translatable (#25166) --- homeassistant/components/frontend/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 3e596381321..d311baf8ae1 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -272,7 +272,7 @@ async def async_setup(hass, config): async_register_built_in_panel( hass, "developer-tools", require_admin=True, - sidebar_title="Developer Tools", sidebar_icon="hass:hammer") + sidebar_title="developer_tools", sidebar_icon="hass:hammer") if DATA_EXTRA_HTML_URL not in hass.data: hass.data[DATA_EXTRA_HTML_URL] = set() From 28bd7b6a4e9bac26d6f96d55e86dc664f64ba072 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 15 Jul 2019 13:57:41 -0700 Subject: [PATCH 38/74] Bumped version to 0.96.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 81ad2f2fe50..dfd6b1a8624 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 96 -PATCH_VERSION = '0b2' +PATCH_VERSION = '0b3' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 366ad8202ac43f07614baf2dd2838dbb71b07b95 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Mon, 15 Jul 2019 22:39:52 +0200 Subject: [PATCH 39/74] Fix device types for some HomeMatic IP sensors (#25167) * Update pyhomematic to 0.1.60 * Devicetype for pyhomematic classes, fixes #24080 --- homeassistant/components/homematic/binary_sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/homematic/binary_sensor.py b/homeassistant/components/homematic/binary_sensor.py index 9d47f74df92..8a1db6a8a7b 100644 --- a/homeassistant/components/homematic/binary_sensor.py +++ b/homeassistant/components/homematic/binary_sensor.py @@ -12,6 +12,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPES_CLASS = { 'IPShutterContact': 'opening', + 'IPShutterContactSabotage': 'opening', 'MaxShutterContact': 'opening', 'Motion': 'motion', 'MotionV2': 'motion', From 0f8f9db319127e2d209ddf46d3667b2695f9c528 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Tue, 16 Jul 2019 04:22:41 +0200 Subject: [PATCH 40/74] Update pysonos to 0.0.21 (#25168) --- 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 68e363d3635..64e7f148beb 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/components/sonos", "requirements": [ - "pysonos==0.0.20" + "pysonos==0.0.21" ], "dependencies": [], "ssdp": { diff --git a/requirements_all.txt b/requirements_all.txt index 1883355ac00..5b62dca268a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1378,7 +1378,7 @@ pysmarty==0.8 pysnmp==4.4.9 # homeassistant.components.sonos -pysonos==0.0.20 +pysonos==0.0.21 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91d77005238..6a74b9d4af6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -298,7 +298,7 @@ pysmartapp==0.3.2 pysmartthings==0.6.9 # homeassistant.components.sonos -pysonos==0.0.20 +pysonos==0.0.21 # homeassistant.components.spc pyspcwebgw==0.4.0 From c7dfec702d060b271958d29858cf6efbd4ea9d88 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 16 Jul 2019 03:32:09 -0400 Subject: [PATCH 41/74] Fix climate is_aux_heat type hint. (#25170) --- homeassistant/components/climate/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index ba6f15567d9..f7ef8a71ce4 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -307,7 +307,7 @@ class ClimateDevice(Entity): raise NotImplementedError @property - def is_aux_heat(self) -> Optional[str]: + def is_aux_heat(self) -> Optional[bool]: """Return true if aux heater. Requires SUPPORT_AUX_HEAT. From e74fc9836d7e031dc528a8c19c2bd9fb8b6a6283 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 16 Jul 2019 11:32:39 +0200 Subject: [PATCH 42/74] Upgrade luftdaten to 0.6.2 (#25177) --- homeassistant/components/luftdaten/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index 59fc9946573..a29c7faa06a 100644 --- a/homeassistant/components/luftdaten/manifest.json +++ b/homeassistant/components/luftdaten/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/components/luftdaten", "requirements": [ - "luftdaten==0.6.1" + "luftdaten==0.6.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 5b62dca268a..067edea9aa0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -736,7 +736,7 @@ logi_circle==0.2.2 london-tube-status==0.2 # homeassistant.components.luftdaten -luftdaten==0.6.1 +luftdaten==0.6.2 # homeassistant.components.lupusec lupupy==0.0.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a74b9d4af6..df99bdf9861 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -190,7 +190,7 @@ libpurecool==0.5.0 libsoundtouch==0.7.2 # homeassistant.components.luftdaten -luftdaten==0.6.1 +luftdaten==0.6.2 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 From 026dbffa77e6e30c4f6f981256f6115cf008eae8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 16 Jul 2019 14:59:46 -0700 Subject: [PATCH 43/74] Bumped version to 0.96.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index dfd6b1a8624..9018eea997b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 96 -PATCH_VERSION = '0b3' +PATCH_VERSION = '0b4' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 5abe4dd1f77d2b38425d9203a7d95bd1c01a414a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Jul 2019 13:08:02 -0700 Subject: [PATCH 44/74] Updated frontend to 20190717.0 --- 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 5bb9e2e40fb..effd199d183 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/components/frontend", "requirements": [ - "home-assistant-frontend==20190715.0" + "home-assistant-frontend==20190717.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9aead28ff94..19b10327332 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 hass-nabucasa==0.15 -home-assistant-frontend==20190715.0 +home-assistant-frontend==20190717.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 067edea9aa0..24ba898fb51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,7 +610,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190715.0 +home-assistant-frontend==20190717.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df99bdf9861..c178ed80f62 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -165,7 +165,7 @@ hdate==0.8.8 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190715.0 +home-assistant-frontend==20190717.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From c03d5f1a73e3eaeda0ddd87ee77fba0190a8b98a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 14 Jul 2019 14:45:44 -0700 Subject: [PATCH 45/74] Correctly set property decorator on preset modes (#25151) --- homeassistant/components/fritzbox/climate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index 5422468641e..b4bb32e5655 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -135,6 +135,7 @@ class FritzboxThermostat(ClimateDevice): if self._target_temperature == self._eco_temperature: return PRESET_ECO + @property def preset_modes(self): """Return supported preset modes.""" return [PRESET_ECO, PRESET_COMFORT] From b5b0f56ae7a743f7b17ee0534f4b945e4f706ea9 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Tue, 16 Jul 2019 18:16:49 -0400 Subject: [PATCH 46/74] Fix device name customization on ZHA add devices page (#25180) * ensure new device exists * clean up dev reg handling * update test * fix tests --- homeassistant/components/zha/__init__.py | 4 +-- homeassistant/components/zha/core/gateway.py | 32 ++++++++++++++------ tests/components/zha/conftest.py | 8 +++-- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 5c8d9381a2e..53b56012e5c 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -90,8 +90,8 @@ async def async_setup_entry(hass, config_entry): # pylint: disable=W0611, W0612 import zhaquirks # noqa - zha_gateway = ZHAGateway(hass, config) - await zha_gateway.async_initialize(config_entry) + zha_gateway = ZHAGateway(hass, config, config_entry) + await zha_gateway.async_initialize() device_registry = await \ hass.helpers.device_registry.async_get_registry() diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 4a38bc647e6..351ad1c5a67 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -15,7 +15,7 @@ import traceback from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.core import callback from homeassistant.helpers.device_registry import ( - async_get_registry as get_dev_reg) + CONNECTION_ZIGBEE, async_get_registry as get_dev_reg) from homeassistant.helpers.dispatcher import async_dispatcher_send from ..api import async_get_device_info @@ -46,13 +46,14 @@ EntityReference = collections.namedtuple( class ZHAGateway: """Gateway that handles events that happen on the ZHA Zigbee network.""" - def __init__(self, hass, config): + def __init__(self, hass, config, config_entry): """Initialize the gateway.""" self._hass = hass self._config = config self._devices = {} self._device_registry = collections.defaultdict(list) self.zha_storage = None + self.ha_device_registry = None self.application_controller = None self.radio_description = None hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self @@ -62,14 +63,16 @@ class ZHAGateway: } self.debug_enabled = False self._log_relay_handler = LogRelayHandler(hass, self) + self._config_entry = config_entry - async def async_initialize(self, config_entry): + async def async_initialize(self): """Initialize controller and connect radio.""" self.zha_storage = await async_get_registry(self._hass) + self.ha_device_registry = await get_dev_reg(self._hass) - usb_path = config_entry.data.get(CONF_USB_PATH) + usb_path = self._config_entry.data.get(CONF_USB_PATH) baudrate = self._config.get(CONF_BAUDRATE, DEFAULT_BAUDRATE) - radio_type = config_entry.data.get(CONF_RADIO_TYPE) + radio_type = self._config_entry.data.get(CONF_RADIO_TYPE) radio_details = RADIO_TYPES[radio_type][RADIO]() radio = radio_details[RADIO] @@ -147,11 +150,10 @@ class ZHAGateway: for entity_ref in entity_refs: remove_tasks.append(entity_ref.remove_future) await asyncio.wait(remove_tasks) - ha_device_registry = await get_dev_reg(self._hass) - reg_device = ha_device_registry.async_get_device( + reg_device = self.ha_device_registry.async_get_device( {(DOMAIN, str(device.ieee))}, set()) if reg_device is not None: - ha_device_registry.async_remove_device(reg_device.id) + self.ha_device_registry.async_remove_device(reg_device.id) def device_removed(self, device): """Handle device being removed from the network.""" @@ -241,6 +243,14 @@ class ZHAGateway: if zha_device is None: zha_device = ZHADevice(self._hass, zigpy_device, self) self._devices[zigpy_device.ieee] = zha_device + self.ha_device_registry.async_get_or_create( + config_entry_id=self._config_entry.entry_id, + connections={(CONNECTION_ZIGBEE, str(zha_device.ieee))}, + identifiers={(DOMAIN, str(zha_device.ieee))}, + name=zha_device.name, + manufacturer=zha_device.manufacturer, + model=zha_device.model + ) if not is_new_join: entry = self.zha_storage.async_get_or_create(zha_device) zha_device.async_update_last_seen(entry.last_seen) @@ -322,7 +332,11 @@ class ZHAGateway: ) if is_new_join: - device_info = async_get_device_info(self._hass, zha_device) + device_info = async_get_device_info( + self._hass, + zha_device, + self.ha_device_registry + ) async_dispatcher_send( self._hass, ZHA_GW_MSG, diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index cd0f615973d..763c59cd255 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -5,6 +5,8 @@ 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.gateway import ZHAGateway from homeassistant.components.zha.core.registries import \ establish_device_mappings @@ -24,7 +26,7 @@ def config_entry_fixture(hass): @pytest.fixture(name='zha_gateway') -async def zha_gateway_fixture(hass): +async def zha_gateway_fixture(hass, config_entry): """Fixture representing a zha gateway. Create a ZHAGateway object that can be used to interact with as if we @@ -37,8 +39,10 @@ async def zha_gateway_fixture(hass): hass.data[DATA_ZHA].get(component, {}) ) zha_storage = await async_get_registry(hass) - gateway = ZHAGateway(hass, {}) + dev_reg = await get_dev_reg(hass) + gateway = ZHAGateway(hass, {}, config_entry) gateway.zha_storage = zha_storage + gateway.ha_device_registry = dev_reg return gateway From 3d5c7736701a84f18d5ef0b13cd3de436a16fc5e Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Tue, 16 Jul 2019 23:18:21 +0100 Subject: [PATCH 47/74] [climate] Tweak evohome migration (#25187) * de-lint * use _evo_tcs instead of _evo_device for TCS * add hvac_action to zones, remove target_temp from controller * fix incorrect hvac_action * de-lint --- homeassistant/components/evohome/climate.py | 66 ++++++++------------- 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index e31a71b19f2..540675d7ef4 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -9,6 +9,7 @@ import evohomeclient2 from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF, + CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, PRESET_AWAY, PRESET_ECO, PRESET_HOME, SUPPORT_TARGET_TEMPERATURE, SUPPORT_PRESET_MODE) from homeassistant.const import PRECISION_TENTHS @@ -183,6 +184,17 @@ class EvoZone(EvoClimateDevice): is_off = self.target_temperature <= self.min_temp return HVAC_MODE_OFF if is_off else HVAC_MODE_HEAT + @property + def hvac_action(self) -> Optional[str]: + """Return the current running hvac operation if supported.""" + if self._evo_tcs.systemModeStatus['mode'] == EVO_HEATOFF: + return CURRENT_HVAC_OFF + if self.target_temperature <= self.min_temp: + return CURRENT_HVAC_OFF + if self.target_temperature <= self.current_temperature: + return CURRENT_HVAC_HEAT + return CURRENT_HVAC_IDLE + @property def current_temperature(self) -> Optional[float]: """Return the current temperature of the evohome Zone.""" @@ -221,7 +233,7 @@ class EvoZone(EvoClimateDevice): return self._evo_device.setpointCapabilities['maxHeatSetpoint'] def set_temperature(self, **kwargs) -> None: - """Set a new target temperature for an hour.""" + """Set a new target temperature.""" until = kwargs.get('until') if until: until = parse_datetime(until) @@ -268,7 +280,7 @@ class EvoController(EvoClimateDevice): @property def hvac_mode(self) -> str: """Return the current operating mode of the evohome Controller.""" - tcs_mode = self._evo_device.systemModeStatus['mode'] + tcs_mode = self._evo_tcs.systemModeStatus['mode'] return HVAC_MODE_OFF if tcs_mode == EVO_HEATOFF else HVAC_MODE_HEAT @property @@ -278,44 +290,18 @@ class EvoController(EvoClimateDevice): Controllers do not have a current temp, but one is expected by HA. """ temps = [z.temperatureStatus['temperature'] - for z in self._evo_device.zones.values() + for z in self._evo_tcs.zones.values() if z.temperatureStatus['isAvailable']] return round(sum(temps) / len(temps), 1) if temps else None - @property - def target_temperature(self) -> Optional[float]: - """Return the average target temperature of the heating Zones. - - Controllers do not have a target temp, but one is expected by HA. - """ - temps = [z.setpointStatus['targetHeatTemperature'] - for z in self._evo_device.zones.values()] - return round(sum(temps) / len(temps), 1) if temps else None - @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - return TCS_PRESET_TO_HA.get(self._evo_device.systemModeStatus['mode']) + return TCS_PRESET_TO_HA.get(self._evo_tcs.systemModeStatus['mode']) - @property - def min_temp(self) -> float: - """Return the minimum target temperature of the heating Zones. - - Controllers do not have a min target temp, but one is required by HA. - """ - temps = [z.setpointCapabilities['minHeatSetpoint'] - for z in self._evo_device.zones.values()] - return min(temps) if temps else 5 - - @property - def max_temp(self) -> float: - """Return the maximum target temperature of the heating Zones. - - Controllers do not have a max target temp, but one is required by HA. - """ - temps = [z.setpointCapabilities['maxHeatSetpoint'] - for z in self._evo_device.zones.values()] - return max(temps) if temps else 35 + def set_temperature(self, **kwargs) -> None: + """The evohome Controller doesn't have a targert temperature.""" + return def set_hvac_mode(self, hvac_mode: str) -> None: """Set an operating mode for the Controller.""" @@ -330,7 +316,7 @@ class EvoController(EvoClimateDevice): def update(self) -> None: """Get the latest state data.""" - pass + return class EvoThermostat(EvoZone): @@ -344,8 +330,6 @@ class EvoThermostat(EvoZone): super().__init__(evo_broker, evo_device) self._name = evo_broker.tcs.location.name - self._icon = 'mdi:radiator' - self._preset_modes = [PRESET_AWAY, PRESET_ECO] @property @@ -353,8 +337,8 @@ class EvoThermostat(EvoZone): """Return the device-specific state attributes.""" status = super().device_state_attributes['status'] - status['systemModeStatus'] = getattr(self._evo_tcs, 'systemModeStatus') - status['activeFaults'] += getattr(self._evo_tcs, 'activeFaults') + status['systemModeStatus'] = self._evo_tcs.systemModeStatus + status['activeFaults'] += self._evo_tcs.activeFaults return {'status': status} @@ -369,9 +353,9 @@ class EvoThermostat(EvoZone): @property def preset_mode(self) -> Optional[str]: """Return the current preset mode, e.g., home, away, temp.""" - if self._evo_tcs.systemModeStatus['mode'] == EVO_AUTOECO: - if self._evo_device.setpointStatus['setpointMode'] == EVO_FOLLOW: - return PRESET_ECO + if self._evo_tcs.systemModeStatus['mode'] == EVO_AUTOECO and \ + self._evo_device.setpointStatus['setpointMode'] == EVO_FOLLOW: + return PRESET_ECO return super().preset_mode From 3cfbbdc7206828bb6618c91e22b86bf56208c703 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Jul 2019 12:09:44 -0700 Subject: [PATCH 48/74] Only include target temp if has right support flag (#25193) * Only include target temp if has right support flag * Remove comma --- homeassistant/components/climate/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/climate/__init__.py b/homeassistant/components/climate/__init__.py index f7ef8a71ce4..347cb275e42 100644 --- a/homeassistant/components/climate/__init__.py +++ b/homeassistant/components/climate/__init__.py @@ -31,7 +31,7 @@ from .const import ( SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_HUMIDITY, - SUPPORT_TARGET_TEMPERATURE_RANGE) + SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_TARGET_TEMPERATURE) from .reproduce_state import async_reproduce_states # noqa DEFAULT_MIN_TEMP = 7 @@ -176,14 +176,16 @@ class ClimateDevice(Entity): ATTR_MAX_TEMP: show_temp( self.hass, self.max_temp, self.temperature_unit, self.precision), - ATTR_TEMPERATURE: show_temp( - self.hass, self.target_temperature, self.temperature_unit, - self.precision), } if self.target_temperature_step: data[ATTR_TARGET_TEMP_STEP] = self.target_temperature_step + if supported_features & SUPPORT_TARGET_TEMPERATURE: + data[ATTR_TEMPERATURE] = show_temp( + self.hass, self.target_temperature, self.temperature_unit, + self.precision) + if supported_features & SUPPORT_TARGET_TEMPERATURE_RANGE: data[ATTR_TARGET_TEMP_HIGH] = show_temp( self.hass, self.target_temperature_high, self.temperature_unit, From 74d0e65958fdf1c35dc136aebcccdd6fc25794f7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Jul 2019 13:42:32 -0700 Subject: [PATCH 49/74] Bumped version to 0.96.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9018eea997b..f491714ec03 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 96 -PATCH_VERSION = '0b4' +PATCH_VERSION = '0' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From e4bb9554982a3b22890ea3b3c8b84b5b64255d39 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Jul 2019 14:04:13 -0700 Subject: [PATCH 50/74] Pin Docker to Debain Stretch (#25206) * Pin Docker to Debain Stretch * Update dev docker too" --- Dockerfile | 2 +- virtualization/Docker/Dockerfile.dev | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 01fdee45a63..73134e4e59c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # When updating this file, please also update virtualization/Docker/Dockerfile.dev # This way, the development image and the production image are kept in sync. -FROM python:3.7 +FROM python:3.7-stretch LABEL maintainer="Paulus Schoutsen " # Uncomment any of the following lines to disable the installation. diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 8c00fb7248a..3e8b86e2450 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -2,7 +2,7 @@ # Based on the production Dockerfile, but with development additions. # Keep this file as close as possible to the production Dockerfile, so the environments match. -FROM python:3.7 +FROM python:3.7-stretch LABEL maintainer="Paulus Schoutsen " # Uncomment any of the following lines to disable the installation. From 5d7f420821895ae578f258fc3bf4baba3d36e30c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Jul 2019 15:07:14 -0700 Subject: [PATCH 51/74] Fix ecobee missing preset mode support flag (#25211) --- homeassistant/components/ecobee/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 96ee9887bf2..058d9f43f83 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -12,7 +12,7 @@ from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH, SUPPORT_TARGET_TEMPERATURE, SUPPORT_AUX_HEAT, SUPPORT_TARGET_TEMPERATURE_RANGE, SUPPORT_FAN_MODE, PRESET_AWAY, FAN_AUTO, FAN_ON, CURRENT_HVAC_OFF, CURRENT_HVAC_HEAT, - CURRENT_HVAC_COOL + CURRENT_HVAC_COOL, SUPPORT_PRESET_MODE ) from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT, TEMP_CELSIUS) @@ -66,7 +66,7 @@ RESUME_PROGRAM_SCHEMA = vol.Schema({ vol.Optional(ATTR_RESUME_ALL, default=DEFAULT_RESUME_ALL): cv.boolean, }) -SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | +SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE | SUPPORT_AUX_HEAT | SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_FAN_MODE) From ff5dd0cf42a2a9d51dfae37bda7be3f9f12b5564 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 17 Jul 2019 15:10:52 -0700 Subject: [PATCH 52/74] Updated frontend to 20190717.1 --- 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 effd199d183..45d6e49e399 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/components/frontend", "requirements": [ - "home-assistant-frontend==20190717.0" + "home-assistant-frontend==20190717.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 19b10327332..5da936731db 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 hass-nabucasa==0.15 -home-assistant-frontend==20190717.0 +home-assistant-frontend==20190717.1 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 24ba898fb51..d5318de96f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,7 +610,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190717.0 +home-assistant-frontend==20190717.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c178ed80f62..4f16968f113 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -165,7 +165,7 @@ hdate==0.8.8 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190717.0 +home-assistant-frontend==20190717.1 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From 90231c5e079da0aa2f3bd31adc131c0680ffd81a Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 18 Jul 2019 00:18:07 +0200 Subject: [PATCH 53/74] Fix schema validation for service calls (#25204) * Fix schema validation for service calls * No need for get * No need for get --- homeassistant/components/wunderlist/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/wunderlist/__init__.py b/homeassistant/components/wunderlist/__init__.py index 5c85c746826..a4299b98b6f 100644 --- a/homeassistant/components/wunderlist/__init__.py +++ b/homeassistant/components/wunderlist/__init__.py @@ -28,7 +28,7 @@ SERVICE_CREATE_TASK = 'create_task' SERVICE_SCHEMA_CREATE_TASK = vol.Schema({ vol.Required(CONF_LIST_NAME): cv.string, vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_STARRED): cv.boolean, + vol.Optional(CONF_STARRED, default=False): cv.boolean, }) @@ -42,7 +42,10 @@ def setup(hass, config): _LOGGER.error("Invalid credentials") return False - hass.services.register(DOMAIN, 'create_task', data.create_task) + hass.services.register( + DOMAIN, 'create_task', data.create_task, + schema=SERVICE_SCHEMA_CREATE_TASK + ) return True @@ -68,9 +71,9 @@ class Wunderlist: def create_task(self, call): """Create a new task on a list of Wunderlist.""" - list_name = call.data.get(CONF_LIST_NAME) - task_title = call.data.get(CONF_NAME) - starred = call.data.get(CONF_STARRED) + list_name = call.data[CONF_LIST_NAME] + task_title = call.data[CONF_NAME] + starred = call.data[CONF_STARRED] list_id = self._list_by_name(list_name) self._client.create_task(list_id, task_title, starred=starred) return True From ccc4f628f15c712006577d9752a8b6adfb76a06f Mon Sep 17 00:00:00 2001 From: Khole Date: Wed, 17 Jul 2019 23:17:44 +0100 Subject: [PATCH 54/74] Hive water heater - Remove Duplication of appending entities (#25210) * climate_water_heater * updated names * Update water_heater * Update requirements * Updated reqirements * Version update * updated Versiojn * Update device list * Removed unused Attributes * removed duplicate appending entities * re-added missing hotwater * Move call to async_added_to_hass * Move session append to async_added_to_hass * White space --- homeassistant/components/hive/climate.py | 7 +++++-- homeassistant/components/hive/water_heater.py | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/hive/climate.py b/homeassistant/components/hive/climate.py index 811f6fe4da4..bfc43e3357f 100644 --- a/homeassistant/components/hive/climate.py +++ b/homeassistant/components/hive/climate.py @@ -35,7 +35,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): climate = HiveClimateEntity(session, discovery_info) add_entities([climate]) - session.entities.append(climate) class HiveClimateEntity(ClimateDevice): @@ -52,7 +51,6 @@ class HiveClimateEntity(ClimateDevice): self.data_updatesource = '{}.{}'.format( self.device_type, self.node_id) self._unique_id = '{}-{}'.format(self.node_id, self.device_type) - self.session.entities.append(self) @property def unique_id(self): @@ -145,6 +143,11 @@ class HiveClimateEntity(ClimateDevice): """Return a list of available preset modes.""" return SUPPORT_PRESET + async def async_added_to_hass(self): + """When entity is added to Home Assistant.""" + await super().async_added_to_hass() + self.session.entities.append(self) + def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" new_mode = HASS_TO_HIVE_STATE[hvac_mode] diff --git a/homeassistant/components/hive/water_heater.py b/homeassistant/components/hive/water_heater.py index f0ec2de08b3..943abde5dc7 100644 --- a/homeassistant/components/hive/water_heater.py +++ b/homeassistant/components/hive/water_heater.py @@ -34,7 +34,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): water_heater = HiveWaterHeater(session, discovery_info) add_entities([water_heater]) - session.entities.append(water_heater) class HiveWaterHeater(WaterHeaterDevice): @@ -50,7 +49,6 @@ class HiveWaterHeater(WaterHeaterDevice): self.device_type, self.node_id) self._unique_id = '{}-{}'.format(self.node_id, self.device_type) self._unit_of_measurement = TEMP_CELSIUS - self.session.entities.append(self) @property def unique_id(self): @@ -99,6 +97,11 @@ class HiveWaterHeater(WaterHeaterDevice): """List of available operation modes.""" return SUPPORT_WATER_HEATER + async def async_added_to_hass(self): + """When entity is added to Home Assistant.""" + await super().async_added_to_hass() + self.session.entities.append(self) + def set_operation_mode(self, operation_mode): """Set operation mode.""" new_mode = HASS_TO_HIVE_STATE[operation_mode] From d57cf01cf2dadbbd9b2347fb67c74294a2e2df37 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Jul 2019 14:07:55 -0700 Subject: [PATCH 55/74] Updated frontend to 20190718.0 --- 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 45d6e49e399..65abf3a20a5 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/components/frontend", "requirements": [ - "home-assistant-frontend==20190717.1" + "home-assistant-frontend==20190718.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5da936731db..294f52834b8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 hass-nabucasa==0.15 -home-assistant-frontend==20190717.1 +home-assistant-frontend==20190718.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index d5318de96f1..b958feaaaa9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,7 +610,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190717.1 +home-assistant-frontend==20190718.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4f16968f113..6a1581bf473 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -165,7 +165,7 @@ hdate==0.8.8 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190717.1 +home-assistant-frontend==20190718.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From 39b249d20213eaa497845a82c692a7ce634eedac Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 18 Jul 2019 11:24:07 +0200 Subject: [PATCH 56/74] Show off value (#25236) --- homeassistant/components/homematic/climate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 86bdac4f4e5..09a1452a48d 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -66,6 +66,8 @@ class HMThermostat(HMDevice, ClimateDevice): Need to be one of HVAC_MODE_*. """ + if self.current_temperature <= self._hmdevice.OFF_VALUE + 0.5: + return HVAC_MODE_OFF if "MANU_MODE" in self._hmdevice.ACTIONNODE: if self._hm_controll_mode == self._hmdevice.MANU_MODE: return HVAC_MODE_HEAT @@ -157,12 +159,12 @@ class HMThermostat(HMDevice, ClimateDevice): @property def min_temp(self): - """Return the minimum temperature - 4.5 means off.""" + """Return the minimum temperature.""" return 4.5 @property def max_temp(self): - """Return the maximum temperature - 30.5 means on.""" + """Return the maximum temperature.""" return 30.5 @property From 59cf6a0c79110c4eee253548ef416c956afd5e7e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 18 Jul 2019 11:25:11 +0200 Subject: [PATCH 57/74] Fix eq3btsmart (#25238) --- homeassistant/components/eq3btsmart/climate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/eq3btsmart/climate.py b/homeassistant/components/eq3btsmart/climate.py index a2f16843505..6a9d65b3883 100644 --- a/homeassistant/components/eq3btsmart/climate.py +++ b/homeassistant/components/eq3btsmart/climate.py @@ -66,9 +66,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for name, device_cfg in config[CONF_DEVICES].items(): mac = device_cfg[CONF_MAC] - devices.append(EQ3BTSmartThermostat(mac, name), True) + devices.append(EQ3BTSmartThermostat(mac, name)) - add_entities(devices) + add_entities(devices, True) class EQ3BTSmartThermostat(ClimateDevice): From 2b69904b949427c3e11dba06e0a8fec7b3516b86 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 18 Jul 2019 19:27:04 +0200 Subject: [PATCH 58/74] Make presets prettier (#25245) --- homeassistant/components/netatmo/climate.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 03a898ba87e..b62d7ca8762 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -23,8 +23,8 @@ from .const import DATA_NETATMO_AUTH _LOGGER = logging.getLogger(__name__) -PRESET_FROST_GUARD = 'frost guard' -PRESET_SCHEDULE = 'schedule' +PRESET_FROST_GUARD = 'Frost Guard' +PRESET_SCHEDULE = 'Schedule' SUPPORT_FLAGS = (SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE) SUPPORT_HVAC = [HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF] @@ -48,9 +48,19 @@ PRESET_MAP_NETATMO = { STATE_NETATMO_OFF: STATE_NETATMO_OFF } +NETATMO_MAP_PRESET = { + STATE_NETATMO_HG: PRESET_FROST_GUARD, + STATE_NETATMO_MAX: PRESET_BOOST, + STATE_NETATMO_SCHEDULE: PRESET_SCHEDULE, + STATE_NETATMO_AWAY: PRESET_AWAY, + STATE_NETATMO_OFF: STATE_NETATMO_OFF, + STATE_NETATMO_MANUAL: 'Manual', +} + HVAC_MAP_NETATMO = { STATE_NETATMO_SCHEDULE: HVAC_MODE_AUTO, STATE_NETATMO_HG: HVAC_MODE_AUTO, + PRESET_FROST_GUARD: HVAC_MODE_AUTO, STATE_NETATMO_MAX: HVAC_MODE_HEAT, STATE_NETATMO_OFF: HVAC_MODE_OFF, STATE_NETATMO_MANUAL: HVAC_MODE_AUTO, @@ -294,8 +304,9 @@ class NetatmoThermostat(ClimateDevice): self._data.room_status[self._room_id]['current_temperature'] self._target_temperature = \ self._data.room_status[self._room_id]['target_temperature'] - self._preset = \ + self._preset = NETATMO_MAP_PRESET[ self._data.room_status[self._room_id]["setpoint_mode"] + ] self._hvac_mode = HVAC_MAP_NETATMO[self._preset] except KeyError: _LOGGER.error( From 37810e010acb53dc95163ef796a163338ccd08ce Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 18 Jul 2019 13:27:56 +0200 Subject: [PATCH 59/74] Fix the unit of measurement for ecobee climate (#25246) * Fix the unit of measurement * Remove unused const --- homeassistant/components/ecobee/climate.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 058d9f43f83..cd5c6fc666f 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -15,7 +15,7 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_COOL, SUPPORT_PRESET_MODE ) from homeassistant.const import ( - ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT, TEMP_CELSIUS) + ATTR_ENTITY_ID, STATE_ON, ATTR_TEMPERATURE, TEMP_FAHRENHEIT) import homeassistant.helpers.config_validation as cv _CONFIGURING = {} @@ -168,9 +168,6 @@ class Thermostat(ClimateDevice): @property def temperature_unit(self): """Return the unit of measurement.""" - if self.thermostat['settings']['useCelsius']: - return TEMP_CELSIUS - return TEMP_FAHRENHEIT @property From 2016cf872efea6f40f054472f1c04e7cc73014d7 Mon Sep 17 00:00:00 2001 From: geekofweek Date: Thu, 18 Jul 2019 12:39:53 -0500 Subject: [PATCH 60/74] ecobee Preset Fix (#25256) * ecobee Preset Fix * Celsius Fix * Checks Fix * Check Fix #2 * Check Fix #3 --- homeassistant/components/ecobee/climate.py | 26 +++++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index cd5c6fc666f..3b034d4a838 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -31,6 +31,8 @@ PRESET_AUX_HEAT_ONLY = 'aux_heat_only' PRESET_HOLD_NEXT_TRANSITION = 'next_transition' PRESET_HOLD_INDEFINITE = 'indefinite' AWAY_MODE = 'awayMode' +PRESET_HOME = 'home' +PRESET_SLEEP = 'sleep' # Order matters, because for reverse mapping we don't want to map HEAT to AUX ECOBEE_HVAC_TO_HASS = collections.OrderedDict([ @@ -48,9 +50,8 @@ PRESET_TO_ECOBEE_HOLD = { PRESET_MODES = [ PRESET_AWAY, - PRESET_TEMPERATURE, - PRESET_HOLD_NEXT_TRANSITION, - PRESET_HOLD_INDEFINITE + PRESET_HOME, + PRESET_SLEEP ] SERVICE_SET_FAN_MIN_ON_TIME = 'ecobee_set_fan_min_on_time' @@ -305,9 +306,9 @@ class Thermostat(ClimateDevice): """Return true if aux heater.""" return 'auxHeat' in self.thermostat['equipmentStatus'] - def set_preset(self, preset): + def set_preset_mode(self, preset_mode): """Activate a preset.""" - if preset == self.preset_mode: + if preset_mode == self.preset_mode: return self.update_without_throttle = True @@ -317,23 +318,26 @@ class Thermostat(ClimateDevice): self.data.ecobee.delete_vacation( self.thermostat_index, self.vacation) - if preset == PRESET_AWAY: + if preset_mode == PRESET_AWAY: self.data.ecobee.set_climate_hold(self.thermostat_index, 'away', 'indefinite') - elif preset == PRESET_TEMPERATURE: + elif preset_mode == PRESET_TEMPERATURE: self.set_temp_hold(self.current_temperature) - elif preset in (PRESET_HOLD_NEXT_TRANSITION, PRESET_HOLD_INDEFINITE): + elif preset_mode in ( + PRESET_HOLD_NEXT_TRANSITION, PRESET_HOLD_INDEFINITE): self.data.ecobee.set_climate_hold( - self.thermostat_index, PRESET_TO_ECOBEE_HOLD[preset], + self.thermostat_index, PRESET_TO_ECOBEE_HOLD[preset_mode], self.hold_preference()) - elif preset is None: + elif preset_mode is None: self.data.ecobee.resume_program(self.thermostat_index) else: - _LOGGER.warning("Received invalid preset: %s", preset) + self.data.ecobee.set_climate_hold( + self.thermostat_index, preset_mode, self.hold_preference()) + self.update_without_throttle = True @property def preset_modes(self): From ec3cb11e2febe14d8e6ace2ef026f9da614b314f Mon Sep 17 00:00:00 2001 From: William Sutton Date: Thu, 18 Jul 2019 15:36:17 -0400 Subject: [PATCH 61/74] Update CT80 Humidity call (#25258) Last PR was from a few versions before, not sure how I had it working, but functioning properly now on .96 --- homeassistant/components/radiotherm/climate.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index e1feb6f4024..b926e0447c0 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -224,13 +224,12 @@ class RadioThermostat(ClimateDevice): if self._is_model_ct80: try: - humiditydata = self.device.tstat.humidity['raw'] + humiditydata = self.device.humidity['raw'] except radiotherm.validate.RadiothermTstatError: _LOGGER.warning('%s (%s) was busy (invalid value returned)', self._name, self.device.host) return - current_humidity = humiditydata['humidity'] - self._current_humidity = current_humidity + self._current_humidity = humiditydata # Map thermostat values into various STATE_ flags. self._current_temperature = current_temp From 46cdbd273a19a79dfe66e8a5c49dbec929b250e7 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Thu, 18 Jul 2019 13:14:58 -0400 Subject: [PATCH 62/74] Restore SmartThings A/C on/off services (#25259) * Restore ST A/C on/off services * Use correct OFF const * Support AC HVAC_MODE_OFF --- .../components/smartthings/climate.py | 25 +++++++- tests/components/smartthings/test_climate.py | 59 +++++++++++++++---- 2 files changed, 71 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index 4fd1e1581f4..ca98f9827c8 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -323,6 +323,9 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateDevice): async def async_set_hvac_mode(self, hvac_mode): """Set new target operation mode.""" + if hvac_mode == HVAC_MODE_OFF: + await self.async_turn_off() + return await self._device.set_air_conditioner_mode( STATE_TO_AC_MODE[hvac_mode], set_status=True) # State is set optimistically in the command above, therefore update @@ -344,18 +347,32 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateDevice): # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state() + async def async_turn_on(self): + """Turn device on.""" + await self._device.switch_on(set_status=True) + # State is set optimistically in the command above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state() + + async def async_turn_off(self): + """Turn device off.""" + await self._device.switch_off(set_status=True) + # State is set optimistically in the command above, therefore update + # the entity state ahead of receiving the confirming push updates + self.async_schedule_update_ha_state() + async def async_update(self): """Update the calculated fields of the AC.""" - operations = set() + modes = {HVAC_MODE_OFF} for mode in self._device.status.supported_ac_modes: state = AC_MODE_TO_STATE.get(mode) if state is not None: - operations.add(state) + modes.add(state) else: _LOGGER.debug('Device %s (%s) returned an invalid supported ' 'AC mode: %s', self._device.label, self._device.device_id, mode) - self._hvac_modes = operations + self._hvac_modes = modes @property def current_temperature(self): @@ -400,6 +417,8 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateDevice): @property def hvac_mode(self): """Return current operation ie. heat, cool, idle.""" + if not self._device.status.switch: + return HVAC_MODE_OFF return AC_MODE_TO_STATE.get(self._device.status.air_conditioner_mode) @property diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index c1ca8e296bf..306b496359f 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -13,15 +13,15 @@ from homeassistant.components.climate.const import ( ATTR_FAN_MODES, ATTR_HVAC_ACTIONS, ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_IDLE, DOMAIN as CLIMATE_DOMAIN, HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, SERVICE_SET_FAN_MODE, SERVICE_SET_HVAC_MODE, SERVICE_SET_TEMPERATURE, SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE) from homeassistant.components.smartthings import climate from homeassistant.components.smartthings.const import DOMAIN from homeassistant.const import ( - ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, STATE_OFF, - STATE_UNKNOWN) + ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, + SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_UNKNOWN) from .conftest import setup_platform @@ -172,7 +172,7 @@ async def test_legacy_thermostat_entity_state(hass, legacy_thermostat): assert state.attributes[ATTR_HVAC_ACTIONS] == 'idle' assert state.attributes[ATTR_HVAC_MODES] == { HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, - STATE_OFF} + HVAC_MODE_OFF} assert state.attributes[ATTR_FAN_MODE] == 'auto' assert state.attributes[ATTR_FAN_MODES] == ['auto', 'on'] assert state.attributes[ATTR_TARGET_TEMP_LOW] == 20 # celsius @@ -184,12 +184,12 @@ async def test_basic_thermostat_entity_state(hass, basic_thermostat): """Tests the state attributes properly match the thermostat type.""" await setup_platform(hass, CLIMATE_DOMAIN, devices=[basic_thermostat]) state = hass.states.get('climate.basic_thermostat') - assert state.state == STATE_OFF + assert state.state == HVAC_MODE_OFF assert state.attributes[ATTR_SUPPORTED_FEATURES] == \ SUPPORT_TARGET_TEMPERATURE_RANGE | SUPPORT_TARGET_TEMPERATURE assert ATTR_HVAC_ACTIONS not in state.attributes assert state.attributes[ATTR_HVAC_MODES] == { - STATE_OFF, HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, HVAC_MODE_COOL} + HVAC_MODE_OFF, HVAC_MODE_HEAT_COOL, HVAC_MODE_HEAT, HVAC_MODE_COOL} assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 21.1 # celsius @@ -205,7 +205,7 @@ async def test_thermostat_entity_state(hass, thermostat): assert state.attributes[ATTR_HVAC_ACTIONS] == CURRENT_HVAC_IDLE assert state.attributes[ATTR_HVAC_MODES] == { HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, - STATE_OFF} + HVAC_MODE_OFF} assert state.attributes[ATTR_FAN_MODE] == 'on' assert state.attributes[ATTR_FAN_MODES] == ['auto', 'on'] assert state.attributes[ATTR_TEMPERATURE] == 20 # celsius @@ -245,7 +245,7 @@ async def test_air_conditioner_entity_state(hass, air_conditioner): SUPPORT_TARGET_TEMPERATURE assert sorted(state.attributes[ATTR_HVAC_MODES]) == [ HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL] + HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF] assert state.attributes[ATTR_FAN_MODE] == 'medium' assert sorted(state.attributes[ATTR_FAN_MODES]) == \ ['auto', 'high', 'low', 'medium', 'turbo'] @@ -277,8 +277,8 @@ async def test_set_fan_mode(hass, thermostat, air_conditioner): assert state.attributes[ATTR_FAN_MODE] == 'auto', entity_id -async def test_set_operation_mode(hass, thermostat, air_conditioner): - """Test the operation mode is set successfully.""" +async def test_set_hvac_mode(hass, thermostat, air_conditioner): + """Test the hvac mode is set successfully.""" await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat, air_conditioner]) entity_ids = ['climate.thermostat', 'climate.air_conditioner'] @@ -293,6 +293,20 @@ async def test_set_operation_mode(hass, thermostat, air_conditioner): assert state.state == HVAC_MODE_COOL, entity_id +async def test_ac_set_hvac_mode_off(hass, air_conditioner): + """Test the AC HVAC mode can be turned off set successfully.""" + await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) + state = hass.states.get('climate.air_conditioner') + assert state.state != HVAC_MODE_OFF + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { + ATTR_ENTITY_ID: 'climate.air_conditioner', + ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True) + state = hass.states.get('climate.air_conditioner') + assert state.state == HVAC_MODE_OFF + + async def test_set_temperature_heat_mode(hass, thermostat): """Test the temperature is set successfully when in heat mode.""" thermostat.status.thermostat_mode = 'heat' @@ -378,6 +392,31 @@ async def test_set_temperature_with_mode(hass, thermostat): assert state.state == HVAC_MODE_HEAT_COOL +async def test_set_turn_off(hass, air_conditioner): + """Test the a/c is turned off successfully.""" + await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) + state = hass.states.get('climate.air_conditioner') + assert state.state == HVAC_MODE_HEAT_COOL + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_TURN_OFF, + blocking=True) + state = hass.states.get('climate.air_conditioner') + assert state.state == HVAC_MODE_OFF + + +async def test_set_turn_on(hass, air_conditioner): + """Test the a/c is turned on successfully.""" + air_conditioner.status.update_attribute_value(Attribute.switch, 'off') + await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) + state = hass.states.get('climate.air_conditioner') + assert state.state == HVAC_MODE_OFF + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_TURN_ON, + blocking=True) + state = hass.states.get('climate.air_conditioner') + assert state.state == HVAC_MODE_HEAT_COOL + + async def test_entity_and_device_attributes(hass, thermostat): """Test the attributes of the entries are correct.""" await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat]) From 86cf02739bf9021f8deb115275dee295247138ac Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Jul 2019 12:32:17 -0700 Subject: [PATCH 63/74] Add hvac modes back to opentherm (#25268) --- homeassistant/components/opentherm_gw/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/opentherm_gw/climate.py b/homeassistant/components/opentherm_gw/climate.py index 4d7ea85383b..d0b706cdad8 100644 --- a/homeassistant/components/opentherm_gw/climate.py +++ b/homeassistant/components/opentherm_gw/climate.py @@ -130,9 +130,14 @@ class OpenThermClimate(ClimateDevice): @property def hvac_mode(self): - """Return current operation ie. heat, cool, idle.""" + """Return current HVAC mode.""" return self._current_operation + @property + def hvac_modes(self): + """Return available HVAC modes.""" + return [] + @property def current_temperature(self): """Return the current temperature.""" From f76700567eef778c95784d2608194f729fcc983f Mon Sep 17 00:00:00 2001 From: stboch Date: Thu, 18 Jul 2019 16:54:05 -0400 Subject: [PATCH 64/74] Added states and modes for zwave climate (#25274) * Update climate.py Added support for Fan Only State Added additional missing modes and states this should correct issue #25216 * Correct line lint error * Corrected mode spelling * Lint --- homeassistant/components/zwave/climate.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zwave/climate.py b/homeassistant/components/zwave/climate.py index 579b1649abd..188f376e753 100644 --- a/homeassistant/components/zwave/climate.py +++ b/homeassistant/components/zwave/climate.py @@ -4,8 +4,9 @@ import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, - DOMAIN, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + CURRENT_HVAC_COOL, CURRENT_HVAC_FAN, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, DOMAIN, HVAC_MODE_COOL, HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, SUPPORT_FAN_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import callback @@ -35,6 +36,11 @@ HVAC_STATE_MAPPINGS = { 'Heat': HVAC_MODE_HEAT, 'Heat Mode': HVAC_MODE_HEAT, 'Heat (Default)': HVAC_MODE_HEAT, + 'Aux Heat': HVAC_MODE_HEAT, + 'Furnace': HVAC_MODE_HEAT, + 'Fan Only': HVAC_MODE_FAN_ONLY, + 'Dry Air': HVAC_MODE_DRY, + 'Moist Air': HVAC_MODE_DRY, 'Cool': HVAC_MODE_COOL, 'Auto': HVAC_MODE_HEAT_COOL, } @@ -43,7 +49,13 @@ HVAC_STATE_MAPPINGS = { HVAC_CURRENT_MAPPINGS = { "Idle": CURRENT_HVAC_IDLE, "Heat": CURRENT_HVAC_HEAT, + "Pending Heat": CURRENT_HVAC_HEAT, + "Heating": CURRENT_HVAC_HEAT, "Cool": CURRENT_HVAC_COOL, + "Pending Cool": CURRENT_HVAC_COOL, + "Cooling": CURRENT_HVAC_COOL, + "Fan Only": CURRENT_HVAC_FAN, + "Vent / Economiser": CURRENT_HVAC_FAN, "Off": CURRENT_HVAC_OFF, } From cc595632bd4b1137614b1ec6f08497593e635fc2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Jul 2019 14:08:50 -0700 Subject: [PATCH 65/74] Bumped version to 0.96.1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f491714ec03..31af562ce1c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 96 -PATCH_VERSION = '0' +PATCH_VERSION = '1' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From 3db106c562ed804f899ffc2df824e4a94f0cab1c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Thu, 18 Jul 2019 23:20:56 +0200 Subject: [PATCH 66/74] Update azure-pipelines-ci.yml for Azure Pipelines --- azure-pipelines-ci.yml | 53 ++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index b94b936976a..a27d7659f1f 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -4,8 +4,13 @@ trigger: batch: true branches: include: + - rc - dev -pr: none + - master +pr: + - rc + - dev + - master resources: containers: @@ -20,6 +25,7 @@ variables: value: '2df3ae11-3bf6-49bc-a809-ba0d340d6a6d' - name: PythonMain value: '35' + - group: codecov stages: @@ -80,7 +86,7 @@ stages: steps: - script: | python --version > .cache - displayName: 'Set python $(python.version) for requirement cache' + displayName: 'Set python $(python.container) for requirement cache' - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 displayName: 'Restore artifacts based on Requirements' inputs: @@ -97,38 +103,55 @@ stages: pip install pytest-azurepipelines -c homeassistant/package_constraints.txt displayName: 'Create Virtual Environment & Install Requirements' condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) - - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - displayName: 'Save artifacts based on Requirements' - inputs: - keyfile: 'requirements_test_all.txt, .cache' - targetfolder: './venv' + # Explicit Cache Save (instead of using RestoreAndSaveCache) + # Dont wait with cache save for all the other task in this job to complete (±30 minutes), other parallel jobs might utilize this + - task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 + displayName: 'Save artifacts based on Requirements' + inputs: + keyfile: 'requirements_test_all.txt, .cache' + targetfolder: './venv' vstsFeed: '$(ArtifactFeed)' - script: | . venv/bin/activate pip install -e . - displayName: 'Install Home Assistant for python $(python.version)' + displayName: 'Install Home Assistant for python $(python.container)' - script: | . venv/bin/activate - pytest --timeout=9 --durations=10 --junitxml=junit/test-results.xml -qq -o console_output_style=count -p no:sugar tests - displayName: 'Run pytest for python $(python.version)' + pytest --timeout=9 --durations=10 --junitxml=test-results.xml -qq -o console_output_style=count -p no:sugar tests + displayName: 'Run pytest for python $(python.container)' + condition: and(succeeded(), ne(variables['python.container'], variables['PythonMain'])) + - script: | + . venv/bin/activate + pytest --timeout=9 --durations=10 --junitxml=test-results.xml --cov --cov-report=xml -qq -o console_output_style=count -p no:sugar tests + codecov + displayName: 'Run pytest for python $(python.container) / coverage' + env: + CODECOV_TOKEN: '$(codecovToken)' + condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain'])) - task: PublishTestResults@2 condition: succeededOrFailed() inputs: - testResultsFiles: '**/test-*.xml' - testRunTitle: 'Publish test results for Python $(python.version)' + testResultsFiles: 'test-results.xml' + testRunTitle: 'Publish test results for Python $(python.container)' + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: cobertura + summaryFileLocation: coverage.xml + displayName: 'publish coverage artifact' + condition: and(succeeded(), eq(variables['python.container'], variables['PythonMain'])) - stage: 'FullCheck' dependsOn: - 'Overview' jobs: - - job: 'Pytlint' + - job: 'Pylint' pool: vmImage: 'ubuntu-latest' container: $[ variables['PythonMain'] ] steps: - script: | python --version > .cache - displayName: 'Set python $(python.version) for requirement cache' + displayName: 'Set python $(PythonMain) for requirement cache' - task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 displayName: 'Restore artifacts based on Requirements' inputs: @@ -154,7 +177,7 @@ stages: - script: | . venv/bin/activate pip install -e . - displayName: 'Install Home Assistant for python $(python.version)' + displayName: 'Install Home Assistant for python $(PythonMain)' - script: | . venv/bin/activate pylint homeassistant From 8b020ea5e69984850f4ea803dd53cd7de49ec67c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 19 Jul 2019 09:43:03 -0700 Subject: [PATCH 67/74] Updated frontend to 20190719.0 --- 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 65abf3a20a5..288e11bdca9 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/components/frontend", "requirements": [ - "home-assistant-frontend==20190718.0" + "home-assistant-frontend==20190719.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 294f52834b8..883937d94ea 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ certifi>=2019.6.16 cryptography==2.7 distro==1.4.0 hass-nabucasa==0.15 -home-assistant-frontend==20190718.0 +home-assistant-frontend==20190719.0 importlib-metadata==0.18 jinja2>=2.10.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index b958feaaaa9..1883fbd72f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -610,7 +610,7 @@ hole==0.3.0 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190718.0 +home-assistant-frontend==20190719.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6a1581bf473..68f6efb33ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -165,7 +165,7 @@ hdate==0.8.8 holidays==0.9.10 # homeassistant.components.frontend -home-assistant-frontend==20190718.0 +home-assistant-frontend==20190719.0 # homeassistant.components.homekit_controller homekit[IP]==0.14.0 From c4d1cd0e033f19e8116ab36285e86db8a53a6afa Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 19 Jul 2019 09:49:28 +0200 Subject: [PATCH 68/74] Fix fritzbox climate HVAC mode / temperature (#25275) * Set the target temperature * Update tests * Update tests * Fix linter complaints --- homeassistant/components/fritzbox/climate.py | 12 ++++++++---- tests/components/fritzbox/test_climate.py | 6 +++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/fritzbox/climate.py b/homeassistant/components/fritzbox/climate.py index b4bb32e5655..012d79587b4 100644 --- a/homeassistant/components/fritzbox/climate.py +++ b/homeassistant/components/fritzbox/climate.py @@ -93,9 +93,10 @@ class FritzboxThermostat(ClimateDevice): @property def target_temperature(self): """Return the temperature we try to reach.""" - if self._target_temperature in (ON_API_TEMPERATURE, - OFF_API_TEMPERATURE): - return None + if self._target_temperature == ON_API_TEMPERATURE: + return ON_REPORT_SET_TEMPERATURE + if self._target_temperature == OFF_API_TEMPERATURE: + return OFF_REPORT_SET_TEMPERATURE return self._target_temperature def set_temperature(self, **kwargs): @@ -110,7 +111,10 @@ class FritzboxThermostat(ClimateDevice): @property def hvac_mode(self): """Return the current operation mode.""" - if self._target_temperature == OFF_REPORT_SET_TEMPERATURE: + if ( + self._target_temperature == OFF_REPORT_SET_TEMPERATURE or + self._target_temperature == OFF_API_TEMPERATURE + ): return HVAC_MODE_OFF return HVAC_MODE_HEAT diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 410ba0734ac..c82ec98373e 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -66,10 +66,10 @@ class TestFritzboxClimate(unittest.TestCase): assert 19.5 == self.thermostat.target_temperature self.thermostat._target_temperature = 126.5 - assert self.thermostat.target_temperature is None + assert self.thermostat.target_temperature == 0.0 self.thermostat._target_temperature = 127.0 - assert self.thermostat.target_temperature is None + assert self.thermostat.target_temperature == 30.0 @patch.object(FritzboxThermostat, 'set_hvac_mode') def test_set_temperature_operation_mode(self, mock_set_op): @@ -103,7 +103,7 @@ class TestFritzboxClimate(unittest.TestCase): self.thermostat._target_temperature = 127.0 assert 'heat' == self.thermostat.hvac_mode self.thermostat._target_temperature = 126.5 - assert 'heat' == self.thermostat.hvac_mode + assert 'off' == self.thermostat.hvac_mode self.thermostat._target_temperature = 22.0 assert 'heat' == self.thermostat.hvac_mode self.thermostat._target_temperature = 16.0 From 68c4e5c0c9e9c0e84c4780b62a8225f6f593162d Mon Sep 17 00:00:00 2001 From: William Scanlon Date: Fri, 19 Jul 2019 02:54:09 -0400 Subject: [PATCH 69/74] Fixed python-wink method names (#25285) * Fixed python-wink method names * Fixed aux heat --- homeassistant/components/wink/climate.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/wink/climate.py b/homeassistant/components/wink/climate.py index 48c8de88746..0a27db396b5 100644 --- a/homeassistant/components/wink/climate.py +++ b/homeassistant/components/wink/climate.py @@ -137,7 +137,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): @property def preset_mode(self): """Return the current preset mode, e.g., home, away, temp.""" - mode = self.wink.current_mode() + mode = self.wink.current_hvac_mode() if mode == "eco": return PRESET_ECO if self.wink.away(): @@ -192,7 +192,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): """Return true if aux heater.""" if 'aux' not in self.wink.hvac_modes(): return None - if self.wink.hvac_action_mode() == 'aux': + if self.wink.current_hvac_mode() == 'aux': return True return False @@ -205,7 +205,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): if not self.wink.is_on(): return HVAC_MODE_OFF - wink_mode = self.wink.current_mode() + wink_mode = self.wink.current_hvac_mode() if wink_mode == "aux": return HVAC_MODE_HEAT if wink_mode == "eco": @@ -220,7 +220,7 @@ class WinkThermostat(WinkDevice, ClimateDevice): """ hvac_list = [HVAC_MODE_OFF] - modes = self.wink.modes() + modes = self.wink.hvac_modes() for mode in modes: if mode in ("eco", "aux"): continue @@ -409,7 +409,7 @@ class WinkAC(WinkDevice, ClimateDevice): if not self.wink.is_on(): return HVAC_MODE_OFF - wink_mode = self.wink.current_mode() + wink_mode = self.wink.current_hvac_mode() if wink_mode == "auto_eco": return HVAC_MODE_AUTO return WINK_HVAC_TO_HA.get(wink_mode) @@ -422,7 +422,7 @@ class WinkAC(WinkDevice, ClimateDevice): """ hvac_list = [HVAC_MODE_OFF] - modes = self.wink.modes() + modes = self.wink.hvac_modes() for mode in modes: if mode == "auto_eco": continue From 8629b86186ccaf117667859fe80f3639fd364605 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Fri, 19 Jul 2019 08:51:02 +0100 Subject: [PATCH 70/74] [climate] Correct honeywell supported_features (#25292) * Initial commit * delint --- homeassistant/components/honeywell/climate.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 78420c98dee..8d0a3602d02 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -11,9 +11,8 @@ from homeassistant.components.climate import ClimateDevice, PLATFORM_SCHEMA from homeassistant.components.climate.const import ( ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, FAN_AUTO, FAN_DIFFUSE, FAN_ON, - SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, - SUPPORT_PRESET_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, - SUPPORT_TARGET_TEMPERATURE_RANGE, + SUPPORT_AUX_HEAT, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE_RANGE, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, PRESET_AWAY, @@ -128,7 +127,6 @@ class HoneywellUSThermostat(ClimateDevice): self._password = password self._supported_features = (SUPPORT_PRESET_MODE | - SUPPORT_TARGET_TEMPERATURE | SUPPORT_TARGET_TEMPERATURE_RANGE) # pylint: disable=protected-access From 49e2583b083266d4152a2ec5afdc5646a390cf84 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Fri, 19 Jul 2019 11:18:13 +0200 Subject: [PATCH 71/74] Fix HM with use wrong datapoint for off (#25298) --- homeassistant/components/homematic/climate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index 09a1452a48d..584b2dd761a 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -66,7 +66,7 @@ class HMThermostat(HMDevice, ClimateDevice): Need to be one of HVAC_MODE_*. """ - if self.current_temperature <= self._hmdevice.OFF_VALUE + 0.5: + if self.target_temperature <= self._hmdevice.OFF_VALUE + 0.5: return HVAC_MODE_OFF if "MANU_MODE" in self._hmdevice.ACTIONNODE: if self._hm_controll_mode == self._hmdevice.MANU_MODE: From fc384ca6d55bae19b43319fe1017734cf91f90c5 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 19 Jul 2019 16:09:29 +0200 Subject: [PATCH 72/74] Fix plant error when adding new value (#25302) * Only add value if int or floar * Simplify check * Simplify check --- homeassistant/components/plant/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index c4abc916c3f..0b2ffe6d8cb 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -363,7 +363,7 @@ class DailyHistory: def add_measurement(self, value, timestamp=None): """Add a new measurement for a certain day.""" day = (timestamp or datetime.now()).date() - if value is None: + if not isinstance(value, (int, float)): return if self._days is None: self._days = deque() @@ -388,4 +388,6 @@ class DailyHistory: oldest = self._days.popleft() del self._max_dict[oldest] self._days.append(day) + if not isinstance(value, (int, float)): + return self._max_dict[day] = value From 662c33af85366753e5cd77a6863b3d2bf3ad6768 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 19 Jul 2019 09:44:53 -0700 Subject: [PATCH 73/74] Bumped version to 0.96.2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 31af562ce1c..623c07f39bf 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -2,7 +2,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 96 -PATCH_VERSION = '1' +PATCH_VERSION = '2' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) REQUIRED_PYTHON_VER = (3, 5, 3) From b4481269ec79091c4130aab8c623acabf3d70f20 Mon Sep 17 00:00:00 2001 From: Andrew Sayre <6730289+andrewsayre@users.noreply.github.com> Date: Fri, 19 Jul 2019 13:19:34 -0400 Subject: [PATCH 74/74] Turn on device before setting mode (#25314) --- .../components/smartthings/climate.py | 16 ++++-- tests/components/smartthings/test_climate.py | 50 +++++++++++++++++++ 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index ca98f9827c8..d0c426ba9fd 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -326,8 +326,13 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateDevice): if hvac_mode == HVAC_MODE_OFF: await self.async_turn_off() return - await self._device.set_air_conditioner_mode( - STATE_TO_AC_MODE[hvac_mode], set_status=True) + tasks = [] + # Turn on the device if it's off before setting mode. + if not self._device.status.switch: + tasks.append(self._device.switch_on(set_status=True)) + tasks.append(self._device.set_air_conditioner_mode( + STATE_TO_AC_MODE[hvac_mode], set_status=True)) + await asyncio.gather(*tasks) # State is set optimistically in the command above, therefore update # the entity state ahead of receiving the confirming push updates self.async_schedule_update_ha_state() @@ -338,7 +343,12 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateDevice): # operation mode operation_mode = kwargs.get(ATTR_HVAC_MODE) if operation_mode: - tasks.append(self.async_set_hvac_mode(operation_mode)) + if operation_mode == HVAC_MODE_OFF: + tasks.append(self._device.switch_off(set_status=True)) + else: + if not self._device.status.switch: + tasks.append(self._device.switch_on(set_status=True)) + tasks.append(self.async_set_hvac_mode(operation_mode)) # temperature tasks.append(self._device.set_cooling_setpoint( kwargs[ATTR_TEMPERATURE], set_status=True)) diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 306b496359f..bc16fe6add2 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -293,6 +293,23 @@ async def test_set_hvac_mode(hass, thermostat, air_conditioner): assert state.state == HVAC_MODE_COOL, entity_id +async def test_ac_set_hvac_mode_from_off(hass, air_conditioner): + """Test setting HVAC mode when the unit is off.""" + air_conditioner.status.update_attribute_value( + Attribute.air_conditioner_mode, 'heat') + air_conditioner.status.update_attribute_value(Attribute.switch, 'off') + await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) + state = hass.states.get('climate.air_conditioner') + assert state.state == HVAC_MODE_OFF + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_HVAC_MODE, { + ATTR_ENTITY_ID: 'climate.air_conditioner', + ATTR_HVAC_MODE: HVAC_MODE_HEAT_COOL}, + blocking=True) + state = hass.states.get('climate.air_conditioner') + assert state.state == HVAC_MODE_HEAT_COOL + + async def test_ac_set_hvac_mode_off(hass, air_conditioner): """Test the AC HVAC mode can be turned off set successfully.""" await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) @@ -376,6 +393,39 @@ async def test_set_temperature_ac_with_mode(hass, air_conditioner): assert state.state == HVAC_MODE_COOL +async def test_set_temperature_ac_with_mode_from_off(hass, air_conditioner): + """Test the temp and mode is set successfully when the unit is off.""" + air_conditioner.status.update_attribute_value( + Attribute.air_conditioner_mode, 'heat') + air_conditioner.status.update_attribute_value(Attribute.switch, 'off') + await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) + assert hass.states.get('climate.air_conditioner').state == HVAC_MODE_OFF + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { + ATTR_ENTITY_ID: 'climate.air_conditioner', + ATTR_TEMPERATURE: 27, + ATTR_HVAC_MODE: HVAC_MODE_COOL}, + blocking=True) + state = hass.states.get('climate.air_conditioner') + assert state.attributes[ATTR_TEMPERATURE] == 27 + assert state.state == HVAC_MODE_COOL + + +async def test_set_temperature_ac_with_mode_to_off(hass, air_conditioner): + """Test the temp and mode is set successfully to turn off the unit.""" + await setup_platform(hass, CLIMATE_DOMAIN, devices=[air_conditioner]) + assert hass.states.get('climate.air_conditioner').state != HVAC_MODE_OFF + await hass.services.async_call( + CLIMATE_DOMAIN, SERVICE_SET_TEMPERATURE, { + ATTR_ENTITY_ID: 'climate.air_conditioner', + ATTR_TEMPERATURE: 27, + ATTR_HVAC_MODE: HVAC_MODE_OFF}, + blocking=True) + state = hass.states.get('climate.air_conditioner') + assert state.attributes[ATTR_TEMPERATURE] == 27 + assert state.state == HVAC_MODE_OFF + + async def test_set_temperature_with_mode(hass, thermostat): """Test the temperature and mode is set successfully.""" await setup_platform(hass, CLIMATE_DOMAIN, devices=[thermostat])