mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 19:57:07 +00:00
Merge pull request #75528 from home-assistant/rc
This commit is contained in:
commit
cd0656bab0
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from math import ceil
|
from math import ceil
|
||||||
from typing import Any, cast
|
from typing import Any
|
||||||
|
|
||||||
from pyairvisual import CloudAPI, NodeSamba
|
from pyairvisual import CloudAPI, NodeSamba
|
||||||
from pyairvisual.errors import (
|
from pyairvisual.errors import (
|
||||||
@ -12,6 +12,7 @@ from pyairvisual.errors import (
|
|||||||
InvalidKeyError,
|
InvalidKeyError,
|
||||||
KeyExpiredError,
|
KeyExpiredError,
|
||||||
NodeProError,
|
NodeProError,
|
||||||
|
UnauthorizedError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@ -210,9 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data = await api_coro
|
return await api_coro
|
||||||
return cast(dict[str, Any], data)
|
except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex:
|
||||||
except (InvalidKeyError, KeyExpiredError) as ex:
|
|
||||||
raise ConfigEntryAuthFailed from ex
|
raise ConfigEntryAuthFailed from ex
|
||||||
except AirVisualError as err:
|
except AirVisualError as err:
|
||||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||||
@ -253,8 +253,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
async with NodeSamba(
|
async with NodeSamba(
|
||||||
entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD]
|
entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD]
|
||||||
) as node:
|
) as node:
|
||||||
data = await node.async_get_latest_measurements()
|
return await node.async_get_latest_measurements()
|
||||||
return cast(dict[str, Any], data)
|
|
||||||
except NodeProError as err:
|
except NodeProError as err:
|
||||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||||
|
|
||||||
|
@ -9,8 +9,10 @@ from pyairvisual import CloudAPI, NodeSamba
|
|||||||
from pyairvisual.errors import (
|
from pyairvisual.errors import (
|
||||||
AirVisualError,
|
AirVisualError,
|
||||||
InvalidKeyError,
|
InvalidKeyError,
|
||||||
|
KeyExpiredError,
|
||||||
NodeProError,
|
NodeProError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
|
UnauthorizedError,
|
||||||
)
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@ -119,7 +121,7 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input[CONF_API_KEY] not in valid_keys:
|
if user_input[CONF_API_KEY] not in valid_keys:
|
||||||
try:
|
try:
|
||||||
await coro
|
await coro
|
||||||
except InvalidKeyError:
|
except (InvalidKeyError, KeyExpiredError, UnauthorizedError):
|
||||||
errors[CONF_API_KEY] = "invalid_api_key"
|
errors[CONF_API_KEY] = "invalid_api_key"
|
||||||
except NotFoundError:
|
except NotFoundError:
|
||||||
errors[CONF_CITY] = "location_not_found"
|
errors[CONF_CITY] = "location_not_found"
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "AirVisual",
|
"name": "AirVisual",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airvisual",
|
"documentation": "https://www.home-assistant.io/integrations/airvisual",
|
||||||
"requirements": ["pyairvisual==5.0.9"],
|
"requirements": ["pyairvisual==2022.07.0"],
|
||||||
"codeowners": ["@bachya"],
|
"codeowners": ["@bachya"],
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyairvisual", "pysmb"]
|
"loggers": ["pyairvisual", "pysmb"]
|
||||||
|
@ -88,6 +88,7 @@ class AladdinDevice(CoverEntity):
|
|||||||
self._device_id = device["device_id"]
|
self._device_id = device["device_id"]
|
||||||
self._number = device["door_number"]
|
self._number = device["door_number"]
|
||||||
self._attr_name = device["name"]
|
self._attr_name = device["name"]
|
||||||
|
self._serial = device["serial"]
|
||||||
self._attr_unique_id = f"{self._device_id}-{self._number}"
|
self._attr_unique_id = f"{self._device_id}-{self._number}"
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
@ -97,8 +98,8 @@ class AladdinDevice(CoverEntity):
|
|||||||
"""Schedule a state update."""
|
"""Schedule a state update."""
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
self._acc.register_callback(update_callback, self._number)
|
self._acc.register_callback(update_callback, self._serial)
|
||||||
await self._acc.get_doors(self._number)
|
await self._acc.get_doors(self._serial)
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""Close Aladdin Connect before removing."""
|
"""Close Aladdin Connect before removing."""
|
||||||
@ -114,7 +115,7 @@ class AladdinDevice(CoverEntity):
|
|||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update status of cover."""
|
"""Update status of cover."""
|
||||||
await self._acc.get_doors(self._number)
|
await self._acc.get_doors(self._serial)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closed(self) -> bool | None:
|
def is_closed(self) -> bool | None:
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "aladdin_connect",
|
"domain": "aladdin_connect",
|
||||||
"name": "Aladdin Connect",
|
"name": "Aladdin Connect",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||||
"requirements": ["AIOAladdinConnect==0.1.25"],
|
"requirements": ["AIOAladdinConnect==0.1.27"],
|
||||||
"codeowners": ["@mkmer"],
|
"codeowners": ["@mkmer"],
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aladdin_connect"],
|
"loggers": ["aladdin_connect"],
|
||||||
|
@ -11,3 +11,4 @@ class DoorDevice(TypedDict):
|
|||||||
door_number: int
|
door_number: int
|
||||||
name: str
|
name: str
|
||||||
status: str
|
status: str
|
||||||
|
serial: str
|
||||||
|
@ -281,14 +281,14 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
name="Lightning Strikes Per Day",
|
name="Lightning Strikes Per Day",
|
||||||
icon="mdi:lightning-bolt",
|
icon="mdi:lightning-bolt",
|
||||||
native_unit_of_measurement="strikes",
|
native_unit_of_measurement="strikes",
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
key=TYPE_LIGHTNING_PER_HOUR,
|
key=TYPE_LIGHTNING_PER_HOUR,
|
||||||
name="Lightning Strikes Per Hour",
|
name="Lightning Strikes Per Hour",
|
||||||
icon="mdi:lightning-bolt",
|
icon="mdi:lightning-bolt",
|
||||||
native_unit_of_measurement="strikes",
|
native_unit_of_measurement="strikes",
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
key=TYPE_MAXDAILYGUST,
|
key=TYPE_MAXDAILYGUST,
|
||||||
|
@ -87,7 +87,7 @@ class ArubaDeviceScanner(DeviceScanner):
|
|||||||
def get_aruba_data(self):
|
def get_aruba_data(self):
|
||||||
"""Retrieve data from Aruba Access Point and return parsed result."""
|
"""Retrieve data from Aruba Access Point and return parsed result."""
|
||||||
|
|
||||||
connect = f"ssh {self.username}@{self.host}"
|
connect = f"ssh {self.username}@{self.host} -o HostKeyAlgorithms=ssh-rsa"
|
||||||
ssh = pexpect.spawn(connect)
|
ssh = pexpect.spawn(connect)
|
||||||
query = ssh.expect(
|
query = ssh.expect(
|
||||||
[
|
[
|
||||||
|
@ -33,7 +33,8 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
entry.data[CONF_PASSWORD],
|
entry.data[CONF_PASSWORD],
|
||||||
get_region_from_name(entry.data[CONF_REGION]),
|
get_region_from_name(entry.data[CONF_REGION]),
|
||||||
observer_position=GPSPosition(hass.config.latitude, hass.config.longitude),
|
observer_position=GPSPosition(hass.config.latitude, hass.config.longitude),
|
||||||
use_metric_units=hass.config.units.is_metric,
|
# Force metric system as BMW API apparently only returns metric values now
|
||||||
|
use_metric_units=True,
|
||||||
)
|
)
|
||||||
self.read_only = entry.options[CONF_READ_ONLY]
|
self.read_only = entry.options[CONF_READ_ONLY]
|
||||||
self._entry = entry
|
self._entry = entry
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "bmw_connected_drive",
|
"domain": "bmw_connected_drive",
|
||||||
"name": "BMW Connected Drive",
|
"name": "BMW Connected Drive",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||||
"requirements": ["bimmer_connected==0.9.6"],
|
"requirements": ["bimmer_connected==0.10.1"],
|
||||||
"codeowners": ["@gerard33", "@rikroe"],
|
"codeowners": ["@gerard33", "@rikroe"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
|
@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
|
|||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_UNIT_SYSTEM_IMPERIAL,
|
|
||||||
LENGTH_KILOMETERS,
|
LENGTH_KILOMETERS,
|
||||||
LENGTH_MILES,
|
LENGTH_MILES,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
@ -183,10 +182,8 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
|
|||||||
self._attr_name = f"{vehicle.name} {description.key}"
|
self._attr_name = f"{vehicle.name} {description.key}"
|
||||||
self._attr_unique_id = f"{vehicle.vin}-{description.key}"
|
self._attr_unique_id = f"{vehicle.vin}-{description.key}"
|
||||||
|
|
||||||
if unit_system.name == CONF_UNIT_SYSTEM_IMPERIAL:
|
# Force metric system as BMW API apparently only returns metric values now
|
||||||
self._attr_native_unit_of_measurement = description.unit_imperial
|
self._attr_native_unit_of_measurement = description.unit_metric
|
||||||
else:
|
|
||||||
self._attr_native_unit_of_measurement = description.unit_metric
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
|
@ -99,7 +99,7 @@ class ForecastSolarOptionFlowHandler(OptionsFlow):
|
|||||||
CONF_API_KEY,
|
CONF_API_KEY,
|
||||||
description={
|
description={
|
||||||
"suggested_value": self.config_entry.options.get(
|
"suggested_value": self.config_entry.options.get(
|
||||||
CONF_API_KEY
|
CONF_API_KEY, ""
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
): str,
|
): str,
|
||||||
|
@ -44,13 +44,15 @@ class HiveDeviceLight(HiveEntity, LightEntity):
|
|||||||
super().__init__(hive, hive_device)
|
super().__init__(hive, hive_device)
|
||||||
if self.device["hiveType"] == "warmwhitelight":
|
if self.device["hiveType"] == "warmwhitelight":
|
||||||
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||||
|
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||||
elif self.device["hiveType"] == "tuneablelight":
|
elif self.device["hiveType"] == "tuneablelight":
|
||||||
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP}
|
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP}
|
||||||
|
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||||
elif self.device["hiveType"] == "colourtuneablelight":
|
elif self.device["hiveType"] == "colourtuneablelight":
|
||||||
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
|
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
|
||||||
|
|
||||||
self._attr_min_mireds = self.device.get("min_mireds")
|
self._attr_min_mireds = 153
|
||||||
self._attr_max_mireds = self.device.get("max_mireds")
|
self._attr_max_mireds = 370
|
||||||
|
|
||||||
@refresh_system
|
@refresh_system
|
||||||
async def async_turn_on(self, **kwargs):
|
async def async_turn_on(self, **kwargs):
|
||||||
@ -94,6 +96,13 @@ class HiveDeviceLight(HiveEntity, LightEntity):
|
|||||||
if self._attr_available:
|
if self._attr_available:
|
||||||
self._attr_is_on = self.device["status"]["state"]
|
self._attr_is_on = self.device["status"]["state"]
|
||||||
self._attr_brightness = self.device["status"]["brightness"]
|
self._attr_brightness = self.device["status"]["brightness"]
|
||||||
|
if self.device["hiveType"] == "tuneablelight":
|
||||||
|
self._attr_color_temp = self.device["status"].get("color_temp")
|
||||||
if self.device["hiveType"] == "colourtuneablelight":
|
if self.device["hiveType"] == "colourtuneablelight":
|
||||||
rgb = self.device["status"]["hs_color"]
|
if self.device["status"]["mode"] == "COLOUR":
|
||||||
self._attr_hs_color = color_util.color_RGB_to_hs(*rgb)
|
rgb = self.device["status"]["hs_color"]
|
||||||
|
self._attr_hs_color = color_util.color_RGB_to_hs(*rgb)
|
||||||
|
self._attr_color_mode = ColorMode.HS
|
||||||
|
else:
|
||||||
|
self._attr_color_temp = self.device["status"].get("color_temp")
|
||||||
|
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||||
|
@ -16,7 +16,7 @@ from homeassistant.components.automation import (
|
|||||||
)
|
)
|
||||||
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
||||||
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
|
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS
|
from .const import DOMAIN, KNOWN_DEVICES, TRIGGERS
|
||||||
@ -86,13 +86,13 @@ class TriggerSource:
|
|||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Attach a trigger."""
|
"""Attach a trigger."""
|
||||||
trigger_data = automation_info["trigger_data"]
|
trigger_data = automation_info["trigger_data"]
|
||||||
|
job = HassJob(action)
|
||||||
|
|
||||||
|
@callback
|
||||||
def event_handler(char):
|
def event_handler(char):
|
||||||
if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
|
if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
|
||||||
return
|
return
|
||||||
self._hass.async_create_task(
|
self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}})
|
||||||
action({"trigger": {**trigger_data, **config}})
|
|
||||||
)
|
|
||||||
|
|
||||||
trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]]
|
trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]]
|
||||||
iid = trigger["characteristic"]
|
iid = trigger["characteristic"]
|
||||||
@ -231,11 +231,11 @@ async def async_setup_triggers_for_entry(hass: HomeAssistant, config_entry):
|
|||||||
|
|
||||||
def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]):
|
def async_fire_triggers(conn: HKDevice, events: dict[tuple[int, int], Any]):
|
||||||
"""Process events generated by a HomeKit accessory into automation triggers."""
|
"""Process events generated by a HomeKit accessory into automation triggers."""
|
||||||
|
trigger_sources: dict[str, TriggerSource] = conn.hass.data[TRIGGERS]
|
||||||
for (aid, iid), ev in events.items():
|
for (aid, iid), ev in events.items():
|
||||||
if aid in conn.devices:
|
if aid in conn.devices:
|
||||||
device_id = conn.devices[aid]
|
device_id = conn.devices[aid]
|
||||||
if device_id in conn.hass.data[TRIGGERS]:
|
if source := trigger_sources.get(device_id):
|
||||||
source = conn.hass.data[TRIGGERS][device_id]
|
|
||||||
source.fire(iid, ev)
|
source.fire(iid, ev)
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,7 +99,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
client = Client(
|
client = Client(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
loop=hass.loop,
|
|
||||||
update_interval=scan_interval.total_seconds(),
|
update_interval=scan_interval.total_seconds(),
|
||||||
infer_arming_state=infer_arming_state,
|
infer_arming_state=infer_arming_state,
|
||||||
)
|
)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "ness_alarm",
|
"domain": "ness_alarm",
|
||||||
"name": "Ness Alarm",
|
"name": "Ness Alarm",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ness_alarm",
|
"documentation": "https://www.home-assistant.io/integrations/ness_alarm",
|
||||||
"requirements": ["nessclient==0.9.15"],
|
"requirements": ["nessclient==0.10.0"],
|
||||||
"codeowners": ["@nickw444"],
|
"codeowners": ["@nickw444"],
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["nessclient"]
|
"loggers": ["nessclient"]
|
||||||
|
@ -59,7 +59,9 @@ class NetgearUpdateEntity(NetgearRouterEntity, UpdateEntity):
|
|||||||
"""Latest version available for install."""
|
"""Latest version available for install."""
|
||||||
if self.coordinator.data is not None:
|
if self.coordinator.data is not None:
|
||||||
new_version = self.coordinator.data.get("NewVersion")
|
new_version = self.coordinator.data.get("NewVersion")
|
||||||
if new_version is not None:
|
if new_version is not None and not new_version.startswith(
|
||||||
|
self.installed_version
|
||||||
|
):
|
||||||
return new_version
|
return new_version
|
||||||
return self.installed_version
|
return self.installed_version
|
||||||
|
|
||||||
|
@ -416,7 +416,7 @@ class OpenThermGatewayDevice:
|
|||||||
self.status = {}
|
self.status = {}
|
||||||
self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update"
|
self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update"
|
||||||
self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update"
|
self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update"
|
||||||
self.gateway = pyotgw.pyotgw()
|
self.gateway = pyotgw.OpenThermGateway()
|
||||||
self.gw_version = None
|
self.gw_version = None
|
||||||
|
|
||||||
async def cleanup(self, event=None):
|
async def cleanup(self, event=None):
|
||||||
@ -427,7 +427,7 @@ class OpenThermGatewayDevice:
|
|||||||
|
|
||||||
async def connect_and_subscribe(self):
|
async def connect_and_subscribe(self):
|
||||||
"""Connect to serial device and subscribe report handler."""
|
"""Connect to serial device and subscribe report handler."""
|
||||||
self.status = await self.gateway.connect(self.hass.loop, self.device_path)
|
self.status = await self.gateway.connect(self.device_path)
|
||||||
version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
|
version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
|
||||||
self.gw_version = version_string[18:] if version_string else None
|
self.gw_version = version_string[18:] if version_string else None
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
|
@ -59,8 +59,8 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
async def test_connection():
|
async def test_connection():
|
||||||
"""Try to connect to the OpenTherm Gateway."""
|
"""Try to connect to the OpenTherm Gateway."""
|
||||||
otgw = pyotgw.pyotgw()
|
otgw = pyotgw.OpenThermGateway()
|
||||||
status = await otgw.connect(self.hass.loop, device)
|
status = await otgw.connect(device)
|
||||||
await otgw.disconnect()
|
await otgw.disconnect()
|
||||||
return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
|
return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "opentherm_gw",
|
"domain": "opentherm_gw",
|
||||||
"name": "OpenTherm Gateway",
|
"name": "OpenTherm Gateway",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
|
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
|
||||||
"requirements": ["pyotgw==1.1b1"],
|
"requirements": ["pyotgw==2.0.0"],
|
||||||
"codeowners": ["@mvn23"],
|
"codeowners": ["@mvn23"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Shelly",
|
"name": "Shelly",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
||||||
"requirements": ["aioshelly==2.0.0"],
|
"requirements": ["aioshelly==2.0.1"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_http._tcp.local.",
|
"type": "_http._tcp.local.",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "SimpliSafe",
|
"name": "SimpliSafe",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
|
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
|
||||||
"requirements": ["simplisafe-python==2022.06.1"],
|
"requirements": ["simplisafe-python==2022.07.0"],
|
||||||
"codeowners": ["@bachya"],
|
"codeowners": ["@bachya"],
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"dhcp": [
|
"dhcp": [
|
||||||
|
@ -187,14 +187,15 @@ def filter_libav_logging() -> None:
|
|||||||
return logging.getLogger(__name__).isEnabledFor(logging.DEBUG)
|
return logging.getLogger(__name__).isEnabledFor(logging.DEBUG)
|
||||||
|
|
||||||
for logging_namespace in (
|
for logging_namespace in (
|
||||||
"libav.mp4",
|
"libav.NULL",
|
||||||
"libav.h264",
|
"libav.h264",
|
||||||
"libav.hevc",
|
"libav.hevc",
|
||||||
|
"libav.hls",
|
||||||
|
"libav.mp4",
|
||||||
|
"libav.mpegts",
|
||||||
"libav.rtsp",
|
"libav.rtsp",
|
||||||
"libav.tcp",
|
"libav.tcp",
|
||||||
"libav.tls",
|
"libav.tls",
|
||||||
"libav.mpegts",
|
|
||||||
"libav.NULL",
|
|
||||||
):
|
):
|
||||||
logging.getLogger(logging_namespace).addFilter(libav_filter)
|
logging.getLogger(logging_namespace).addFilter(libav_filter)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"domain": "switchbot",
|
"domain": "switchbot",
|
||||||
"name": "SwitchBot",
|
"name": "SwitchBot",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||||
"requirements": ["PySwitchbot==0.14.0"],
|
"requirements": ["PySwitchbot==0.14.1"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"codeowners": ["@danielhiversen", "@RenierM26"],
|
"codeowners": ["@danielhiversen", "@RenierM26"],
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Tomorrow.io",
|
"name": "Tomorrow.io",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/tomorrowio",
|
"documentation": "https://www.home-assistant.io/integrations/tomorrowio",
|
||||||
"requirements": ["pytomorrowio==0.3.3"],
|
"requirements": ["pytomorrowio==0.3.4"],
|
||||||
"codeowners": ["@raman325", "@lymanepp"],
|
"codeowners": ["@raman325", "@lymanepp"],
|
||||||
"iot_class": "cloud_polling"
|
"iot_class": "cloud_polling"
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,7 @@ class ProtectData:
|
|||||||
self._pending_camera_ids: set[str] = set()
|
self._pending_camera_ids: set[str] = set()
|
||||||
self._unsub_interval: CALLBACK_TYPE | None = None
|
self._unsub_interval: CALLBACK_TYPE | None = None
|
||||||
self._unsub_websocket: CALLBACK_TYPE | None = None
|
self._unsub_websocket: CALLBACK_TYPE | None = None
|
||||||
|
self._auth_failures = 0
|
||||||
|
|
||||||
self.last_update_success = False
|
self.last_update_success = False
|
||||||
self.api = protect
|
self.api = protect
|
||||||
@ -117,9 +118,13 @@ class ProtectData:
|
|||||||
try:
|
try:
|
||||||
updates = await self.api.update(force=force)
|
updates = await self.api.update(force=force)
|
||||||
except NotAuthorized:
|
except NotAuthorized:
|
||||||
await self.async_stop()
|
if self._auth_failures < 10:
|
||||||
_LOGGER.exception("Reauthentication required")
|
_LOGGER.exception("Auth error while updating")
|
||||||
self._entry.async_start_reauth(self._hass)
|
self._auth_failures += 1
|
||||||
|
else:
|
||||||
|
await self.async_stop()
|
||||||
|
_LOGGER.exception("Reauthentication required")
|
||||||
|
self._entry.async_start_reauth(self._hass)
|
||||||
self.last_update_success = False
|
self.last_update_success = False
|
||||||
except ClientError:
|
except ClientError:
|
||||||
if self.last_update_success:
|
if self.last_update_success:
|
||||||
@ -129,6 +134,7 @@ class ProtectData:
|
|||||||
self._async_process_updates(self.api.bootstrap)
|
self._async_process_updates(self.api.bootstrap)
|
||||||
else:
|
else:
|
||||||
self.last_update_success = True
|
self.last_update_success = True
|
||||||
|
self._auth_failures = 0
|
||||||
self._async_process_updates(updates)
|
self._async_process_updates(updates)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"name": "Venstar",
|
"name": "Venstar",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/venstar",
|
"documentation": "https://www.home-assistant.io/integrations/venstar",
|
||||||
"requirements": ["venstarcolortouch==0.17"],
|
"requirements": ["venstarcolortouch==0.18"],
|
||||||
"codeowners": ["@garbled1"],
|
"codeowners": ["@garbled1"],
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["venstarcolortouch"]
|
"loggers": ["venstarcolortouch"]
|
||||||
|
@ -73,7 +73,6 @@ CAPABILITIES_COLOR_LOOP = 0x4
|
|||||||
CAPABILITIES_COLOR_XY = 0x08
|
CAPABILITIES_COLOR_XY = 0x08
|
||||||
CAPABILITIES_COLOR_TEMP = 0x10
|
CAPABILITIES_COLOR_TEMP = 0x10
|
||||||
|
|
||||||
DEFAULT_TRANSITION = 1
|
|
||||||
DEFAULT_MIN_BRIGHTNESS = 2
|
DEFAULT_MIN_BRIGHTNESS = 2
|
||||||
|
|
||||||
UPDATE_COLORLOOP_ACTION = 0x1
|
UPDATE_COLORLOOP_ACTION = 0x1
|
||||||
@ -119,7 +118,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
"""Operations common to all light entities."""
|
"""Operations common to all light entities."""
|
||||||
|
|
||||||
_FORCE_ON = False
|
_FORCE_ON = False
|
||||||
_DEFAULT_COLOR_FROM_OFF_TRANSITION = 0
|
_DEFAULT_MIN_TRANSITION_TIME = 0
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialize the light."""
|
"""Initialize the light."""
|
||||||
@ -140,7 +139,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
self._level_channel = None
|
self._level_channel = None
|
||||||
self._color_channel = None
|
self._color_channel = None
|
||||||
self._identify_channel = None
|
self._identify_channel = None
|
||||||
self._default_transition = None
|
self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME
|
||||||
self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes
|
self._attr_color_mode = ColorMode.UNKNOWN # Set by sub classes
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -216,33 +215,49 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
transition = kwargs.get(light.ATTR_TRANSITION)
|
transition = kwargs.get(light.ATTR_TRANSITION)
|
||||||
duration = (
|
duration = (
|
||||||
transition * 10
|
transition * 10
|
||||||
if transition
|
if transition is not None
|
||||||
else self._default_transition * 10
|
else self._zha_config_transition * 10
|
||||||
if self._default_transition
|
) or self._DEFAULT_MIN_TRANSITION_TIME # if 0 is passed in some devices still need the minimum default
|
||||||
else DEFAULT_TRANSITION
|
|
||||||
)
|
|
||||||
brightness = kwargs.get(light.ATTR_BRIGHTNESS)
|
brightness = kwargs.get(light.ATTR_BRIGHTNESS)
|
||||||
effect = kwargs.get(light.ATTR_EFFECT)
|
effect = kwargs.get(light.ATTR_EFFECT)
|
||||||
flash = kwargs.get(light.ATTR_FLASH)
|
flash = kwargs.get(light.ATTR_FLASH)
|
||||||
|
temperature = kwargs.get(light.ATTR_COLOR_TEMP)
|
||||||
|
hs_color = kwargs.get(light.ATTR_HS_COLOR)
|
||||||
|
|
||||||
# If the light is currently off but a turn_on call with a color/temperature is sent,
|
# If the light is currently off but a turn_on call with a color/temperature is sent,
|
||||||
# the light needs to be turned on first at a low brightness level where the light is immediately transitioned
|
# the light needs to be turned on first at a low brightness level where the light is immediately transitioned
|
||||||
# to the correct color. Afterwards, the transition is only from the low brightness to the new brightness.
|
# to the correct color. Afterwards, the transition is only from the low brightness to the new brightness.
|
||||||
# Otherwise, the transition is from the color the light had before being turned on to the new color.
|
# Otherwise, the transition is from the color the light had before being turned on to the new color.
|
||||||
# This can look especially bad with transitions longer than a second.
|
# This can look especially bad with transitions longer than a second. We do not want to do this for
|
||||||
color_provided_from_off = (
|
# devices that need to be forced to use the on command because we would end up with 4 commands sent:
|
||||||
not self._state
|
# move to level, on, color, move to level... We also will not set this if the bulb is already in the
|
||||||
|
# desired color mode with the desired color or color temperature.
|
||||||
|
new_color_provided_while_off = (
|
||||||
|
not isinstance(self, LightGroup)
|
||||||
|
and not self._FORCE_ON
|
||||||
|
and not self._state
|
||||||
|
and (
|
||||||
|
(
|
||||||
|
temperature is not None
|
||||||
|
and (
|
||||||
|
self._color_temp != temperature
|
||||||
|
or self._attr_color_mode != ColorMode.COLOR_TEMP
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
hs_color is not None
|
||||||
|
and (
|
||||||
|
self.hs_color != hs_color
|
||||||
|
or self._attr_color_mode != ColorMode.HS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
and brightness_supported(self._attr_supported_color_modes)
|
and brightness_supported(self._attr_supported_color_modes)
|
||||||
and (light.ATTR_COLOR_TEMP in kwargs or light.ATTR_HS_COLOR in kwargs)
|
|
||||||
)
|
)
|
||||||
final_duration = duration
|
|
||||||
if color_provided_from_off:
|
|
||||||
# Set the duration for the color changing commands to 0.
|
|
||||||
duration = 0
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
brightness is None
|
brightness is None
|
||||||
and (self._off_with_transition or color_provided_from_off)
|
and (self._off_with_transition or new_color_provided_while_off)
|
||||||
and self._off_brightness is not None
|
and self._off_brightness is not None
|
||||||
):
|
):
|
||||||
brightness = self._off_brightness
|
brightness = self._off_brightness
|
||||||
@ -254,11 +269,11 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
|
|
||||||
t_log = {}
|
t_log = {}
|
||||||
|
|
||||||
if color_provided_from_off:
|
if new_color_provided_while_off:
|
||||||
# If the light is currently off, we first need to turn it on at a low brightness level with no transition.
|
# If the light is currently off, we first need to turn it on at a low brightness level with no transition.
|
||||||
# After that, we set it to the desired color/temperature with no transition.
|
# After that, we set it to the desired color/temperature with no transition.
|
||||||
result = await self._level_channel.move_to_level_with_on_off(
|
result = await self._level_channel.move_to_level_with_on_off(
|
||||||
DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_COLOR_FROM_OFF_TRANSITION
|
DEFAULT_MIN_BRIGHTNESS, self._DEFAULT_MIN_TRANSITION_TIME
|
||||||
)
|
)
|
||||||
t_log["move_to_level_with_on_off"] = result
|
t_log["move_to_level_with_on_off"] = result
|
||||||
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
||||||
@ -269,7 +284,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
(brightness is not None or transition)
|
(brightness is not None or transition)
|
||||||
and not color_provided_from_off
|
and not new_color_provided_while_off
|
||||||
and brightness_supported(self._attr_supported_color_modes)
|
and brightness_supported(self._attr_supported_color_modes)
|
||||||
):
|
):
|
||||||
result = await self._level_channel.move_to_level_with_on_off(
|
result = await self._level_channel.move_to_level_with_on_off(
|
||||||
@ -285,7 +300,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
brightness is None
|
brightness is None
|
||||||
and not color_provided_from_off
|
and not new_color_provided_while_off
|
||||||
or (self._FORCE_ON and brightness)
|
or (self._FORCE_ON and brightness)
|
||||||
):
|
):
|
||||||
# since some lights don't always turn on with move_to_level_with_on_off,
|
# since some lights don't always turn on with move_to_level_with_on_off,
|
||||||
@ -297,9 +312,13 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
return
|
return
|
||||||
self._state = True
|
self._state = True
|
||||||
|
|
||||||
if light.ATTR_COLOR_TEMP in kwargs:
|
if temperature is not None:
|
||||||
temperature = kwargs[light.ATTR_COLOR_TEMP]
|
result = await self._color_channel.move_to_color_temp(
|
||||||
result = await self._color_channel.move_to_color_temp(temperature, duration)
|
temperature,
|
||||||
|
self._DEFAULT_MIN_TRANSITION_TIME
|
||||||
|
if new_color_provided_while_off
|
||||||
|
else duration,
|
||||||
|
)
|
||||||
t_log["move_to_color_temp"] = result
|
t_log["move_to_color_temp"] = result
|
||||||
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
||||||
self.debug("turned on: %s", t_log)
|
self.debug("turned on: %s", t_log)
|
||||||
@ -308,11 +327,14 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
self._color_temp = temperature
|
self._color_temp = temperature
|
||||||
self._hs_color = None
|
self._hs_color = None
|
||||||
|
|
||||||
if light.ATTR_HS_COLOR in kwargs:
|
if hs_color is not None:
|
||||||
hs_color = kwargs[light.ATTR_HS_COLOR]
|
|
||||||
xy_color = color_util.color_hs_to_xy(*hs_color)
|
xy_color = color_util.color_hs_to_xy(*hs_color)
|
||||||
result = await self._color_channel.move_to_color(
|
result = await self._color_channel.move_to_color(
|
||||||
int(xy_color[0] * 65535), int(xy_color[1] * 65535), duration
|
int(xy_color[0] * 65535),
|
||||||
|
int(xy_color[1] * 65535),
|
||||||
|
self._DEFAULT_MIN_TRANSITION_TIME
|
||||||
|
if new_color_provided_while_off
|
||||||
|
else duration,
|
||||||
)
|
)
|
||||||
t_log["move_to_color"] = result
|
t_log["move_to_color"] = result
|
||||||
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
||||||
@ -322,9 +344,9 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
self._hs_color = hs_color
|
self._hs_color = hs_color
|
||||||
self._color_temp = None
|
self._color_temp = None
|
||||||
|
|
||||||
if color_provided_from_off:
|
if new_color_provided_while_off:
|
||||||
# The light is has the correct color, so we can now transition it to the correct brightness level.
|
# The light is has the correct color, so we can now transition it to the correct brightness level.
|
||||||
result = await self._level_channel.move_to_level(level, final_duration)
|
result = await self._level_channel.move_to_level(level, duration)
|
||||||
t_log["move_to_level_if_color"] = result
|
t_log["move_to_level_if_color"] = result
|
||||||
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
||||||
self.debug("turned on: %s", t_log)
|
self.debug("turned on: %s", t_log)
|
||||||
@ -371,12 +393,13 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
|
|
||||||
async def async_turn_off(self, **kwargs):
|
async def async_turn_off(self, **kwargs):
|
||||||
"""Turn the entity off."""
|
"""Turn the entity off."""
|
||||||
duration = kwargs.get(light.ATTR_TRANSITION)
|
transition = kwargs.get(light.ATTR_TRANSITION)
|
||||||
supports_level = brightness_supported(self._attr_supported_color_modes)
|
supports_level = brightness_supported(self._attr_supported_color_modes)
|
||||||
|
|
||||||
if duration and supports_level:
|
# is not none looks odd here but it will override built in bulb transition times if we pass 0 in here
|
||||||
|
if transition is not None and supports_level:
|
||||||
result = await self._level_channel.move_to_level_with_on_off(
|
result = await self._level_channel.move_to_level_with_on_off(
|
||||||
0, duration * 10
|
0, transition * 10
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = await self._on_off_channel.off()
|
result = await self._on_off_channel.off()
|
||||||
@ -387,7 +410,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
|||||||
|
|
||||||
if supports_level:
|
if supports_level:
|
||||||
# store current brightness so that the next turn_on uses it.
|
# store current brightness so that the next turn_on uses it.
|
||||||
self._off_with_transition = bool(duration)
|
self._off_with_transition = transition is not None
|
||||||
self._off_brightness = self._brightness
|
self._off_brightness = self._brightness
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
@ -460,7 +483,7 @@ class Light(BaseLight, ZhaEntity):
|
|||||||
if effect_list:
|
if effect_list:
|
||||||
self._effect_list = effect_list
|
self._effect_list = effect_list
|
||||||
|
|
||||||
self._default_transition = async_get_zha_config_value(
|
self._zha_config_transition = async_get_zha_config_value(
|
||||||
zha_device.gateway.config_entry,
|
zha_device.gateway.config_entry,
|
||||||
ZHA_OPTIONS,
|
ZHA_OPTIONS,
|
||||||
CONF_DEFAULT_LIGHT_TRANSITION,
|
CONF_DEFAULT_LIGHT_TRANSITION,
|
||||||
@ -472,6 +495,7 @@ class Light(BaseLight, ZhaEntity):
|
|||||||
"""Set the state."""
|
"""Set the state."""
|
||||||
self._state = bool(value)
|
self._state = bool(value)
|
||||||
if value:
|
if value:
|
||||||
|
self._off_with_transition = False
|
||||||
self._off_brightness = None
|
self._off_brightness = None
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@ -605,7 +629,7 @@ class HueLight(Light):
|
|||||||
@STRICT_MATCH(
|
@STRICT_MATCH(
|
||||||
channel_names=CHANNEL_ON_OFF,
|
channel_names=CHANNEL_ON_OFF,
|
||||||
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
|
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
|
||||||
manufacturers={"Jasco", "Quotra-Vision"},
|
manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"},
|
||||||
)
|
)
|
||||||
class ForceOnLight(Light):
|
class ForceOnLight(Light):
|
||||||
"""Representation of a light which does not respect move_to_level_with_on_off."""
|
"""Representation of a light which does not respect move_to_level_with_on_off."""
|
||||||
@ -621,7 +645,7 @@ class ForceOnLight(Light):
|
|||||||
class SengledLight(Light):
|
class SengledLight(Light):
|
||||||
"""Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition."""
|
"""Representation of a Sengled light which does not react to move_to_color_temp with 0 as a transition."""
|
||||||
|
|
||||||
_DEFAULT_COLOR_FROM_OFF_TRANSITION = 1
|
_DEFAULT_MIN_TRANSITION_TIME = 1
|
||||||
|
|
||||||
|
|
||||||
@GROUP_MATCH()
|
@GROUP_MATCH()
|
||||||
@ -639,7 +663,7 @@ class LightGroup(BaseLight, ZhaGroupEntity):
|
|||||||
self._color_channel = group.endpoint[Color.cluster_id]
|
self._color_channel = group.endpoint[Color.cluster_id]
|
||||||
self._identify_channel = group.endpoint[Identify.cluster_id]
|
self._identify_channel = group.endpoint[Identify.cluster_id]
|
||||||
self._debounced_member_refresh = None
|
self._debounced_member_refresh = None
|
||||||
self._default_transition = async_get_zha_config_value(
|
self._zha_config_transition = async_get_zha_config_value(
|
||||||
zha_device.gateway.config_entry,
|
zha_device.gateway.config_entry,
|
||||||
ZHA_OPTIONS,
|
ZHA_OPTIONS,
|
||||||
CONF_DEFAULT_LIGHT_TRANSITION,
|
CONF_DEFAULT_LIGHT_TRANSITION,
|
||||||
|
@ -7,7 +7,7 @@ from .backports.enum import StrEnum
|
|||||||
|
|
||||||
MAJOR_VERSION: Final = 2022
|
MAJOR_VERSION: Final = 2022
|
||||||
MINOR_VERSION: Final = 7
|
MINOR_VERSION: Final = 7
|
||||||
PATCH_VERSION: Final = "5"
|
PATCH_VERSION: Final = "6"
|
||||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||||
|
@ -11,6 +11,10 @@ import orjson
|
|||||||
|
|
||||||
from homeassistant.core import Event, State
|
from homeassistant.core import Event, State
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.json import (
|
||||||
|
JSONEncoder as DefaultHASSJSONEncoder,
|
||||||
|
json_encoder_default as default_hass_orjson_encoder,
|
||||||
|
)
|
||||||
|
|
||||||
from .file import write_utf8_file, write_utf8_file_atomic
|
from .file import write_utf8_file, write_utf8_file_atomic
|
||||||
|
|
||||||
@ -45,10 +49,12 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict:
|
|||||||
return {} if default is None else default
|
return {} if default is None else default
|
||||||
|
|
||||||
|
|
||||||
def _orjson_encoder(data: Any) -> str:
|
def _orjson_default_encoder(data: Any) -> str:
|
||||||
"""JSON encoder that uses orjson."""
|
"""JSON encoder that uses orjson with hass defaults."""
|
||||||
return orjson.dumps(
|
return orjson.dumps(
|
||||||
data, option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS
|
data,
|
||||||
|
option=orjson.OPT_INDENT_2 | orjson.OPT_NON_STR_KEYS,
|
||||||
|
default=default_hass_orjson_encoder,
|
||||||
).decode("utf-8")
|
).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
@ -64,13 +70,19 @@ def save_json(
|
|||||||
|
|
||||||
Returns True on success.
|
Returns True on success.
|
||||||
"""
|
"""
|
||||||
dump: Callable[[Any], Any] = json.dumps
|
dump: Callable[[Any], Any]
|
||||||
try:
|
try:
|
||||||
if encoder:
|
# For backwards compatibility, if they pass in the
|
||||||
|
# default json encoder we use _orjson_default_encoder
|
||||||
|
# which is the orjson equivalent to the default encoder.
|
||||||
|
if encoder and encoder is not DefaultHASSJSONEncoder:
|
||||||
|
# If they pass a custom encoder that is not the
|
||||||
|
# DefaultHASSJSONEncoder, we use the slow path of json.dumps
|
||||||
|
dump = json.dumps
|
||||||
json_data = json.dumps(data, indent=2, cls=encoder)
|
json_data = json.dumps(data, indent=2, cls=encoder)
|
||||||
else:
|
else:
|
||||||
dump = _orjson_encoder
|
dump = _orjson_default_encoder
|
||||||
json_data = _orjson_encoder(data)
|
json_data = _orjson_default_encoder(data)
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data, dump=dump))}"
|
msg = f"Failed to serialize to JSON: {filename}. Bad data at {format_unserializable_data(find_paths_unserializable_data(data, dump=dump))}"
|
||||||
_LOGGER.error(msg)
|
_LOGGER.error(msg)
|
||||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
import fnmatch
|
import fnmatch
|
||||||
from io import StringIO
|
from io import StringIO, TextIOWrapper
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -169,7 +169,7 @@ def parse_yaml(
|
|||||||
except yaml.YAMLError:
|
except yaml.YAMLError:
|
||||||
# Loading failed, so we now load with the slow line loader
|
# Loading failed, so we now load with the slow line loader
|
||||||
# since the C one will not give us line numbers
|
# since the C one will not give us line numbers
|
||||||
if isinstance(content, (StringIO, TextIO)):
|
if isinstance(content, (StringIO, TextIO, TextIOWrapper)):
|
||||||
# Rewind the stream so we can try again
|
# Rewind the stream so we can try again
|
||||||
content.seek(0, 0)
|
content.seek(0, 0)
|
||||||
return _parse_yaml_pure_python(content, secrets)
|
return _parse_yaml_pure_python(content, secrets)
|
||||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "homeassistant"
|
name = "homeassistant"
|
||||||
version = "2022.7.5"
|
version = "2022.7.6"
|
||||||
license = {text = "Apache-2.0"}
|
license = {text = "Apache-2.0"}
|
||||||
description = "Open-source home automation platform running on Python 3."
|
description = "Open-source home automation platform running on Python 3."
|
||||||
readme = "README.rst"
|
readme = "README.rst"
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
AEMET-OpenData==0.2.1
|
AEMET-OpenData==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.aladdin_connect
|
# homeassistant.components.aladdin_connect
|
||||||
AIOAladdinConnect==0.1.25
|
AIOAladdinConnect==0.1.27
|
||||||
|
|
||||||
# homeassistant.components.adax
|
# homeassistant.components.adax
|
||||||
Adax-local==0.1.4
|
Adax-local==0.1.4
|
||||||
@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
|
|||||||
PySocks==1.7.1
|
PySocks==1.7.1
|
||||||
|
|
||||||
# homeassistant.components.switchbot
|
# homeassistant.components.switchbot
|
||||||
PySwitchbot==0.14.0
|
PySwitchbot==0.14.1
|
||||||
|
|
||||||
# homeassistant.components.transport_nsw
|
# homeassistant.components.transport_nsw
|
||||||
PyTransportNSW==0.1.1
|
PyTransportNSW==0.1.1
|
||||||
@ -244,7 +244,7 @@ aiosenseme==0.6.1
|
|||||||
aiosenz==1.0.0
|
aiosenz==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.shelly
|
# homeassistant.components.shelly
|
||||||
aioshelly==2.0.0
|
aioshelly==2.0.1
|
||||||
|
|
||||||
# homeassistant.components.skybell
|
# homeassistant.components.skybell
|
||||||
aioskybell==22.7.0
|
aioskybell==22.7.0
|
||||||
@ -396,7 +396,7 @@ beautifulsoup4==4.11.1
|
|||||||
bellows==0.31.1
|
bellows==0.31.1
|
||||||
|
|
||||||
# homeassistant.components.bmw_connected_drive
|
# homeassistant.components.bmw_connected_drive
|
||||||
bimmer_connected==0.9.6
|
bimmer_connected==0.10.1
|
||||||
|
|
||||||
# homeassistant.components.bizkaibus
|
# homeassistant.components.bizkaibus
|
||||||
bizkaibus==0.1.1
|
bizkaibus==0.1.1
|
||||||
@ -1068,7 +1068,7 @@ nad_receiver==0.3.0
|
|||||||
ndms2_client==0.1.1
|
ndms2_client==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.ness_alarm
|
# homeassistant.components.ness_alarm
|
||||||
nessclient==0.9.15
|
nessclient==0.10.0
|
||||||
|
|
||||||
# homeassistant.components.netdata
|
# homeassistant.components.netdata
|
||||||
netdata==1.0.1
|
netdata==1.0.1
|
||||||
@ -1366,7 +1366,7 @@ pyaftership==21.11.0
|
|||||||
pyairnow==1.1.0
|
pyairnow==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.airvisual
|
# homeassistant.components.airvisual
|
||||||
pyairvisual==5.0.9
|
pyairvisual==2022.07.0
|
||||||
|
|
||||||
# homeassistant.components.almond
|
# homeassistant.components.almond
|
||||||
pyalmond==0.0.2
|
pyalmond==0.0.2
|
||||||
@ -1715,7 +1715,7 @@ pyopnsense==0.2.0
|
|||||||
pyoppleio==1.0.5
|
pyoppleio==1.0.5
|
||||||
|
|
||||||
# homeassistant.components.opentherm_gw
|
# homeassistant.components.opentherm_gw
|
||||||
pyotgw==1.1b1
|
pyotgw==2.0.0
|
||||||
|
|
||||||
# homeassistant.auth.mfa_modules.notify
|
# homeassistant.auth.mfa_modules.notify
|
||||||
# homeassistant.auth.mfa_modules.totp
|
# homeassistant.auth.mfa_modules.totp
|
||||||
@ -1973,7 +1973,7 @@ pythonegardia==1.0.40
|
|||||||
pytile==2022.02.0
|
pytile==2022.02.0
|
||||||
|
|
||||||
# homeassistant.components.tomorrowio
|
# homeassistant.components.tomorrowio
|
||||||
pytomorrowio==0.3.3
|
pytomorrowio==0.3.4
|
||||||
|
|
||||||
# homeassistant.components.touchline
|
# homeassistant.components.touchline
|
||||||
pytouchline==0.7
|
pytouchline==0.7
|
||||||
@ -2168,7 +2168,7 @@ simplehound==0.3
|
|||||||
simplepush==1.1.4
|
simplepush==1.1.4
|
||||||
|
|
||||||
# homeassistant.components.simplisafe
|
# homeassistant.components.simplisafe
|
||||||
simplisafe-python==2022.06.1
|
simplisafe-python==2022.07.0
|
||||||
|
|
||||||
# homeassistant.components.sisyphus
|
# homeassistant.components.sisyphus
|
||||||
sisyphus-control==3.1.2
|
sisyphus-control==3.1.2
|
||||||
@ -2387,7 +2387,7 @@ vehicle==0.4.0
|
|||||||
velbus-aio==2022.6.2
|
velbus-aio==2022.6.2
|
||||||
|
|
||||||
# homeassistant.components.venstar
|
# homeassistant.components.venstar
|
||||||
venstarcolortouch==0.17
|
venstarcolortouch==0.18
|
||||||
|
|
||||||
# homeassistant.components.vilfo
|
# homeassistant.components.vilfo
|
||||||
vilfo-api-client==0.3.2
|
vilfo-api-client==0.3.2
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
AEMET-OpenData==0.2.1
|
AEMET-OpenData==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.aladdin_connect
|
# homeassistant.components.aladdin_connect
|
||||||
AIOAladdinConnect==0.1.25
|
AIOAladdinConnect==0.1.27
|
||||||
|
|
||||||
# homeassistant.components.adax
|
# homeassistant.components.adax
|
||||||
Adax-local==0.1.4
|
Adax-local==0.1.4
|
||||||
@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
|
|||||||
PySocks==1.7.1
|
PySocks==1.7.1
|
||||||
|
|
||||||
# homeassistant.components.switchbot
|
# homeassistant.components.switchbot
|
||||||
PySwitchbot==0.14.0
|
PySwitchbot==0.14.1
|
||||||
|
|
||||||
# homeassistant.components.transport_nsw
|
# homeassistant.components.transport_nsw
|
||||||
PyTransportNSW==0.1.1
|
PyTransportNSW==0.1.1
|
||||||
@ -213,7 +213,7 @@ aiosenseme==0.6.1
|
|||||||
aiosenz==1.0.0
|
aiosenz==1.0.0
|
||||||
|
|
||||||
# homeassistant.components.shelly
|
# homeassistant.components.shelly
|
||||||
aioshelly==2.0.0
|
aioshelly==2.0.1
|
||||||
|
|
||||||
# homeassistant.components.skybell
|
# homeassistant.components.skybell
|
||||||
aioskybell==22.7.0
|
aioskybell==22.7.0
|
||||||
@ -311,7 +311,7 @@ beautifulsoup4==4.11.1
|
|||||||
bellows==0.31.1
|
bellows==0.31.1
|
||||||
|
|
||||||
# homeassistant.components.bmw_connected_drive
|
# homeassistant.components.bmw_connected_drive
|
||||||
bimmer_connected==0.9.6
|
bimmer_connected==0.10.1
|
||||||
|
|
||||||
# homeassistant.components.blebox
|
# homeassistant.components.blebox
|
||||||
blebox_uniapi==2.0.1
|
blebox_uniapi==2.0.1
|
||||||
@ -742,7 +742,7 @@ mutesync==0.0.1
|
|||||||
ndms2_client==0.1.1
|
ndms2_client==0.1.1
|
||||||
|
|
||||||
# homeassistant.components.ness_alarm
|
# homeassistant.components.ness_alarm
|
||||||
nessclient==0.9.15
|
nessclient==0.10.0
|
||||||
|
|
||||||
# homeassistant.components.discovery
|
# homeassistant.components.discovery
|
||||||
netdisco==3.0.0
|
netdisco==3.0.0
|
||||||
@ -929,7 +929,7 @@ pyaehw4a1==0.3.9
|
|||||||
pyairnow==1.1.0
|
pyairnow==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.airvisual
|
# homeassistant.components.airvisual
|
||||||
pyairvisual==5.0.9
|
pyairvisual==2022.07.0
|
||||||
|
|
||||||
# homeassistant.components.almond
|
# homeassistant.components.almond
|
||||||
pyalmond==0.0.2
|
pyalmond==0.0.2
|
||||||
@ -1167,7 +1167,7 @@ pyopenuv==2022.04.0
|
|||||||
pyopnsense==0.2.0
|
pyopnsense==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.opentherm_gw
|
# homeassistant.components.opentherm_gw
|
||||||
pyotgw==1.1b1
|
pyotgw==2.0.0
|
||||||
|
|
||||||
# homeassistant.auth.mfa_modules.notify
|
# homeassistant.auth.mfa_modules.notify
|
||||||
# homeassistant.auth.mfa_modules.totp
|
# homeassistant.auth.mfa_modules.totp
|
||||||
@ -1314,7 +1314,7 @@ python_awair==0.2.3
|
|||||||
pytile==2022.02.0
|
pytile==2022.02.0
|
||||||
|
|
||||||
# homeassistant.components.tomorrowio
|
# homeassistant.components.tomorrowio
|
||||||
pytomorrowio==0.3.3
|
pytomorrowio==0.3.4
|
||||||
|
|
||||||
# homeassistant.components.traccar
|
# homeassistant.components.traccar
|
||||||
pytraccar==0.10.0
|
pytraccar==0.10.0
|
||||||
@ -1440,7 +1440,7 @@ simplehound==0.3
|
|||||||
simplepush==1.1.4
|
simplepush==1.1.4
|
||||||
|
|
||||||
# homeassistant.components.simplisafe
|
# homeassistant.components.simplisafe
|
||||||
simplisafe-python==2022.06.1
|
simplisafe-python==2022.07.0
|
||||||
|
|
||||||
# homeassistant.components.slack
|
# homeassistant.components.slack
|
||||||
slackclient==2.5.0
|
slackclient==2.5.0
|
||||||
@ -1590,7 +1590,7 @@ vehicle==0.4.0
|
|||||||
velbus-aio==2022.6.2
|
velbus-aio==2022.6.2
|
||||||
|
|
||||||
# homeassistant.components.venstar
|
# homeassistant.components.venstar
|
||||||
venstarcolortouch==0.17
|
venstarcolortouch==0.18
|
||||||
|
|
||||||
# homeassistant.components.vilfo
|
# homeassistant.components.vilfo
|
||||||
vilfo-api-client==0.3.2
|
vilfo-api-client==0.3.2
|
||||||
|
@ -4,8 +4,10 @@ from unittest.mock import patch
|
|||||||
from pyairvisual.errors import (
|
from pyairvisual.errors import (
|
||||||
AirVisualError,
|
AirVisualError,
|
||||||
InvalidKeyError,
|
InvalidKeyError,
|
||||||
|
KeyExpiredError,
|
||||||
NodeProError,
|
NodeProError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
|
UnauthorizedError,
|
||||||
)
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -84,6 +86,28 @@ async def test_duplicate_error(hass, config, config_entry, data):
|
|||||||
{CONF_API_KEY: "invalid_api_key"},
|
{CONF_API_KEY: "invalid_api_key"},
|
||||||
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_CITY: "Beijing",
|
||||||
|
CONF_STATE: "Beijing",
|
||||||
|
CONF_COUNTRY: "China",
|
||||||
|
},
|
||||||
|
KeyExpiredError,
|
||||||
|
{CONF_API_KEY: "invalid_api_key"},
|
||||||
|
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_API_KEY: "abcde12345",
|
||||||
|
CONF_CITY: "Beijing",
|
||||||
|
CONF_STATE: "Beijing",
|
||||||
|
CONF_COUNTRY: "China",
|
||||||
|
},
|
||||||
|
UnauthorizedError,
|
||||||
|
{CONF_API_KEY: "invalid_api_key"},
|
||||||
|
INTEGRATION_TYPE_GEOGRAPHY_NAME,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
CONF_API_KEY: "abcde12345",
|
CONF_API_KEY: "abcde12345",
|
||||||
|
@ -10,6 +10,7 @@ DEVICE_CONFIG_OPEN = {
|
|||||||
"name": "home",
|
"name": "home",
|
||||||
"status": "open",
|
"status": "open",
|
||||||
"link_status": "Connected",
|
"link_status": "Connected",
|
||||||
|
"serial": "12345",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ DEVICE_CONFIG_OPEN = {
|
|||||||
"name": "home",
|
"name": "home",
|
||||||
"status": "open",
|
"status": "open",
|
||||||
"link_status": "Connected",
|
"link_status": "Connected",
|
||||||
|
"serial": "12345",
|
||||||
}
|
}
|
||||||
|
|
||||||
DEVICE_CONFIG_OPENING = {
|
DEVICE_CONFIG_OPENING = {
|
||||||
@ -41,6 +42,7 @@ DEVICE_CONFIG_OPENING = {
|
|||||||
"name": "home",
|
"name": "home",
|
||||||
"status": "opening",
|
"status": "opening",
|
||||||
"link_status": "Connected",
|
"link_status": "Connected",
|
||||||
|
"serial": "12345",
|
||||||
}
|
}
|
||||||
|
|
||||||
DEVICE_CONFIG_CLOSED = {
|
DEVICE_CONFIG_CLOSED = {
|
||||||
@ -49,6 +51,7 @@ DEVICE_CONFIG_CLOSED = {
|
|||||||
"name": "home",
|
"name": "home",
|
||||||
"status": "closed",
|
"status": "closed",
|
||||||
"link_status": "Connected",
|
"link_status": "Connected",
|
||||||
|
"serial": "12345",
|
||||||
}
|
}
|
||||||
|
|
||||||
DEVICE_CONFIG_CLOSING = {
|
DEVICE_CONFIG_CLOSING = {
|
||||||
@ -57,6 +60,7 @@ DEVICE_CONFIG_CLOSING = {
|
|||||||
"name": "home",
|
"name": "home",
|
||||||
"status": "closing",
|
"status": "closing",
|
||||||
"link_status": "Connected",
|
"link_status": "Connected",
|
||||||
|
"serial": "12345",
|
||||||
}
|
}
|
||||||
|
|
||||||
DEVICE_CONFIG_DISCONNECTED = {
|
DEVICE_CONFIG_DISCONNECTED = {
|
||||||
@ -65,6 +69,7 @@ DEVICE_CONFIG_DISCONNECTED = {
|
|||||||
"name": "home",
|
"name": "home",
|
||||||
"status": "open",
|
"status": "open",
|
||||||
"link_status": "Disconnected",
|
"link_status": "Disconnected",
|
||||||
|
"serial": "12345",
|
||||||
}
|
}
|
||||||
|
|
||||||
DEVICE_CONFIG_BAD = {
|
DEVICE_CONFIG_BAD = {
|
||||||
|
@ -43,10 +43,12 @@ async def test_form_user(hass):
|
|||||||
"homeassistant.components.opentherm_gw.async_setup_entry",
|
"homeassistant.components.opentherm_gw.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_setup_entry, patch(
|
) as mock_setup_entry, patch(
|
||||||
"pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS
|
"pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS
|
||||||
) as mock_pyotgw_connect, patch(
|
) as mock_pyotgw_connect, patch(
|
||||||
"pyotgw.pyotgw.disconnect", return_value=None
|
"pyotgw.OpenThermGateway.disconnect", return_value=None
|
||||||
) as mock_pyotgw_disconnect:
|
) as mock_pyotgw_disconnect, patch(
|
||||||
|
"pyotgw.status.StatusManager._process_updates", return_value=None
|
||||||
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
|
result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
|
||||||
)
|
)
|
||||||
@ -75,10 +77,12 @@ async def test_form_import(hass):
|
|||||||
"homeassistant.components.opentherm_gw.async_setup_entry",
|
"homeassistant.components.opentherm_gw.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_setup_entry, patch(
|
) as mock_setup_entry, patch(
|
||||||
"pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS
|
"pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS
|
||||||
) as mock_pyotgw_connect, patch(
|
) as mock_pyotgw_connect, patch(
|
||||||
"pyotgw.pyotgw.disconnect", return_value=None
|
"pyotgw.OpenThermGateway.disconnect", return_value=None
|
||||||
) as mock_pyotgw_disconnect:
|
) as mock_pyotgw_disconnect, patch(
|
||||||
|
"pyotgw.status.StatusManager._process_updates", return_value=None
|
||||||
|
):
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_IMPORT},
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
@ -117,10 +121,12 @@ async def test_form_duplicate_entries(hass):
|
|||||||
"homeassistant.components.opentherm_gw.async_setup_entry",
|
"homeassistant.components.opentherm_gw.async_setup_entry",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
) as mock_setup_entry, patch(
|
) as mock_setup_entry, patch(
|
||||||
"pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS
|
"pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS
|
||||||
) as mock_pyotgw_connect, patch(
|
) as mock_pyotgw_connect, patch(
|
||||||
"pyotgw.pyotgw.disconnect", return_value=None
|
"pyotgw.OpenThermGateway.disconnect", return_value=None
|
||||||
) as mock_pyotgw_disconnect:
|
) as mock_pyotgw_disconnect, patch(
|
||||||
|
"pyotgw.status.StatusManager._process_updates", return_value=None
|
||||||
|
):
|
||||||
result1 = await hass.config_entries.flow.async_configure(
|
result1 = await hass.config_entries.flow.async_configure(
|
||||||
flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
|
flow1["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
|
||||||
)
|
)
|
||||||
@ -148,8 +154,10 @@ async def test_form_connection_timeout(hass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"pyotgw.pyotgw.connect", side_effect=(asyncio.TimeoutError)
|
"pyotgw.OpenThermGateway.connect", side_effect=(asyncio.TimeoutError)
|
||||||
) as mock_connect:
|
) as mock_connect, patch(
|
||||||
|
"pyotgw.status.StatusManager._process_updates", return_value=None
|
||||||
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"},
|
{CONF_NAME: "Test Entry 1", CONF_DEVICE: "socket://192.0.2.254:1234"},
|
||||||
@ -166,7 +174,11 @@ async def test_form_connection_error(hass):
|
|||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
|
||||||
with patch("pyotgw.pyotgw.connect", side_effect=(SerialException)) as mock_connect:
|
with patch(
|
||||||
|
"pyotgw.OpenThermGateway.connect", side_effect=(SerialException)
|
||||||
|
) as mock_connect, patch(
|
||||||
|
"pyotgw.status.StatusManager._process_updates", return_value=None
|
||||||
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
|
result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
|
||||||
)
|
)
|
||||||
@ -196,7 +208,11 @@ async def test_options_migration(hass):
|
|||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.connect_and_subscribe",
|
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.connect_and_subscribe",
|
||||||
return_value=True,
|
return_value=True,
|
||||||
), patch("homeassistant.components.opentherm_gw.async_setup", return_value=True):
|
), patch(
|
||||||
|
"homeassistant.components.opentherm_gw.async_setup", return_value=True
|
||||||
|
), patch(
|
||||||
|
"pyotgw.status.StatusManager._process_updates", return_value=None
|
||||||
|
):
|
||||||
await hass.config_entries.async_setup(entry.entry_id)
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ async def test_device_registry_insert(hass):
|
|||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup",
|
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup",
|
||||||
return_value=None,
|
return_value=None,
|
||||||
), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS):
|
), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS):
|
||||||
await setup.async_setup_component(hass, DOMAIN, {})
|
await setup.async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
@ -62,7 +62,7 @@ async def test_device_registry_update(hass):
|
|||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup",
|
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup",
|
||||||
return_value=None,
|
return_value=None,
|
||||||
), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS_UPD):
|
), patch("pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS_UPD):
|
||||||
await setup.async_setup_component(hass, DOMAIN, {})
|
await setup.async_setup_component(hass, DOMAIN, {})
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
@ -9,14 +9,18 @@ import aiohttp
|
|||||||
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
|
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
|
||||||
from pyunifiprotect.data import NVR, Bootstrap, Light
|
from pyunifiprotect.data import NVR, Bootstrap, Light
|
||||||
|
|
||||||
from homeassistant.components.unifiprotect.const import CONF_DISABLE_RTSP, DOMAIN
|
from homeassistant.components.unifiprotect.const import (
|
||||||
|
CONF_DISABLE_RTSP,
|
||||||
|
DEFAULT_SCAN_INTERVAL,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from . import _patch_discovery
|
from . import _patch_discovery
|
||||||
from .utils import MockUFPFixture, init_entry
|
from .utils import MockUFPFixture, init_entry, time_changed
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -145,12 +149,23 @@ async def test_setup_failed_update(hass: HomeAssistant, ufp: MockUFPFixture):
|
|||||||
async def test_setup_failed_update_reauth(hass: HomeAssistant, ufp: MockUFPFixture):
|
async def test_setup_failed_update_reauth(hass: HomeAssistant, ufp: MockUFPFixture):
|
||||||
"""Test setup of unifiprotect entry with update that gives unauthroized error."""
|
"""Test setup of unifiprotect entry with update that gives unauthroized error."""
|
||||||
|
|
||||||
ufp.api.update = AsyncMock(side_effect=NotAuthorized)
|
|
||||||
|
|
||||||
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
await hass.config_entries.async_setup(ufp.entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert ufp.entry.state == ConfigEntryState.SETUP_RETRY
|
assert ufp.entry.state == ConfigEntryState.LOADED
|
||||||
assert ufp.api.update.called
|
|
||||||
|
# reauth should not be triggered until there are 10 auth failures in a row
|
||||||
|
# to verify it is not transient
|
||||||
|
ufp.api.update = AsyncMock(side_effect=NotAuthorized)
|
||||||
|
for _ in range(10):
|
||||||
|
await time_changed(hass, DEFAULT_SCAN_INTERVAL)
|
||||||
|
assert len(hass.config_entries.flow._progress) == 0
|
||||||
|
|
||||||
|
assert ufp.api.update.call_count == 10
|
||||||
|
assert ufp.entry.state == ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
await time_changed(hass, DEFAULT_SCAN_INTERVAL)
|
||||||
|
assert ufp.api.update.call_count == 11
|
||||||
|
assert len(hass.config_entries.flow._progress) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture):
|
async def test_setup_failed_error(hass: HomeAssistant, ufp: MockUFPFixture):
|
||||||
|
@ -15,7 +15,11 @@ from homeassistant.components.light import (
|
|||||||
ColorMode,
|
ColorMode,
|
||||||
)
|
)
|
||||||
from homeassistant.components.zha.core.group import GroupMember
|
from homeassistant.components.zha.core.group import GroupMember
|
||||||
from homeassistant.components.zha.light import FLASH_EFFECTS
|
from homeassistant.components.zha.light import (
|
||||||
|
CAPABILITIES_COLOR_TEMP,
|
||||||
|
CAPABILITIES_COLOR_XY,
|
||||||
|
FLASH_EFFECTS,
|
||||||
|
)
|
||||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
@ -142,6 +146,10 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined):
|
|||||||
ieee=IEEE_GROUPABLE_DEVICE,
|
ieee=IEEE_GROUPABLE_DEVICE,
|
||||||
nwk=0xB79D,
|
nwk=0xB79D,
|
||||||
)
|
)
|
||||||
|
color_cluster = zigpy_device.endpoints[1].light_color
|
||||||
|
color_cluster.PLUGGED_ATTR_READS = {
|
||||||
|
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY
|
||||||
|
}
|
||||||
zha_device = await zha_device_joined(zigpy_device)
|
zha_device = await zha_device_joined(zigpy_device)
|
||||||
zha_device.available = True
|
zha_device.available = True
|
||||||
return zha_device
|
return zha_device
|
||||||
@ -167,8 +175,13 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined):
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
ieee=IEEE_GROUPABLE_DEVICE2,
|
ieee=IEEE_GROUPABLE_DEVICE2,
|
||||||
|
manufacturer="Sengled",
|
||||||
nwk=0xC79E,
|
nwk=0xC79E,
|
||||||
)
|
)
|
||||||
|
color_cluster = zigpy_device.endpoints[1].light_color
|
||||||
|
color_cluster.PLUGGED_ATTR_READS = {
|
||||||
|
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY
|
||||||
|
}
|
||||||
zha_device = await zha_device_joined(zigpy_device)
|
zha_device = await zha_device_joined(zigpy_device)
|
||||||
zha_device.available = True
|
zha_device.available = True
|
||||||
return zha_device
|
return zha_device
|
||||||
@ -201,6 +214,38 @@ async def device_light_3(hass, zigpy_device_mock, zha_device_joined):
|
|||||||
return zha_device
|
return zha_device
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined):
|
||||||
|
"""Mock eWeLink light."""
|
||||||
|
|
||||||
|
zigpy_device = zigpy_device_mock(
|
||||||
|
{
|
||||||
|
1: {
|
||||||
|
SIG_EP_INPUT: [
|
||||||
|
general.OnOff.cluster_id,
|
||||||
|
general.LevelControl.cluster_id,
|
||||||
|
lighting.Color.cluster_id,
|
||||||
|
general.Groups.cluster_id,
|
||||||
|
general.Identify.cluster_id,
|
||||||
|
],
|
||||||
|
SIG_EP_OUTPUT: [],
|
||||||
|
SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
|
||||||
|
SIG_EP_PROFILE: zha.PROFILE_ID,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ieee="03:2d:6f:00:0a:90:69:e3",
|
||||||
|
manufacturer="eWeLink",
|
||||||
|
nwk=0xB79D,
|
||||||
|
)
|
||||||
|
color_cluster = zigpy_device.endpoints[1].light_color
|
||||||
|
color_cluster.PLUGGED_ATTR_READS = {
|
||||||
|
"color_capabilities": CAPABILITIES_COLOR_TEMP | CAPABILITIES_COLOR_XY
|
||||||
|
}
|
||||||
|
zha_device = await zha_device_joined(zigpy_device)
|
||||||
|
zha_device.available = True
|
||||||
|
return zha_device
|
||||||
|
|
||||||
|
|
||||||
async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored):
|
async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored):
|
||||||
"""Test zha light platform refresh."""
|
"""Test zha light platform refresh."""
|
||||||
|
|
||||||
@ -323,6 +368,758 @@ async def test_light(
|
|||||||
await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG)
|
await async_test_flash_from_hass(hass, cluster_identify, entity_id, FLASH_LONG)
|
||||||
|
|
||||||
|
|
||||||
|
@patch(
|
||||||
|
"zigpy.zcl.clusters.lighting.Color.request",
|
||||||
|
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
|
||||||
|
)
|
||||||
|
@patch(
|
||||||
|
"zigpy.zcl.clusters.general.Identify.request",
|
||||||
|
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
|
||||||
|
)
|
||||||
|
@patch(
|
||||||
|
"zigpy.zcl.clusters.general.LevelControl.request",
|
||||||
|
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
|
||||||
|
)
|
||||||
|
@patch(
|
||||||
|
"zigpy.zcl.clusters.general.OnOff.request",
|
||||||
|
new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]),
|
||||||
|
)
|
||||||
|
async def test_transitions(
|
||||||
|
hass, device_light_1, device_light_2, eWeLink_light, coordinator
|
||||||
|
):
|
||||||
|
"""Test ZHA light transition code."""
|
||||||
|
zha_gateway = get_zha_gateway(hass)
|
||||||
|
assert zha_gateway is not None
|
||||||
|
zha_gateway.coordinator_zha_device = coordinator
|
||||||
|
coordinator._zha_gateway = zha_gateway
|
||||||
|
device_light_1._zha_gateway = zha_gateway
|
||||||
|
device_light_2._zha_gateway = zha_gateway
|
||||||
|
member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee]
|
||||||
|
members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)]
|
||||||
|
|
||||||
|
assert coordinator.is_coordinator
|
||||||
|
|
||||||
|
# test creating a group with 2 members
|
||||||
|
zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert zha_group is not None
|
||||||
|
assert len(zha_group.members) == 2
|
||||||
|
for member in zha_group.members:
|
||||||
|
assert member.device.ieee in member_ieee_addresses
|
||||||
|
assert member.group == zha_group
|
||||||
|
assert member.endpoint is not None
|
||||||
|
|
||||||
|
device_1_entity_id = await find_entity_id(Platform.LIGHT, device_light_1, hass)
|
||||||
|
device_2_entity_id = await find_entity_id(Platform.LIGHT, device_light_2, hass)
|
||||||
|
eWeLink_light_entity_id = await find_entity_id(Platform.LIGHT, eWeLink_light, hass)
|
||||||
|
assert device_1_entity_id != device_2_entity_id
|
||||||
|
|
||||||
|
group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group)
|
||||||
|
assert hass.states.get(group_entity_id) is not None
|
||||||
|
|
||||||
|
assert device_1_entity_id in zha_group.member_entity_ids
|
||||||
|
assert device_2_entity_id in zha_group.member_entity_ids
|
||||||
|
|
||||||
|
dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off
|
||||||
|
dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off
|
||||||
|
eWeLink_cluster_on_off = eWeLink_light.device.endpoints[1].on_off
|
||||||
|
|
||||||
|
dev1_cluster_level = device_light_1.device.endpoints[1].level
|
||||||
|
dev2_cluster_level = device_light_2.device.endpoints[1].level
|
||||||
|
eWeLink_cluster_level = eWeLink_light.device.endpoints[1].level
|
||||||
|
|
||||||
|
dev1_cluster_color = device_light_1.device.endpoints[1].light_color
|
||||||
|
dev2_cluster_color = device_light_2.device.endpoints[1].light_color
|
||||||
|
eWeLink_cluster_color = eWeLink_light.device.endpoints[1].light_color
|
||||||
|
|
||||||
|
# allow traffic to flow through the gateway and device
|
||||||
|
await async_enable_traffic(hass, [device_light_1, device_light_2])
|
||||||
|
await async_wait_for_updates(hass)
|
||||||
|
|
||||||
|
# test that the lights were created and are off
|
||||||
|
group_state = hass.states.get(group_entity_id)
|
||||||
|
assert group_state.state == STATE_OFF
|
||||||
|
light1_state = hass.states.get(device_1_entity_id)
|
||||||
|
assert light1_state.state == STATE_OFF
|
||||||
|
light2_state = hass.states.get(device_2_entity_id)
|
||||||
|
assert light2_state.state == STATE_OFF
|
||||||
|
|
||||||
|
# first test 0 length transition with no color provided
|
||||||
|
dev1_cluster_on_off.request.reset_mock()
|
||||||
|
dev1_cluster_level.request.reset_mock()
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{"entity_id": device_1_entity_id, "transition": 0, "brightness": 50},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_on_off.request.call_count == 0
|
||||||
|
assert dev1_cluster_on_off.request.await_count == 0
|
||||||
|
assert dev1_cluster_color.request.call_count == 0
|
||||||
|
assert dev1_cluster_color.request.await_count == 0
|
||||||
|
assert dev1_cluster_level.request.call_count == 1
|
||||||
|
assert dev1_cluster_level.request.await_count == 1
|
||||||
|
assert dev1_cluster_level.request.call_args == call(
|
||||||
|
False,
|
||||||
|
4,
|
||||||
|
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
|
||||||
|
50, # brightness (level in ZCL)
|
||||||
|
0, # transition time
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
light1_state = hass.states.get(device_1_entity_id)
|
||||||
|
assert light1_state.state == STATE_ON
|
||||||
|
assert light1_state.attributes["brightness"] == 50
|
||||||
|
|
||||||
|
dev1_cluster_level.request.reset_mock()
|
||||||
|
|
||||||
|
# test non 0 length transition with color provided while light is on
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": device_1_entity_id,
|
||||||
|
"transition": 3,
|
||||||
|
"brightness": 18,
|
||||||
|
"color_temp": 432,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_on_off.request.call_count == 0
|
||||||
|
assert dev1_cluster_on_off.request.await_count == 0
|
||||||
|
assert dev1_cluster_color.request.call_count == 1
|
||||||
|
assert dev1_cluster_color.request.await_count == 1
|
||||||
|
assert dev1_cluster_level.request.call_count == 1
|
||||||
|
assert dev1_cluster_level.request.await_count == 1
|
||||||
|
assert dev1_cluster_level.request.call_args == call(
|
||||||
|
False,
|
||||||
|
4,
|
||||||
|
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
|
||||||
|
18, # brightness (level in ZCL)
|
||||||
|
30, # transition time (ZCL time in 10ths of a second)
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_color.request.call_args == call(
|
||||||
|
False,
|
||||||
|
10,
|
||||||
|
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
|
||||||
|
432, # color temp mireds
|
||||||
|
30.0, # transition time (ZCL time in 10ths of a second)
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
light1_state = hass.states.get(device_1_entity_id)
|
||||||
|
assert light1_state.state == STATE_ON
|
||||||
|
assert light1_state.attributes["brightness"] == 18
|
||||||
|
assert light1_state.attributes["color_temp"] == 432
|
||||||
|
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||||
|
|
||||||
|
dev1_cluster_level.request.reset_mock()
|
||||||
|
dev1_cluster_color.request.reset_mock()
|
||||||
|
|
||||||
|
# test 0 length transition to turn light off
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_off",
|
||||||
|
{
|
||||||
|
"entity_id": device_1_entity_id,
|
||||||
|
"transition": 0,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_on_off.request.call_count == 0
|
||||||
|
assert dev1_cluster_on_off.request.await_count == 0
|
||||||
|
assert dev1_cluster_color.request.call_count == 0
|
||||||
|
assert dev1_cluster_color.request.await_count == 0
|
||||||
|
assert dev1_cluster_level.request.call_count == 1
|
||||||
|
assert dev1_cluster_level.request.await_count == 1
|
||||||
|
assert dev1_cluster_level.request.call_args == call(
|
||||||
|
False,
|
||||||
|
4,
|
||||||
|
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
|
||||||
|
0, # brightness (level in ZCL)
|
||||||
|
0, # transition time (ZCL time in 10ths of a second)
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
light1_state = hass.states.get(device_1_entity_id)
|
||||||
|
assert light1_state.state == STATE_OFF
|
||||||
|
|
||||||
|
dev1_cluster_level.request.reset_mock()
|
||||||
|
|
||||||
|
# test non 0 length transition and color temp while turning light on (color_provided_while_off)
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": device_1_entity_id,
|
||||||
|
"transition": 1,
|
||||||
|
"brightness": 25,
|
||||||
|
"color_temp": 235,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_on_off.request.call_count == 0
|
||||||
|
assert dev1_cluster_on_off.request.await_count == 0
|
||||||
|
assert dev1_cluster_color.request.call_count == 1
|
||||||
|
assert dev1_cluster_color.request.await_count == 1
|
||||||
|
assert dev1_cluster_level.request.call_count == 2
|
||||||
|
assert dev1_cluster_level.request.await_count == 2
|
||||||
|
|
||||||
|
# first it comes on with no transition at 2 brightness
|
||||||
|
assert dev1_cluster_level.request.call_args_list[0] == call(
|
||||||
|
False,
|
||||||
|
4,
|
||||||
|
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
|
||||||
|
2, # brightness (level in ZCL)
|
||||||
|
0, # transition time (ZCL time in 10ths of a second)
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_color.request.call_args == call(
|
||||||
|
False,
|
||||||
|
10,
|
||||||
|
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
|
||||||
|
235, # color temp mireds
|
||||||
|
0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_level.request.call_args_list[1] == call(
|
||||||
|
False,
|
||||||
|
0,
|
||||||
|
dev1_cluster_level.commands_by_name["move_to_level"].schema,
|
||||||
|
25, # brightness (level in ZCL)
|
||||||
|
10.0, # transition time (ZCL time in 10ths of a second)
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
light1_state = hass.states.get(device_1_entity_id)
|
||||||
|
assert light1_state.state == STATE_ON
|
||||||
|
assert light1_state.attributes["brightness"] == 25
|
||||||
|
assert light1_state.attributes["color_temp"] == 235
|
||||||
|
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||||
|
|
||||||
|
dev1_cluster_level.request.reset_mock()
|
||||||
|
dev1_cluster_color.request.reset_mock()
|
||||||
|
|
||||||
|
# turn light 1 back off
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_off",
|
||||||
|
{
|
||||||
|
"entity_id": device_1_entity_id,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_on_off.request.call_count == 1
|
||||||
|
assert dev1_cluster_on_off.request.await_count == 1
|
||||||
|
assert dev1_cluster_color.request.call_count == 0
|
||||||
|
assert dev1_cluster_color.request.await_count == 0
|
||||||
|
assert dev1_cluster_level.request.call_count == 0
|
||||||
|
assert dev1_cluster_level.request.await_count == 0
|
||||||
|
group_state = hass.states.get(group_entity_id)
|
||||||
|
assert group_state.state == STATE_OFF
|
||||||
|
|
||||||
|
dev1_cluster_on_off.request.reset_mock()
|
||||||
|
dev1_cluster_color.request.reset_mock()
|
||||||
|
dev1_cluster_level.request.reset_mock()
|
||||||
|
|
||||||
|
# test no transition provided and color temp while turning light on (color_provided_while_off)
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": device_1_entity_id,
|
||||||
|
"brightness": 25,
|
||||||
|
"color_temp": 236,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_on_off.request.call_count == 0
|
||||||
|
assert dev1_cluster_on_off.request.await_count == 0
|
||||||
|
assert dev1_cluster_color.request.call_count == 1
|
||||||
|
assert dev1_cluster_color.request.await_count == 1
|
||||||
|
assert dev1_cluster_level.request.call_count == 2
|
||||||
|
assert dev1_cluster_level.request.await_count == 2
|
||||||
|
|
||||||
|
# first it comes on with no transition at 2 brightness
|
||||||
|
assert dev1_cluster_level.request.call_args_list[0] == call(
|
||||||
|
False,
|
||||||
|
4,
|
||||||
|
dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
|
||||||
|
2, # brightness (level in ZCL)
|
||||||
|
0, # transition time (ZCL time in 10ths of a second)
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_color.request.call_args == call(
|
||||||
|
False,
|
||||||
|
10,
|
||||||
|
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
|
||||||
|
236, # color temp mireds
|
||||||
|
0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_level.request.call_args_list[1] == call(
|
||||||
|
False,
|
||||||
|
0,
|
||||||
|
dev1_cluster_level.commands_by_name["move_to_level"].schema,
|
||||||
|
25, # brightness (level in ZCL)
|
||||||
|
0, # transition time (ZCL time in 10ths of a second)
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
light1_state = hass.states.get(device_1_entity_id)
|
||||||
|
assert light1_state.state == STATE_ON
|
||||||
|
assert light1_state.attributes["brightness"] == 25
|
||||||
|
assert light1_state.attributes["color_temp"] == 236
|
||||||
|
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||||
|
|
||||||
|
dev1_cluster_level.request.reset_mock()
|
||||||
|
dev1_cluster_color.request.reset_mock()
|
||||||
|
|
||||||
|
# turn light 1 back off to setup group test
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_off",
|
||||||
|
{
|
||||||
|
"entity_id": device_1_entity_id,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_on_off.request.call_count == 1
|
||||||
|
assert dev1_cluster_on_off.request.await_count == 1
|
||||||
|
assert dev1_cluster_color.request.call_count == 0
|
||||||
|
assert dev1_cluster_color.request.await_count == 0
|
||||||
|
assert dev1_cluster_level.request.call_count == 0
|
||||||
|
assert dev1_cluster_level.request.await_count == 0
|
||||||
|
group_state = hass.states.get(group_entity_id)
|
||||||
|
assert group_state.state == STATE_OFF
|
||||||
|
|
||||||
|
dev1_cluster_on_off.request.reset_mock()
|
||||||
|
dev1_cluster_color.request.reset_mock()
|
||||||
|
dev1_cluster_level.request.reset_mock()
|
||||||
|
|
||||||
|
# test no transition when the same color temp is provided from off
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": device_1_entity_id,
|
||||||
|
"color_temp": 236,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_on_off.request.call_count == 1
|
||||||
|
assert dev1_cluster_on_off.request.await_count == 1
|
||||||
|
assert dev1_cluster_color.request.call_count == 1
|
||||||
|
assert dev1_cluster_color.request.await_count == 1
|
||||||
|
assert dev1_cluster_level.request.call_count == 0
|
||||||
|
assert dev1_cluster_level.request.await_count == 0
|
||||||
|
|
||||||
|
assert dev1_cluster_on_off.request.call_args == call(
|
||||||
|
False,
|
||||||
|
1,
|
||||||
|
dev1_cluster_on_off.commands_by_name["on"].schema,
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert dev1_cluster_color.request.call_args == call(
|
||||||
|
False,
|
||||||
|
10,
|
||||||
|
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
|
||||||
|
236, # color temp mireds
|
||||||
|
0, # transition time (ZCL time in 10ths of a second) - no transition when color_provided_while_off
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
light1_state = hass.states.get(device_1_entity_id)
|
||||||
|
assert light1_state.state == STATE_ON
|
||||||
|
assert light1_state.attributes["brightness"] == 25
|
||||||
|
assert light1_state.attributes["color_temp"] == 236
|
||||||
|
assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||||
|
|
||||||
|
dev1_cluster_on_off.request.reset_mock()
|
||||||
|
dev1_cluster_color.request.reset_mock()
|
||||||
|
|
||||||
|
# turn light 1 back off to setup group test
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_off",
|
||||||
|
{
|
||||||
|
"entity_id": device_1_entity_id,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_on_off.request.call_count == 1
|
||||||
|
assert dev1_cluster_on_off.request.await_count == 1
|
||||||
|
assert dev1_cluster_color.request.call_count == 0
|
||||||
|
assert dev1_cluster_color.request.await_count == 0
|
||||||
|
assert dev1_cluster_level.request.call_count == 0
|
||||||
|
assert dev1_cluster_level.request.await_count == 0
|
||||||
|
group_state = hass.states.get(group_entity_id)
|
||||||
|
assert group_state.state == STATE_OFF
|
||||||
|
|
||||||
|
dev1_cluster_on_off.request.reset_mock()
|
||||||
|
dev1_cluster_color.request.reset_mock()
|
||||||
|
dev1_cluster_level.request.reset_mock()
|
||||||
|
|
||||||
|
# test sengled light uses default minimum transition time
|
||||||
|
dev2_cluster_on_off.request.reset_mock()
|
||||||
|
dev2_cluster_color.request.reset_mock()
|
||||||
|
dev2_cluster_level.request.reset_mock()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{"entity_id": device_2_entity_id, "transition": 0, "brightness": 100},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev2_cluster_on_off.request.call_count == 0
|
||||||
|
assert dev2_cluster_on_off.request.await_count == 0
|
||||||
|
assert dev2_cluster_color.request.call_count == 0
|
||||||
|
assert dev2_cluster_color.request.await_count == 0
|
||||||
|
assert dev2_cluster_level.request.call_count == 1
|
||||||
|
assert dev2_cluster_level.request.await_count == 1
|
||||||
|
assert dev2_cluster_level.request.call_args == call(
|
||||||
|
False,
|
||||||
|
4,
|
||||||
|
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
|
||||||
|
100, # brightness (level in ZCL)
|
||||||
|
1, # transition time - sengled light uses default minimum
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
light2_state = hass.states.get(device_2_entity_id)
|
||||||
|
assert light2_state.state == STATE_ON
|
||||||
|
assert light2_state.attributes["brightness"] == 100
|
||||||
|
|
||||||
|
dev2_cluster_level.request.reset_mock()
|
||||||
|
|
||||||
|
# turn the sengled light back off
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_off",
|
||||||
|
{
|
||||||
|
"entity_id": device_2_entity_id,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev2_cluster_on_off.request.call_count == 1
|
||||||
|
assert dev2_cluster_on_off.request.await_count == 1
|
||||||
|
assert dev2_cluster_color.request.call_count == 0
|
||||||
|
assert dev2_cluster_color.request.await_count == 0
|
||||||
|
assert dev2_cluster_level.request.call_count == 0
|
||||||
|
assert dev2_cluster_level.request.await_count == 0
|
||||||
|
light2_state = hass.states.get(device_2_entity_id)
|
||||||
|
assert light2_state.state == STATE_OFF
|
||||||
|
|
||||||
|
dev2_cluster_on_off.request.reset_mock()
|
||||||
|
|
||||||
|
# test non 0 length transition and color temp while turning light on and sengled (color_provided_while_off)
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": device_2_entity_id,
|
||||||
|
"transition": 1,
|
||||||
|
"brightness": 25,
|
||||||
|
"color_temp": 235,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev2_cluster_on_off.request.call_count == 0
|
||||||
|
assert dev2_cluster_on_off.request.await_count == 0
|
||||||
|
assert dev2_cluster_color.request.call_count == 1
|
||||||
|
assert dev2_cluster_color.request.await_count == 1
|
||||||
|
assert dev2_cluster_level.request.call_count == 2
|
||||||
|
assert dev2_cluster_level.request.await_count == 2
|
||||||
|
|
||||||
|
# first it comes on with no transition at 2 brightness
|
||||||
|
assert dev2_cluster_level.request.call_args_list[0] == call(
|
||||||
|
False,
|
||||||
|
4,
|
||||||
|
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
|
||||||
|
2, # brightness (level in ZCL)
|
||||||
|
1, # transition time (ZCL time in 10ths of a second)
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
assert dev2_cluster_color.request.call_args == call(
|
||||||
|
False,
|
||||||
|
10,
|
||||||
|
dev2_cluster_color.commands_by_name["move_to_color_temp"].schema,
|
||||||
|
235, # color temp mireds
|
||||||
|
1, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
assert dev2_cluster_level.request.call_args_list[1] == call(
|
||||||
|
False,
|
||||||
|
0,
|
||||||
|
dev2_cluster_level.commands_by_name["move_to_level"].schema,
|
||||||
|
25, # brightness (level in ZCL)
|
||||||
|
10.0, # transition time (ZCL time in 10ths of a second)
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
light2_state = hass.states.get(device_2_entity_id)
|
||||||
|
assert light2_state.state == STATE_ON
|
||||||
|
assert light2_state.attributes["brightness"] == 25
|
||||||
|
assert light2_state.attributes["color_temp"] == 235
|
||||||
|
assert light2_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||||
|
|
||||||
|
dev2_cluster_level.request.reset_mock()
|
||||||
|
dev2_cluster_color.request.reset_mock()
|
||||||
|
|
||||||
|
# turn the sengled light back off
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_off",
|
||||||
|
{
|
||||||
|
"entity_id": device_2_entity_id,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev2_cluster_on_off.request.call_count == 1
|
||||||
|
assert dev2_cluster_on_off.request.await_count == 1
|
||||||
|
assert dev2_cluster_color.request.call_count == 0
|
||||||
|
assert dev2_cluster_color.request.await_count == 0
|
||||||
|
assert dev2_cluster_level.request.call_count == 0
|
||||||
|
assert dev2_cluster_level.request.await_count == 0
|
||||||
|
light2_state = hass.states.get(device_2_entity_id)
|
||||||
|
assert light2_state.state == STATE_OFF
|
||||||
|
|
||||||
|
dev2_cluster_on_off.request.reset_mock()
|
||||||
|
|
||||||
|
# test non 0 length transition and color temp while turning group light on (color_provided_while_off)
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": group_entity_id,
|
||||||
|
"transition": 1,
|
||||||
|
"brightness": 25,
|
||||||
|
"color_temp": 235,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
group_on_off_channel = zha_group.endpoint[general.OnOff.cluster_id]
|
||||||
|
group_level_channel = zha_group.endpoint[general.LevelControl.cluster_id]
|
||||||
|
group_color_channel = zha_group.endpoint[lighting.Color.cluster_id]
|
||||||
|
assert group_on_off_channel.request.call_count == 0
|
||||||
|
assert group_on_off_channel.request.await_count == 0
|
||||||
|
assert group_color_channel.request.call_count == 1
|
||||||
|
assert group_color_channel.request.await_count == 1
|
||||||
|
assert group_level_channel.request.call_count == 1
|
||||||
|
assert group_level_channel.request.await_count == 1
|
||||||
|
|
||||||
|
# groups are omitted from the 3 call dance for color_provided_while_off
|
||||||
|
assert group_color_channel.request.call_args == call(
|
||||||
|
False,
|
||||||
|
10,
|
||||||
|
dev2_cluster_color.commands_by_name["move_to_color_temp"].schema,
|
||||||
|
235, # color temp mireds
|
||||||
|
10.0, # transition time (ZCL time in 10ths of a second) - sengled transition == 1 when color_provided_while_off
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
assert group_level_channel.request.call_args == call(
|
||||||
|
False,
|
||||||
|
4,
|
||||||
|
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
|
||||||
|
25, # brightness (level in ZCL)
|
||||||
|
10.0, # transition time (ZCL time in 10ths of a second)
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
group_state = hass.states.get(group_entity_id)
|
||||||
|
assert group_state.state == STATE_ON
|
||||||
|
assert group_state.attributes["brightness"] == 25
|
||||||
|
assert group_state.attributes["color_temp"] == 235
|
||||||
|
assert group_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||||
|
|
||||||
|
group_on_off_channel.request.reset_mock()
|
||||||
|
group_color_channel.request.reset_mock()
|
||||||
|
group_level_channel.request.reset_mock()
|
||||||
|
|
||||||
|
# turn the sengled light back on
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": device_2_entity_id,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev2_cluster_on_off.request.call_count == 1
|
||||||
|
assert dev2_cluster_on_off.request.await_count == 1
|
||||||
|
assert dev2_cluster_color.request.call_count == 0
|
||||||
|
assert dev2_cluster_color.request.await_count == 0
|
||||||
|
assert dev2_cluster_level.request.call_count == 0
|
||||||
|
assert dev2_cluster_level.request.await_count == 0
|
||||||
|
light2_state = hass.states.get(device_2_entity_id)
|
||||||
|
assert light2_state.state == STATE_ON
|
||||||
|
|
||||||
|
dev2_cluster_on_off.request.reset_mock()
|
||||||
|
|
||||||
|
# turn the light off with a transition
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_off",
|
||||||
|
{"entity_id": device_2_entity_id, "transition": 2},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev2_cluster_on_off.request.call_count == 0
|
||||||
|
assert dev2_cluster_on_off.request.await_count == 0
|
||||||
|
assert dev2_cluster_color.request.call_count == 0
|
||||||
|
assert dev2_cluster_color.request.await_count == 0
|
||||||
|
assert dev2_cluster_level.request.call_count == 1
|
||||||
|
assert dev2_cluster_level.request.await_count == 1
|
||||||
|
assert dev2_cluster_level.request.call_args == call(
|
||||||
|
False,
|
||||||
|
4,
|
||||||
|
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
|
||||||
|
0, # brightness (level in ZCL)
|
||||||
|
20, # transition time
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
light2_state = hass.states.get(device_2_entity_id)
|
||||||
|
assert light2_state.state == STATE_OFF
|
||||||
|
|
||||||
|
dev2_cluster_level.request.reset_mock()
|
||||||
|
|
||||||
|
# turn the light back on with no args should use a transition and last known brightness
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{"entity_id": device_2_entity_id},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert dev2_cluster_on_off.request.call_count == 0
|
||||||
|
assert dev2_cluster_on_off.request.await_count == 0
|
||||||
|
assert dev2_cluster_color.request.call_count == 0
|
||||||
|
assert dev2_cluster_color.request.await_count == 0
|
||||||
|
assert dev2_cluster_level.request.call_count == 1
|
||||||
|
assert dev2_cluster_level.request.await_count == 1
|
||||||
|
assert dev2_cluster_level.request.call_args == call(
|
||||||
|
False,
|
||||||
|
4,
|
||||||
|
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
|
||||||
|
25, # brightness (level in ZCL) - this is the last brightness we set a few tests above
|
||||||
|
1, # transition time - sengled light uses default minimum
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
light2_state = hass.states.get(device_2_entity_id)
|
||||||
|
assert light2_state.state == STATE_ON
|
||||||
|
|
||||||
|
dev2_cluster_level.request.reset_mock()
|
||||||
|
|
||||||
|
# test eWeLink color temp while turning light on from off (color_provided_while_off)
|
||||||
|
await hass.services.async_call(
|
||||||
|
LIGHT_DOMAIN,
|
||||||
|
"turn_on",
|
||||||
|
{
|
||||||
|
"entity_id": eWeLink_light_entity_id,
|
||||||
|
"color_temp": 235,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert eWeLink_cluster_on_off.request.call_count == 1
|
||||||
|
assert eWeLink_cluster_on_off.request.await_count == 1
|
||||||
|
assert eWeLink_cluster_color.request.call_count == 1
|
||||||
|
assert eWeLink_cluster_color.request.await_count == 1
|
||||||
|
assert eWeLink_cluster_level.request.call_count == 0
|
||||||
|
assert eWeLink_cluster_level.request.await_count == 0
|
||||||
|
|
||||||
|
# first it comes on
|
||||||
|
assert eWeLink_cluster_on_off.request.call_args_list[0] == call(
|
||||||
|
False,
|
||||||
|
1,
|
||||||
|
eWeLink_cluster_on_off.commands_by_name["on"].schema,
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
assert dev1_cluster_color.request.call_args == call(
|
||||||
|
False,
|
||||||
|
10,
|
||||||
|
dev1_cluster_color.commands_by_name["move_to_color_temp"].schema,
|
||||||
|
235, # color temp mireds
|
||||||
|
0, # transition time (ZCL time in 10ths of a second)
|
||||||
|
expect_reply=True,
|
||||||
|
manufacturer=None,
|
||||||
|
tries=1,
|
||||||
|
tsn=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
eWeLink_state = hass.states.get(eWeLink_light_entity_id)
|
||||||
|
assert eWeLink_state.state == STATE_ON
|
||||||
|
assert eWeLink_state.attributes["color_temp"] == 235
|
||||||
|
assert eWeLink_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||||
|
|
||||||
|
|
||||||
async def async_test_on_off_from_light(hass, cluster, entity_id):
|
async def async_test_on_off_from_light(hass, cluster, entity_id):
|
||||||
"""Test on off functionality from the light."""
|
"""Test on off functionality from the light."""
|
||||||
# turn on at light
|
# turn on at light
|
||||||
@ -463,7 +1260,7 @@ async def async_test_level_on_off_from_hass(
|
|||||||
4,
|
4,
|
||||||
level_cluster.commands_by_name["move_to_level_with_on_off"].schema,
|
level_cluster.commands_by_name["move_to_level_with_on_off"].schema,
|
||||||
10,
|
10,
|
||||||
1,
|
0,
|
||||||
expect_reply=True,
|
expect_reply=True,
|
||||||
manufacturer=None,
|
manufacturer=None,
|
||||||
tries=1,
|
tries=1,
|
||||||
@ -601,7 +1398,10 @@ async def test_zha_group_light_entity(
|
|||||||
# test that the lights were created and are off
|
# test that the lights were created and are off
|
||||||
group_state = hass.states.get(group_entity_id)
|
group_state = hass.states.get(group_entity_id)
|
||||||
assert group_state.state == STATE_OFF
|
assert group_state.state == STATE_OFF
|
||||||
assert group_state.attributes["supported_color_modes"] == [ColorMode.HS]
|
assert group_state.attributes["supported_color_modes"] == [
|
||||||
|
ColorMode.COLOR_TEMP,
|
||||||
|
ColorMode.HS,
|
||||||
|
]
|
||||||
# Light which is off has no color mode
|
# Light which is off has no color mode
|
||||||
assert "color_mode" not in group_state.attributes
|
assert "color_mode" not in group_state.attributes
|
||||||
|
|
||||||
@ -629,7 +1429,10 @@ async def test_zha_group_light_entity(
|
|||||||
# Check state
|
# Check state
|
||||||
group_state = hass.states.get(group_entity_id)
|
group_state = hass.states.get(group_entity_id)
|
||||||
assert group_state.state == STATE_ON
|
assert group_state.state == STATE_ON
|
||||||
assert group_state.attributes["supported_color_modes"] == [ColorMode.HS]
|
assert group_state.attributes["supported_color_modes"] == [
|
||||||
|
ColorMode.COLOR_TEMP,
|
||||||
|
ColorMode.HS,
|
||||||
|
]
|
||||||
assert group_state.attributes["color_mode"] == ColorMode.HS
|
assert group_state.attributes["color_mode"] == ColorMode.HS
|
||||||
|
|
||||||
# test long flashing the lights from the HA
|
# test long flashing the lights from the HA
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import json
|
import json
|
||||||
|
from typing import NamedTuple
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -13,8 +14,9 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import CoreState
|
from homeassistant.core import CoreState
|
||||||
from homeassistant.helpers import storage
|
from homeassistant.helpers import storage
|
||||||
from homeassistant.util import dt
|
from homeassistant.util import dt
|
||||||
|
from homeassistant.util.color import RGBColor
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
from tests.common import async_fire_time_changed, async_test_home_assistant
|
||||||
|
|
||||||
MOCK_VERSION = 1
|
MOCK_VERSION = 1
|
||||||
MOCK_VERSION_2 = 2
|
MOCK_VERSION_2 = 2
|
||||||
@ -460,3 +462,47 @@ async def test_changing_delayed_written_data(hass, store, hass_storage):
|
|||||||
"key": MOCK_KEY,
|
"key": MOCK_KEY,
|
||||||
"data": {"hello": "world"},
|
"data": {"hello": "world"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_saving_load_round_trip(tmpdir):
|
||||||
|
"""Test saving and loading round trip."""
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
hass = await async_test_home_assistant(loop)
|
||||||
|
|
||||||
|
hass.config.config_dir = await hass.async_add_executor_job(
|
||||||
|
tmpdir.mkdir, "temp_storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
class NamedTupleSubclass(NamedTuple):
|
||||||
|
"""A NamedTuple subclass."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
|
||||||
|
nts = NamedTupleSubclass("a")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"named_tuple_subclass": nts,
|
||||||
|
"rgb_color": RGBColor(255, 255, 0),
|
||||||
|
"set": {1, 2, 3},
|
||||||
|
"list": [1, 2, 3],
|
||||||
|
"tuple": (1, 2, 3),
|
||||||
|
"dict_with_int": {1: 1, 2: 2},
|
||||||
|
"dict_with_named_tuple": {1: nts, 2: nts},
|
||||||
|
}
|
||||||
|
|
||||||
|
store = storage.Store(
|
||||||
|
hass, MOCK_VERSION_2, MOCK_KEY, minor_version=MOCK_MINOR_VERSION_1
|
||||||
|
)
|
||||||
|
await store.async_save(data)
|
||||||
|
load = await store.async_load()
|
||||||
|
assert load == {
|
||||||
|
"dict_with_int": {"1": 1, "2": 2},
|
||||||
|
"dict_with_named_tuple": {"1": ["a"], "2": ["a"]},
|
||||||
|
"list": [1, 2, 3],
|
||||||
|
"named_tuple_subclass": ["a"],
|
||||||
|
"rgb_color": [255, 255, 0],
|
||||||
|
"set": [1, 2, 3],
|
||||||
|
"tuple": [1, 2, 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass.async_stop(force=True)
|
||||||
|
@ -5,13 +5,13 @@ from json import JSONEncoder, dumps
|
|||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
from tempfile import mkdtemp
|
from tempfile import mkdtemp
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.core import Event, State
|
from homeassistant.core import Event, State
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.template import TupleWrapper
|
from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder
|
||||||
from homeassistant.util.json import (
|
from homeassistant.util.json import (
|
||||||
SerializationError,
|
SerializationError,
|
||||||
find_paths_unserializable_data,
|
find_paths_unserializable_data,
|
||||||
@ -82,23 +82,15 @@ def test_overwrite_and_reload(atomic_writes):
|
|||||||
|
|
||||||
def test_save_bad_data():
|
def test_save_bad_data():
|
||||||
"""Test error from trying to save unserializable data."""
|
"""Test error from trying to save unserializable data."""
|
||||||
|
|
||||||
|
class CannotSerializeMe:
|
||||||
|
"""Cannot serialize this."""
|
||||||
|
|
||||||
with pytest.raises(SerializationError) as excinfo:
|
with pytest.raises(SerializationError) as excinfo:
|
||||||
save_json("test4", {"hello": set()})
|
save_json("test4", {"hello": CannotSerializeMe()})
|
||||||
|
|
||||||
assert (
|
assert "Failed to serialize to JSON: test4. Bad data at $.hello=" in str(
|
||||||
"Failed to serialize to JSON: test4. Bad data at $.hello=set()(<class 'set'>"
|
excinfo.value
|
||||||
in str(excinfo.value)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_save_bad_data_tuple_wrapper():
|
|
||||||
"""Test error from trying to save unserializable data."""
|
|
||||||
with pytest.raises(SerializationError) as excinfo:
|
|
||||||
save_json("test4", {"hello": TupleWrapper(("4", "5"))})
|
|
||||||
|
|
||||||
assert (
|
|
||||||
"Failed to serialize to JSON: test4. Bad data at $.hello=('4', '5')(<class 'homeassistant.helpers.template.TupleWrapper'>"
|
|
||||||
in str(excinfo.value)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -127,6 +119,21 @@ def test_custom_encoder():
|
|||||||
assert data == "9"
|
assert data == "9"
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_encoder_is_passed():
|
||||||
|
"""Test we use orjson if they pass in the default encoder."""
|
||||||
|
fname = _path_for("test6")
|
||||||
|
with patch(
|
||||||
|
"homeassistant.util.json.orjson.dumps", return_value=b"{}"
|
||||||
|
) as mock_orjson_dumps:
|
||||||
|
save_json(fname, {"any": 1}, encoder=DefaultHASSJSONEncoder)
|
||||||
|
assert len(mock_orjson_dumps.mock_calls) == 1
|
||||||
|
# Patch json.dumps to make sure we are using the orjson path
|
||||||
|
with patch("homeassistant.util.json.json.dumps", side_effect=Exception):
|
||||||
|
save_json(fname, {"any": {1}}, encoder=DefaultHASSJSONEncoder)
|
||||||
|
data = load_json(fname)
|
||||||
|
assert data == {"any": [1]}
|
||||||
|
|
||||||
|
|
||||||
def test_find_unserializable_data():
|
def test_find_unserializable_data():
|
||||||
"""Find unserializeable data."""
|
"""Find unserializeable data."""
|
||||||
assert find_paths_unserializable_data(1) == {}
|
assert find_paths_unserializable_data(1) == {}
|
||||||
|
26
tests/util/yaml/fixtures/bad.yaml.txt
Normal file
26
tests/util/yaml/fixtures/bad.yaml.txt
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
- id: '1658085239190'
|
||||||
|
alias: Config validation test
|
||||||
|
description: ''
|
||||||
|
trigger:
|
||||||
|
- platform: time
|
||||||
|
at: 00:02:03
|
||||||
|
condition: []
|
||||||
|
action:
|
||||||
|
- service: script.notify_admin
|
||||||
|
data:
|
||||||
|
title: 'Here's something that does not work...!'
|
||||||
|
message: failing
|
||||||
|
mode: single
|
||||||
|
- id: '165808523911590'
|
||||||
|
alias: Config validation test FIXED
|
||||||
|
description: ''
|
||||||
|
trigger:
|
||||||
|
- platform: time
|
||||||
|
at: 00:02:03
|
||||||
|
condition: []
|
||||||
|
action:
|
||||||
|
- service: script.notify_admin
|
||||||
|
data:
|
||||||
|
title: 'Here is something that should work...!'
|
||||||
|
message: fixed?
|
||||||
|
mode: single
|
@ -2,6 +2,7 @@
|
|||||||
import importlib
|
import importlib
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
@ -490,3 +491,12 @@ def test_input(try_both_loaders, try_both_dumpers):
|
|||||||
def test_c_loader_is_available_in_ci():
|
def test_c_loader_is_available_in_ci():
|
||||||
"""Verify we are testing the C loader in the CI."""
|
"""Verify we are testing the C loader in the CI."""
|
||||||
assert yaml.loader.HAS_C_LOADER is True
|
assert yaml.loader.HAS_C_LOADER is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_loading_actual_file_with_syntax(hass, try_both_loaders):
|
||||||
|
"""Test loading a real file with syntax errors."""
|
||||||
|
with pytest.raises(HomeAssistantError):
|
||||||
|
fixture_path = pathlib.Path(__file__).parent.joinpath(
|
||||||
|
"fixtures", "bad.yaml.txt"
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(load_yaml_config_file, fixture_path)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user