diff --git a/homeassistant/components/free_mobile/manifest.json b/homeassistant/components/free_mobile/manifest.json index ea6ea921a38..7fb7f998643 100644 --- a/homeassistant/components/free_mobile/manifest.json +++ b/homeassistant/components/free_mobile/manifest.json @@ -2,7 +2,7 @@ "domain": "free_mobile", "name": "Free Mobile", "documentation": "https://www.home-assistant.io/integrations/free_mobile", - "requirements": ["freesms==0.1.2"], + "requirements": ["freesms==0.2.0"], "codeowners": [], "iot_class": "cloud_push" } diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index dcf4f1bc765..dfc524bf16c 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -578,9 +578,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): schedule_id = sid if not schedule_id: - _LOGGER.error( - "%s is not a invalid schedule", kwargs.get(ATTR_SCHEDULE_NAME) - ) + _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) return self._data.switch_home_schedule(home_id=self._home_id, schedule_id=schedule_id) diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json index bd33efb6ea1..090bc3dd9d6 100644 --- a/homeassistant/components/netatmo/manifest.json +++ b/homeassistant/components/netatmo/manifest.json @@ -2,13 +2,27 @@ "domain": "netatmo", "name": "Netatmo", "documentation": "https://www.home-assistant.io/integrations/netatmo", - "requirements": ["pyatmo==4.2.2"], - "after_dependencies": ["cloud", "media_source"], - "dependencies": ["webhook"], - "codeowners": ["@cgtobi"], + "requirements": [ + "pyatmo==4.2.3" + ], + "after_dependencies": [ + "cloud", + "media_source" + ], + "dependencies": [ + "webhook" + ], + "codeowners": [ + "@cgtobi" + ], "config_flow": true, "homekit": { - "models": ["Healty Home Coach", "Netatmo Relay", "Presence", "Welcome"] + "models": [ + "Healty Home Coach", + "Netatmo Relay", + "Presence", + "Welcome" + ] }, "iot_class": "cloud_polling" -} +} \ No newline at end of file diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index a87280aaabd..c117d262cbf 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.45"], + "requirements": ["pysonos==0.0.47"], "after_dependencies": ["plex"], "ssdp": [ { diff --git a/homeassistant/components/sonos/speaker.py b/homeassistant/components/sonos/speaker.py index 56eba68abe1..f090a7062e5 100644 --- a/homeassistant/components/sonos/speaker.py +++ b/homeassistant/components/sonos/speaker.py @@ -159,8 +159,9 @@ class SonosSpeaker: self, target: SubscriptionBase, sub_callback: Callable ) -> None: """Create a Sonos subscription.""" - subscription = await target.subscribe(auto_renew=True) + subscription = await target.subscribe(auto_renew=True, requested_timeout=1200) subscription.callback = sub_callback + subscription.auto_renew_fail = self.async_renew_failed self._subscriptions.append(subscription) @callback @@ -241,11 +242,19 @@ class SonosSpeaker: self.async_write_entity_states() + @callback + def async_renew_failed(self, exception: Exception) -> None: + """Handle a failed subscription renewal.""" + if self.available: + self.hass.async_add_job(self.async_unseen) + async def async_unseen(self, now: datetime.datetime | None = None) -> None: """Make this player unavailable when it was not seen recently.""" self.async_write_entity_states() - self._seen_timer = None + if self._seen_timer: + self._seen_timer() + self._seen_timer = None if self._poll_timer: self._poll_timer() diff --git a/homeassistant/components/tasmota/light.py b/homeassistant/components/tasmota/light.py index 53db34a9001..58a1ff1fb23 100644 --- a/homeassistant/components/tasmota/light.py +++ b/homeassistant/components/tasmota/light.py @@ -55,6 +55,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) +def clamp(value): + """Clamp value to the range 0..255.""" + return min(max(value, 0), 255) + + class TasmotaLight( TasmotaAvailability, TasmotaDiscoveryUpdate, @@ -136,22 +141,7 @@ class TasmotaLight( percent_bright = brightness / TASMOTA_BRIGHTNESS_MAX self._brightness = percent_bright * 255 if "color" in attributes: - - def clamp(value): - """Clamp value to the range 0..255.""" - return min(max(value, 0), 255) - - rgb = attributes["color"] - # Tasmota's RGB color is adjusted for brightness, compensate - if self._brightness > 0: - red_compensated = clamp(round(rgb[0] / self._brightness * 255)) - green_compensated = clamp(round(rgb[1] / self._brightness * 255)) - blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) - else: - red_compensated = 0 - green_compensated = 0 - blue_compensated = 0 - self._rgb = [red_compensated, green_compensated, blue_compensated] + self._rgb = attributes["color"][0:3] if "color_temp" in attributes: self._color_temp = attributes["color_temp"] if "effect" in attributes: @@ -207,14 +197,38 @@ class TasmotaLight( @property def rgb_color(self): """Return the rgb color value.""" - return self._rgb + if self._rgb is None: + return None + rgb = self._rgb + # Tasmota's RGB color is adjusted for brightness, compensate + if self._brightness > 0: + red_compensated = clamp(round(rgb[0] / self._brightness * 255)) + green_compensated = clamp(round(rgb[1] / self._brightness * 255)) + blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) + else: + red_compensated = 0 + green_compensated = 0 + blue_compensated = 0 + return [red_compensated, green_compensated, blue_compensated] @property def rgbw_color(self): """Return the rgbw color value.""" if self._rgb is None or self._white_value is None: return None - return [*self._rgb, self._white_value] + rgb = self._rgb + # Tasmota's color is adjusted for brightness, compensate + if self._brightness > 0: + red_compensated = clamp(round(rgb[0] / self._brightness * 255)) + green_compensated = clamp(round(rgb[1] / self._brightness * 255)) + blue_compensated = clamp(round(rgb[2] / self._brightness * 255)) + white_compensated = clamp(round(self._white_value / self._brightness * 255)) + else: + red_compensated = 0 + green_compensated = 0 + blue_compensated = 0 + white_compensated = 0 + return [red_compensated, green_compensated, blue_compensated, white_compensated] @property def force_update(self): @@ -250,18 +264,10 @@ class TasmotaLight( if ATTR_RGBW_COLOR in kwargs and COLOR_MODE_RGBW in supported_color_modes: rgbw = kwargs[ATTR_RGBW_COLOR] + attributes["color"] = [rgbw[0], rgbw[1], rgbw[2], rgbw[3]] # Tasmota does not support direct RGBW control, the light must be set to # either white mode or color mode. Set the mode to white if white channel - # is on, and to color otheruse - if rgbw[3] == 0: - attributes["color"] = [rgbw[0], rgbw[1], rgbw[2]] - else: - white_value_normalized = rgbw[3] / DEFAULT_BRIGHTNESS_MAX - device_white_value = min( - round(white_value_normalized * TASMOTA_BRIGHTNESS_MAX), - TASMOTA_BRIGHTNESS_MAX, - ) - attributes["white_value"] = device_white_value + # is on, and to color otherwise if ATTR_TRANSITION in kwargs: attributes["transition"] = kwargs[ATTR_TRANSITION] diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index a6e7a1d45a8..15b5501adce 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -3,7 +3,7 @@ "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.2.12"], + "requirements": ["hatasmota==0.2.13"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"], diff --git a/homeassistant/const.py b/homeassistant/const.py index 6df593e984b..bd802526e0a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 MINOR_VERSION = 5 -PATCH_VERSION = "4" +PATCH_VERSION = "5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 8, 0) diff --git a/requirements_all.txt b/requirements_all.txt index af2e557446a..9d227d63288 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -618,7 +618,7 @@ fortiosapi==0.10.8 freebox-api==0.0.10 # homeassistant.components.free_mobile -freesms==0.1.2 +freesms==0.2.0 # homeassistant.components.fritz # homeassistant.components.fritzbox_callmonitor @@ -735,7 +735,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.12 +hatasmota==0.2.13 # homeassistant.components.jewish_calendar hdate==0.10.2 @@ -1286,7 +1286,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==4.2.2 +pyatmo==4.2.3 # homeassistant.components.atome pyatome==0.1.1 @@ -1741,7 +1741,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.45 +pysonos==0.0.47 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 91f7c1422ee..dedf01de7ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -405,7 +405,7 @@ hangups==0.4.11 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.12 +hatasmota==0.2.13 # homeassistant.components.jewish_calendar hdate==0.10.2 @@ -702,7 +702,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==4.2.2 +pyatmo==4.2.3 # homeassistant.components.apple_tv pyatv==0.7.7 @@ -959,7 +959,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.45 +pysonos==0.0.47 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index a3cad1e6d81..16359c85498 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -432,7 +432,7 @@ async def test_service_schedule_thermostats(hass, climate_entry, caplog): await hass.async_block_till_done() mock_switch_home_schedule.assert_not_called() - assert "summer is not a invalid schedule" in caplog.text + assert "summer is not a valid schedule" in caplog.text async def test_service_preset_mode_already_boost_valves(hass, climate_entry): diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 74e8d2a5e59..44f57581694 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -36,7 +36,7 @@ DEFAULT_CONFIG = { "ofln": "Offline", "onln": "Online", "state": ["OFF", "ON", "TOGGLE", "HOLD"], - "sw": "8.4.0.2", + "sw": "9.4.0.4", "swn": [None, None, None, None, None], "t": "tasmota_49A3BC", "ft": "%topic%/%prefix%/", diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index 3a27409e433..b74799d1d12 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -197,7 +197,7 @@ async def test_attributes_rgbw(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) config["rl"][0] = 2 - config["lt_st"] = 4 # 5 channel light (RGBW) + config["lt_st"] = 4 # 4 channel light (RGBW) mac = config["mac"] async_fire_mqtt_message( @@ -406,6 +406,99 @@ async def test_controlling_state_via_mqtt_ct(hass, mqtt_mock, setup_tasmota): assert state.attributes.get("color_mode") == "color_temp" +async def test_controlling_state_via_mqtt_rgbw(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 4 # 4 channel light (RGBW) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get(ATTR_ASSUMED_STATE) + assert "color_mode" not in state.attributes + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON"}') + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_mode") == "rgbw" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"OFF"}') + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert "color_mode" not in state.attributes + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("color_mode") == "rgbw" + + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Color":"128,64,0","White":0}', + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("rgb_color") == (255, 128, 0) + assert state.attributes.get("rgbw_color") == (255, 128, 0, 0) + assert state.attributes.get("color_mode") == "rgbw" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 127.5 + assert state.attributes.get("rgb_color") == (255, 192, 128) + assert state.attributes.get("rgbw_color") == (255, 128, 0, 255) + assert state.attributes.get("color_mode") == "rgbw" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":0}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 0 + assert state.attributes.get("rgb_color") == (0, 0, 0) + assert state.attributes.get("rgbw_color") == (0, 0, 0, 0) + assert state.attributes.get("color_mode") == "rgbw" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Scheme":3}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "Cycle down" + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"ON"}') + + state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "tasmota_49A3BC/stat/RESULT", '{"POWER":"OFF"}') + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async def test_controlling_state_via_mqtt_rgbww(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -667,7 +760,17 @@ async def test_controlling_state_via_mqtt_rgbww_tuya(hass, mqtt_mock, setup_tasm assert state.attributes.get("color_mode") == "rgb" async_fire_mqtt_message( - hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","White":50}' + hass, + "tasmota_49A3BC/tele/STATE", + '{"POWER":"ON","Dimmer":0}', + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (0, 0, 0) + assert state.attributes.get("color_mode") == "rgb" + + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":50,"White":50}' ) state = hass.states.get("light.test") assert state.state == STATE_ON @@ -799,9 +902,10 @@ async def test_sending_mqtt_commands_rgbww_tuya(hass, mqtt_mock, setup_tasmota): ) -async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): +async def test_sending_mqtt_commands_rgbw_legacy(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) + config["sw"] = "9.4.0.3" # RGBW support was added in 9.4.0.4 config["rl"][0] = 2 config["lt_st"] = 4 # 4 channel light (RGBW) mac = config["mac"] @@ -895,6 +999,102 @@ async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): mqtt_mock.async_publish.reset_mock() +async def test_sending_mqtt_commands_rgbw(hass, mqtt_mock, setup_tasmota): + """Test the sending MQTT commands.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 4 # 4 channel light (RGBW) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Turn the light on and verify MQTT message is sent + await common.async_turn_on(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Tasmota is not optimistic, the state should still be off + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + # Turn the light off and verify MQTT message is sent + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Turn the light on and verify MQTT messages are sent + await common.async_turn_on(hass, "light.test", brightness=192) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer4 75", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set color when setting color + await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 32]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Color2 128,64,32", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Set color when setting white is off + await common.async_turn_on(hass, "light.test", rgbw_color=[128, 64, 32, 0]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Color2 128,64,32,0", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Set white when white is on + await common.async_turn_on(hass, "light.test", rgbw_color=[16, 64, 32, 128]) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Color2 16,64,32,128", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", white_value=128) + # white_value should be ignored + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", effect="Random") + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Power1 ON;NoDelay;Scheme 4", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): """Test the sending MQTT commands.""" config = copy.deepcopy(DEFAULT_CONFIG) diff --git a/tests/fixtures/netatmo/homesdata.json b/tests/fixtures/netatmo/homesdata.json index aecab91550c..9c5e985218f 100644 --- a/tests/fixtures/netatmo/homesdata.json +++ b/tests/fixtures/netatmo/homesdata.json @@ -89,7 +89,7 @@ "room_id": "3688132631" } ], - "therm_schedules": [ + "schedules": [ { "zones": [ { @@ -398,171 +398,6 @@ "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" } ], - "schedules": [ - { - "zones": [ - { - "type": 0, - "name": "Komfort", - "rooms_temp": [ - { - "temp": 21, - "room_id": "2746182631" - } - ], - "id": 0, - "rooms": [ - { - "id": "2746182631", - "therm_setpoint_temperature": 21 - } - ] - }, - { - "type": 1, - "name": "Nacht", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 1, - "rooms": [ - { - "id": "2746182631", - "therm_setpoint_temperature": 17 - } - ] - }, - { - "type": 5, - "name": "Eco", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 4, - "rooms": [ - { - "id": "2746182631", - "therm_setpoint_temperature": 17 - } - ] - } - ], - "timetable": [ - { - "zone_id": 1, - "m_offset": 0 - }, - { - "zone_id": 0, - "m_offset": 360 - }, - { - "zone_id": 4, - "m_offset": 420 - }, - { - "zone_id": 0, - "m_offset": 960 - }, - { - "zone_id": 1, - "m_offset": 1410 - }, - { - "zone_id": 0, - "m_offset": 1800 - }, - { - "zone_id": 4, - "m_offset": 1860 - }, - { - "zone_id": 0, - "m_offset": 2400 - }, - { - "zone_id": 1, - "m_offset": 2850 - }, - { - "zone_id": 0, - "m_offset": 3240 - }, - { - "zone_id": 4, - "m_offset": 3300 - }, - { - "zone_id": 0, - "m_offset": 3840 - }, - { - "zone_id": 1, - "m_offset": 4290 - }, - { - "zone_id": 0, - "m_offset": 4680 - }, - { - "zone_id": 4, - "m_offset": 4740 - }, - { - "zone_id": 0, - "m_offset": 5280 - }, - { - "zone_id": 1, - "m_offset": 5730 - }, - { - "zone_id": 0, - "m_offset": 6120 - }, - { - "zone_id": 4, - "m_offset": 6180 - }, - { - "zone_id": 0, - "m_offset": 6720 - }, - { - "zone_id": 1, - "m_offset": 7170 - }, - { - "zone_id": 0, - "m_offset": 7620 - }, - { - "zone_id": 1, - "m_offset": 8610 - }, - { - "zone_id": 0, - "m_offset": 9060 - }, - { - "zone_id": 1, - "m_offset": 10050 - } - ], - "hg_temp": 7, - "away_temp": 14, - "name": "Default", - "id": "591b54a2764ff4d50d8b5795", - "selected": true, - "type": "therm" - } - ], "therm_mode": "schedule" }, {