diff --git a/build.json b/build.json index bdb59943d72..1b9c72e8675 100644 --- a/build.json +++ b/build.json @@ -2,11 +2,11 @@ "image": "homeassistant/{arch}-homeassistant", "shadow_repository": "ghcr.io/home-assistant", "build_from": { - "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.08.0", - "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.08.0", - "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.08.0", - "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.08.0", - "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.08.0" + "aarch64": "ghcr.io/home-assistant/aarch64-homeassistant-base:2021.09.0", + "armhf": "ghcr.io/home-assistant/armhf-homeassistant-base:2021.09.0", + "armv7": "ghcr.io/home-assistant/armv7-homeassistant-base:2021.09.0", + "amd64": "ghcr.io/home-assistant/amd64-homeassistant-base:2021.09.0", + "i386": "ghcr.io/home-assistant/i386-homeassistant-base:2021.09.0" }, "labels": { "io.hass.type": "core", @@ -19,4 +19,4 @@ "org.opencontainers.image.licenses": "Apache License 2.0" }, "version_tag": true -} +} \ No newline at end of file diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 78f7bcf485c..092d122d5cf 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==9.2.0"], + "requirements": ["pychromecast==9.2.1"], "after_dependencies": [ "cloud", "http", diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index e83852d122f..f296b861aa4 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -224,6 +224,12 @@ class GenericThermostat(ClimateEntity, RestoreEntity): ): self._async_update_temp(sensor_state) self.async_write_ha_state() + switch_state = self.hass.states.get(self.heater_entity_id) + if switch_state and switch_state.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): + self.hass.create_task(self._check_switch_initial_state()) if self.hass.state == CoreState.running: _async_startup() @@ -267,14 +273,6 @@ class GenericThermostat(ClimateEntity, RestoreEntity): if not self._hvac_mode: self._hvac_mode = HVAC_MODE_OFF - # Prevent the device from keep running if HVAC_MODE_OFF - if self._hvac_mode == HVAC_MODE_OFF and self._is_device_active: - await self._async_heater_turn_off() - _LOGGER.warning( - "The climate mode is OFF, but the switch device is ON. Turning off device %s", - self.heater_entity_id, - ) - @property def should_poll(self): """Return the polling state.""" @@ -408,12 +406,24 @@ class GenericThermostat(ClimateEntity, RestoreEntity): await self._async_control_heating() self.async_write_ha_state() + async def _check_switch_initial_state(self): + """Prevent the device from keep running if HVAC_MODE_OFF.""" + if self._hvac_mode == HVAC_MODE_OFF and self._is_device_active: + _LOGGER.warning( + "The climate mode is OFF, but the switch device is ON. Turning off device %s", + self.heater_entity_id, + ) + await self._async_heater_turn_off() + @callback def _async_switch_changed(self, event): """Handle heater switch state changes.""" new_state = event.data.get("new_state") + old_state = event.data.get("old_state") if new_state is None: return + if old_state is None: + self.hass.create_task(self._check_switch_initial_state()) self.async_write_ha_state() @callback @@ -433,7 +443,6 @@ class GenericThermostat(ClimateEntity, RestoreEntity): if not self._active and None not in ( self._cur_temp, self._target_temp, - self._is_device_active, ): self._active = True _LOGGER.info( diff --git a/homeassistant/components/growatt_server/config_flow.py b/homeassistant/components/growatt_server/config_flow.py index d6b2c7db9fe..c4a97a81f0a 100644 --- a/homeassistant/components/growatt_server/config_flow.py +++ b/homeassistant/components/growatt_server/config_flow.py @@ -47,7 +47,7 @@ class GrowattServerConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not login_response["success"] and login_response["errCode"] == "102": return self._async_show_user_form({"base": "invalid_auth"}) - self.user_id = login_response["userId"] + self.user_id = login_response["user"]["id"] self.data = user_input return await self.async_step_plant() diff --git a/homeassistant/components/growatt_server/manifest.json b/homeassistant/components/growatt_server/manifest.json index ab2d07c147b..79472359ab9 100644 --- a/homeassistant/components/growatt_server/manifest.json +++ b/homeassistant/components/growatt_server/manifest.json @@ -3,7 +3,7 @@ "name": "Growatt", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/growatt_server/", - "requirements": ["growattServer==1.0.1"], + "requirements": ["growattServer==1.1.0"], "codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/growatt_server/sensor.py b/homeassistant/components/growatt_server/sensor.py index 03da4fe4b57..f2eea640e99 100644 --- a/homeassistant/components/growatt_server/sensor.py +++ b/homeassistant/components/growatt_server/sensor.py @@ -861,7 +861,7 @@ def get_device_list(api, config): if not login_response["success"] and login_response["errCode"] == "102": _LOGGER.error("Username, Password or URL may be incorrect!") return - user_id = login_response["userId"] + user_id = login_response["user"]["id"] if plant_id == DEFAULT_PLANT_ID: plant_info = api.plant_list(user_id) plant_id = plant_info["data"][0]["plantId"] diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index c36a32b0d5b..95c5f87b6c2 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -245,7 +245,7 @@ class Thermostat(HomeAccessory): def _set_chars(self, char_values): _LOGGER.debug("Thermostat _set_chars: %s", char_values) events = [] - params = {} + params = {ATTR_ENTITY_ID: self.entity_id} service = None state = self.hass.states.get(self.entity_id) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -285,12 +285,20 @@ class Thermostat(HomeAccessory): target_hc = hc_fallback break - service = SERVICE_SET_HVAC_MODE_THERMOSTAT - hass_value = self.hc_homekit_to_hass[target_hc] - params = {ATTR_HVAC_MODE: hass_value} + params[ATTR_HVAC_MODE] = self.hc_homekit_to_hass[target_hc] events.append( f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" ) + # Many integrations do not actually implement `hvac_mode` for the + # `SERVICE_SET_TEMPERATURE_THERMOSTAT` service so we made a call to + # `SERVICE_SET_HVAC_MODE_THERMOSTAT` before calling `SERVICE_SET_TEMPERATURE_THERMOSTAT` + # to ensure the device is in the right mode before setting the temp. + self.async_call_service( + DOMAIN_CLIMATE, + SERVICE_SET_HVAC_MODE_THERMOSTAT, + params.copy(), + ", ".join(events), + ) if CHAR_TARGET_TEMPERATURE in char_values: hc_target_temp = char_values[CHAR_TARGET_TEMPERATURE] @@ -357,7 +365,6 @@ class Thermostat(HomeAccessory): ) if service: - params[ATTR_ENTITY_ID] = self.entity_id self.async_call_service( DOMAIN_CLIMATE, service, diff --git a/homeassistant/components/kodi/manifest.json b/homeassistant/components/kodi/manifest.json index 78d0c6e5998..f88a893c7fa 100644 --- a/homeassistant/components/kodi/manifest.json +++ b/homeassistant/components/kodi/manifest.json @@ -2,7 +2,7 @@ "domain": "kodi", "name": "Kodi", "documentation": "https://www.home-assistant.io/integrations/kodi", - "requirements": ["pykodi==0.2.5"], + "requirements": ["pykodi==0.2.6"], "codeowners": ["@OnFreund", "@cgtobi"], "zeroconf": ["_xbmc-jsonrpc-h._tcp.local."], "config_flow": true, diff --git a/homeassistant/components/openuv/manifest.json b/homeassistant/components/openuv/manifest.json index 24af3f3a3af..207bd307d21 100644 --- a/homeassistant/components/openuv/manifest.json +++ b/homeassistant/components/openuv/manifest.json @@ -3,7 +3,7 @@ "name": "OpenUV", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openuv", - "requirements": ["pyopenuv==2.2.0"], + "requirements": ["pyopenuv==2.2.1"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/philips_js/media_player.py b/homeassistant/components/philips_js/media_player.py index e4512fc52f0..4499fb61e2a 100644 --- a/homeassistant/components/philips_js/media_player.py +++ b/homeassistant/components/philips_js/media_player.py @@ -213,9 +213,12 @@ class PhilipsTVMediaPlayer(CoordinatorEntity, MediaPlayerEntity): async def async_turn_off(self): """Turn off the device.""" - await self._tv.sendKey("Standby") - self._state = STATE_OFF - await self._async_update_soon() + if self._state == STATE_ON: + await self._tv.sendKey("Standby") + self._state = STATE_OFF + await self._async_update_soon() + else: + _LOGGER.debug("Ignoring turn off when already in expected state") async def async_volume_up(self): """Send volume up command.""" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 40d8ecc675e..27461d0d8ad 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.7.0", + "plexapi==4.7.1", "plexauth==0.0.6", "plexwebsocket==0.0.13" ], diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index fac929e7e99..d5eceab05fc 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -189,7 +189,7 @@ class RainMachineEntity(CoordinatorEntity): self._attr_device_info = { "identifiers": {(DOMAIN, controller.mac)}, "connections": {(dr.CONNECTION_NETWORK_MAC, controller.mac)}, - "name": controller.name, + "name": str(controller.name), "manufacturer": "RainMachine", "model": ( f"Version {controller.hardware_version} " diff --git a/homeassistant/components/sms/const.py b/homeassistant/components/sms/const.py index b73e7954fc1..ab2c15a0c49 100644 --- a/homeassistant/components/sms/const.py +++ b/homeassistant/components/sms/const.py @@ -2,3 +2,4 @@ DOMAIN = "sms" SMS_GATEWAY = "SMS_GATEWAY" +SMS_STATE_UNREAD = "UnRead" diff --git a/homeassistant/components/sms/gateway.py b/homeassistant/components/sms/gateway.py index 5003f7019ca..3034580d5e0 100644 --- a/homeassistant/components/sms/gateway.py +++ b/homeassistant/components/sms/gateway.py @@ -6,7 +6,7 @@ from gammu.asyncworker import GammuAsyncWorker # pylint: disable=import-error from homeassistant.core import callback -from .const import DOMAIN +from .const import DOMAIN, SMS_STATE_UNREAD _LOGGER = logging.getLogger(__name__) @@ -14,24 +14,39 @@ _LOGGER = logging.getLogger(__name__) class Gateway: """SMS gateway to interact with a GSM modem.""" - def __init__(self, worker, hass): + def __init__(self, config, hass): """Initialize the sms gateway.""" - self._worker = worker + self._worker = GammuAsyncWorker(self.sms_pull) + self._worker.configure(config) self._hass = hass + self._first_pull = True async def init_async(self): """Initialize the sms gateway asynchronously.""" + await self._worker.init_async() try: await self._worker.set_incoming_sms_async() except gammu.ERR_NOTSUPPORTED: - _LOGGER.warning("Your phone does not support incoming SMS notifications!") + _LOGGER.warning("Falling back to pulling method for SMS notifications") except gammu.GSMError: _LOGGER.warning( - "GSM error, your phone does not support incoming SMS notifications!" + "GSM error, falling back to pulling method for SMS notifications" ) else: await self._worker.set_incoming_callback_async(self.sms_callback) + def sms_pull(self, state_machine): + """Pull device. + + @param state_machine: state machine + @type state_machine: gammu.StateMachine + """ + state_machine.ReadDevice() + + _LOGGER.debug("Pulling modem") + self.sms_read_messages(state_machine, self._first_pull) + self._first_pull = False + def sms_callback(self, state_machine, callback_type, callback_data): """Receive notification about incoming event. @@ -45,7 +60,15 @@ class Gateway: _LOGGER.debug( "Received incoming event type:%s,data:%s", callback_type, callback_data ) - entries = self.get_and_delete_all_sms(state_machine) + self.sms_read_messages(state_machine) + + def sms_read_messages(self, state_machine, force=False): + """Read all received SMS messages. + + @param state_machine: state machine which invoked action + @type state_machine: gammu.StateMachine + """ + entries = self.get_and_delete_all_sms(state_machine, force) _LOGGER.debug("SMS entries:%s", entries) data = [] @@ -53,22 +76,25 @@ class Gateway: decoded_entry = gammu.DecodeSMS(entry) message = entry[0] _LOGGER.debug("Processing sms:%s,decoded:%s", message, decoded_entry) - if decoded_entry is None: - text = message["Text"] - else: - text = "" - for inner_entry in decoded_entry["Entries"]: - if inner_entry["Buffer"] is not None: - text = text + inner_entry["Buffer"] + sms_state = message["State"] + _LOGGER.debug("SMS state:%s", sms_state) + if sms_state == SMS_STATE_UNREAD: + if decoded_entry is None: + text = message["Text"] + else: + text = "" + for inner_entry in decoded_entry["Entries"]: + if inner_entry["Buffer"] is not None: + text += inner_entry["Buffer"] - event_data = { - "phone": message["Number"], - "date": str(message["DateTime"]), - "message": text, - } + event_data = { + "phone": message["Number"], + "date": str(message["DateTime"]), + "message": text, + } - _LOGGER.debug("Append event data:%s", event_data) - data.append(event_data) + _LOGGER.debug("Append event data:%s", event_data) + data.append(event_data) self._hass.add_job(self._notify_incoming_sms, data) @@ -161,10 +187,7 @@ class Gateway: async def create_sms_gateway(config, hass): """Create the sms gateway.""" try: - worker = GammuAsyncWorker() - worker.configure(config) - await worker.init_async() - gateway = Gateway(worker, hass) + gateway = Gateway(config, hass) await gateway.init_async() return gateway except gammu.GSMError as exc: diff --git a/homeassistant/components/sms/manifest.json b/homeassistant/components/sms/manifest.json index 9a466236758..6d736ac44e7 100644 --- a/homeassistant/components/sms/manifest.json +++ b/homeassistant/components/sms/manifest.json @@ -3,7 +3,7 @@ "name": "SMS notifications via GSM-modem", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sms", - "requirements": ["python-gammu==3.1"], + "requirements": ["python-gammu==3.2.3"], "codeowners": ["@ocalvo"], "iot_class": "local_polling" } diff --git a/homeassistant/components/switcher_kis/manifest.json b/homeassistant/components/switcher_kis/manifest.json index 33ec7a67d92..88d3d2a3ad4 100644 --- a/homeassistant/components/switcher_kis/manifest.json +++ b/homeassistant/components/switcher_kis/manifest.json @@ -3,7 +3,7 @@ "name": "Switcher", "documentation": "https://www.home-assistant.io/integrations/switcher_kis/", "codeowners": ["@tomerfi","@thecode"], - "requirements": ["aioswitcher==2.0.5"], + "requirements": ["aioswitcher==2.0.6"], "iot_class": "local_push", "config_flow": true } diff --git a/homeassistant/components/xiaomi_miio/__init__.py b/homeassistant/components/xiaomi_miio/__init__.py index cde597432df..7dac02aaa53 100644 --- a/homeassistant/components/xiaomi_miio/__init__.py +++ b/homeassistant/components/xiaomi_miio/__init__.py @@ -171,12 +171,23 @@ async def async_create_miio_device_and_coordinator( async def async_update_data(): """Fetch data from the device using async_add_executor_job.""" - try: + + async def _async_fetch_data(): + """Fetch data from the device.""" async with async_timeout.timeout(10): state = await hass.async_add_executor_job(device.status) _LOGGER.debug("Got new state: %s", state) return state + try: + return await _async_fetch_data() + except DeviceException as ex: + if getattr(ex, "code", None) != -9999: + raise UpdateFailed(ex) from ex + _LOGGER.info("Got exception while fetching the state, trying again: %s", ex) + # Try to fetch the data a second time after error code -9999 + try: + return await _async_fetch_data() except DeviceException as ex: raise UpdateFailed(ex) from ex diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index a0deb0fdf21..ed4ce0cbf58 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -70,8 +70,8 @@ ACTION_RECOVER = "recover" ACTION_STAY = "stay" ACTION_OFF = "off" -ACTIVE_MODE_NIGHTLIGHT = "1" -ACTIVE_COLOR_FLOWING = "1" +ACTIVE_MODE_NIGHTLIGHT = 1 +ACTIVE_COLOR_FLOWING = 1 NIGHTLIGHT_SWITCH_TYPE_LIGHT = "light" @@ -610,7 +610,7 @@ class YeelightDevice: # Only ceiling lights have active_mode, from SDK docs: # active_mode 0: daylight mode / 1: moonlight mode (ceiling light only) if self._active_mode is not None: - return self._active_mode == ACTIVE_MODE_NIGHTLIGHT + return int(self._active_mode) == ACTIVE_MODE_NIGHTLIGHT if self._nightlight_brightness is not None: return int(self._nightlight_brightness) > 0 @@ -620,7 +620,7 @@ class YeelightDevice: @property def is_color_flow_enabled(self) -> bool: """Return true / false if color flow is currently running.""" - return self._color_flow == ACTIVE_COLOR_FLOWING + return int(self._color_flow) == ACTIVE_COLOR_FLOWING @property def _active_mode(self): diff --git a/homeassistant/const.py b/homeassistant/const.py index 110c49fbc01..dde2c8db2f5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -5,7 +5,7 @@ from typing import Final MAJOR_VERSION: Final = 2021 MINOR_VERSION: Final = 9 -PATCH_VERSION: Final = "6" +PATCH_VERSION: Final = "7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) diff --git a/requirements_all.txt b/requirements_all.txt index 1ebca9da385..deafc44258e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ aiorecollect==1.0.8 aioshelly==0.6.4 # homeassistant.components.switcher_kis -aioswitcher==2.0.5 +aioswitcher==2.0.6 # homeassistant.components.syncthing aiosyncthing==0.5.1 @@ -739,7 +739,7 @@ greeneye_monitor==2.1 greenwavereality==0.5.1 # homeassistant.components.growatt_server -growattServer==1.0.1 +growattServer==1.1.0 # homeassistant.components.gstreamer gstreamer-player==1.1.2 @@ -1200,7 +1200,7 @@ pillow==8.2.0 pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.7.0 +plexapi==4.7.1 # homeassistant.components.plex plexauth==0.0.6 @@ -1373,7 +1373,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==9.2.0 +pychromecast==9.2.1 # homeassistant.components.pocketcasts pycketcasts==1.0.0 @@ -1551,7 +1551,7 @@ pykira==0.1.1 pykmtronic==0.3.0 # homeassistant.components.kodi -pykodi==0.2.5 +pykodi==0.2.6 # homeassistant.components.kraken pykrakenapi==0.1.8 @@ -1668,7 +1668,7 @@ pyobihai==1.3.1 pyombi==0.1.10 # homeassistant.components.openuv -pyopenuv==2.2.0 +pyopenuv==2.2.1 # homeassistant.components.opnsense pyopnsense==0.2.0 @@ -1856,7 +1856,7 @@ python-family-hub-local==0.0.2 python-forecastio==1.4.0 # homeassistant.components.sms -# python-gammu==3.1 +# python-gammu==3.2.3 # homeassistant.components.gc100 python-gc100==1.0.3a0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba9d9bf0128..2e48ce94e0b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aiorecollect==1.0.8 aioshelly==0.6.4 # homeassistant.components.switcher_kis -aioswitcher==2.0.5 +aioswitcher==2.0.6 # homeassistant.components.syncthing aiosyncthing==0.5.1 @@ -423,7 +423,7 @@ googlemaps==2.5.1 greeclimate==0.11.8 # homeassistant.components.growatt_server -growattServer==1.0.1 +growattServer==1.1.0 # homeassistant.components.profiler guppy3==3.1.0 @@ -678,7 +678,7 @@ pilight==0.1.1 pillow==8.2.0 # homeassistant.components.plex -plexapi==4.7.0 +plexapi==4.7.1 # homeassistant.components.plex plexauth==0.0.6 @@ -788,7 +788,7 @@ pybotvac==0.0.22 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==9.2.0 +pychromecast==9.2.1 # homeassistant.components.climacell pyclimacell==0.18.2 @@ -891,7 +891,7 @@ pykira==0.1.1 pykmtronic==0.3.0 # homeassistant.components.kodi -pykodi==0.2.5 +pykodi==0.2.6 # homeassistant.components.kraken pykrakenapi==0.1.8 @@ -966,7 +966,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.openuv -pyopenuv==2.2.0 +pyopenuv==2.2.1 # homeassistant.components.opnsense pyopnsense==0.2.0 diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 4b9fbca41e2..317f9d3e74e 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -42,6 +42,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM from tests.common import ( assert_setup_component, async_fire_time_changed, + async_mock_service, mock_restore_cache, ) from tests.components.climate import common @@ -1174,14 +1175,15 @@ async def test_custom_setup_params(hass): assert state.attributes.get("temperature") == TARGET_TEMP -async def test_restore_state(hass): +@pytest.mark.parametrize("hvac_mode", [HVAC_MODE_OFF, HVAC_MODE_HEAT, HVAC_MODE_COOL]) +async def test_restore_state(hass, hvac_mode): """Ensure states are restored on startup.""" mock_restore_cache( hass, ( State( "climate.test_thermostat", - HVAC_MODE_OFF, + hvac_mode, {ATTR_TEMPERATURE: "20", ATTR_PRESET_MODE: PRESET_AWAY}, ), ), @@ -1206,7 +1208,7 @@ async def test_restore_state(hass): state = hass.states.get("climate.test_thermostat") assert state.attributes[ATTR_TEMPERATURE] == 20 assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY - assert state.state == HVAC_MODE_OFF + assert state.state == hvac_mode async def test_no_restore_state(hass): @@ -1332,6 +1334,66 @@ async def test_restore_will_turn_off_(hass): assert hass.states.get(heater_switch).state == STATE_ON +async def test_restore_will_turn_off_when_loaded_second(hass): + """Ensure that restored state is coherent with real situation. + + Switch is not available until after component is loaded + """ + heater_switch = "input_boolean.test" + mock_restore_cache( + hass, + ( + State( + "climate.test_thermostat", + HVAC_MODE_HEAT, + {ATTR_TEMPERATURE: "18", ATTR_PRESET_MODE: PRESET_NONE}, + ), + State(heater_switch, STATE_ON, {}), + ), + ) + + hass.state = CoreState.starting + + await hass.async_block_till_done() + assert hass.states.get(heater_switch) is None + + _setup_sensor(hass, 16) + + await async_setup_component( + hass, + DOMAIN, + { + "climate": { + "platform": "generic_thermostat", + "name": "test_thermostat", + "heater": heater_switch, + "target_sensor": ENT_SENSOR, + "target_temp": 20, + "initial_hvac_mode": HVAC_MODE_OFF, + } + }, + ) + await hass.async_block_till_done() + state = hass.states.get("climate.test_thermostat") + assert state.attributes[ATTR_TEMPERATURE] == 20 + assert state.state == HVAC_MODE_OFF + + calls_on = async_mock_service(hass, ha.DOMAIN, SERVICE_TURN_ON) + calls_off = async_mock_service(hass, ha.DOMAIN, SERVICE_TURN_OFF) + + assert await async_setup_component( + hass, input_boolean.DOMAIN, {"input_boolean": {"test": None}} + ) + await hass.async_block_till_done() + # heater must be switched off + assert len(calls_on) == 0 + assert len(calls_off) == 1 + call = calls_off[0] + assert call.domain == HASS_DOMAIN + assert call.service == SERVICE_TURN_OFF + assert call.data["entity_id"] == "input_boolean.test" + + async def test_restore_state_uncoherence_case(hass): """ Test restore from a strange state. diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py index 096052fd6cf..db46ed36911 100644 --- a/tests/components/growatt_server/test_config_flow.py +++ b/tests/components/growatt_server/test_config_flow.py @@ -40,7 +40,7 @@ GROWATT_PLANT_LIST_RESPONSE = { }, "success": True, } -GROWATT_LOGIN_RESPONSE = {"userId": 123456, "userLevel": 1, "success": True} +GROWATT_LOGIN_RESPONSE = {"user": {"id": 123456}, "userLevel": 1, "success": True} async def test_show_authenticate_form(hass): diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index d9c2a6bf0ed..e73465b0ab0 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -560,6 +560,119 @@ async def test_thermostat_auto(hass, hk_driver, events): ) +async def test_thermostat_mode_and_temp_change(hass, hk_driver, events): + """Test if accessory where the mode and temp change in the same call.""" + entity_id = "climate.test" + + # support_auto = True + hass.states.async_set( + entity_id, + HVAC_MODE_OFF, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_TARGET_TEMPERATURE + | SUPPORT_TARGET_TEMPERATURE_RANGE, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + acc = Thermostat(hass, hk_driver, "Climate", entity_id, 1, None) + hk_driver.add_accessory(acc) + + await acc.run() + await hass.async_block_till_done() + + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_heating_thresh_temp.value == 19.0 + + assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP + assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == 7.0 + assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1 + assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP + assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == 7.0 + assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1 + + hass.states.async_set( + entity_id, + HVAC_MODE_COOL, + { + ATTR_TARGET_TEMP_HIGH: 23.0, + ATTR_TARGET_TEMP_LOW: 19.0, + ATTR_CURRENT_TEMPERATURE: 21.0, + ATTR_HVAC_ACTION: CURRENT_HVAC_COOL, + ATTR_HVAC_MODES: [ + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_COOL, + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + ], + }, + ) + await hass.async_block_till_done() + assert acc.char_heating_thresh_temp.value == 19.0 + assert acc.char_cooling_thresh_temp.value == 23.0 + assert acc.char_current_heat_cool.value == HC_HEAT_COOL_COOL + assert acc.char_target_heat_cool.value == HC_HEAT_COOL_COOL + assert acc.char_current_temp.value == 21.0 + assert acc.char_display_units.value == 0 + + # Set from HomeKit + call_set_temperature = async_mock_service(hass, DOMAIN_CLIMATE, "set_temperature") + call_set_hvac_mode = async_mock_service(hass, DOMAIN_CLIMATE, "set_hvac_mode") + + char_heating_thresh_temp_iid = acc.char_heating_thresh_temp.to_HAP()[HAP_REPR_IID] + char_cooling_thresh_temp_iid = acc.char_cooling_thresh_temp.to_HAP()[HAP_REPR_IID] + char_target_heat_cool_iid = acc.char_target_heat_cool.to_HAP()[HAP_REPR_IID] + + hk_driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_heating_thresh_temp_iid, + HAP_REPR_VALUE: 20.0, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_cooling_thresh_temp_iid, + HAP_REPR_VALUE: 25.0, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_target_heat_cool_iid, + HAP_REPR_VALUE: HC_HEAT_COOL_AUTO, + }, + ] + }, + "mock_addr", + ) + + await hass.async_block_till_done() + assert call_set_hvac_mode[0] + assert call_set_hvac_mode[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_hvac_mode[0].data[ATTR_HVAC_MODE] == HVAC_MODE_HEAT_COOL + assert call_set_temperature[0] + assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 20.0 + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 25.0 + assert acc.char_heating_thresh_temp.value == 20.0 + assert acc.char_cooling_thresh_temp.value == 25.0 + assert len(events) == 2 + assert events[-2].data[ATTR_VALUE] == "TargetHeatingCoolingState to 3" + assert ( + events[-1].data[ATTR_VALUE] + == "TargetHeatingCoolingState to 3, CoolingThresholdTemperature to 25.0°C, HeatingThresholdTemperature to 20.0°C" + ) + + async def test_thermostat_humidity(hass, hk_driver, events): """Test if accessory and HA are updated accordingly with humidity.""" entity_id = "climate.test"