mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17: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 datetime import timedelta
|
||||
from math import ceil
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from pyairvisual import CloudAPI, NodeSamba
|
||||
from pyairvisual.errors import (
|
||||
@ -12,6 +12,7 @@ from pyairvisual.errors import (
|
||||
InvalidKeyError,
|
||||
KeyExpiredError,
|
||||
NodeProError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@ -210,9 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
try:
|
||||
data = await api_coro
|
||||
return cast(dict[str, Any], data)
|
||||
except (InvalidKeyError, KeyExpiredError) as ex:
|
||||
return await api_coro
|
||||
except (InvalidKeyError, KeyExpiredError, UnauthorizedError) as ex:
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
except AirVisualError as 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(
|
||||
entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD]
|
||||
) as node:
|
||||
data = await node.async_get_latest_measurements()
|
||||
return cast(dict[str, Any], data)
|
||||
return await node.async_get_latest_measurements()
|
||||
except NodeProError as err:
|
||||
raise UpdateFailed(f"Error while retrieving data: {err}") from err
|
||||
|
||||
|
@ -9,8 +9,10 @@ from pyairvisual import CloudAPI, NodeSamba
|
||||
from pyairvisual.errors import (
|
||||
AirVisualError,
|
||||
InvalidKeyError,
|
||||
KeyExpiredError,
|
||||
NodeProError,
|
||||
NotFoundError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
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:
|
||||
try:
|
||||
await coro
|
||||
except InvalidKeyError:
|
||||
except (InvalidKeyError, KeyExpiredError, UnauthorizedError):
|
||||
errors[CONF_API_KEY] = "invalid_api_key"
|
||||
except NotFoundError:
|
||||
errors[CONF_CITY] = "location_not_found"
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "AirVisual",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airvisual",
|
||||
"requirements": ["pyairvisual==5.0.9"],
|
||||
"requirements": ["pyairvisual==2022.07.0"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyairvisual", "pysmb"]
|
||||
|
@ -88,6 +88,7 @@ class AladdinDevice(CoverEntity):
|
||||
self._device_id = device["device_id"]
|
||||
self._number = device["door_number"]
|
||||
self._attr_name = device["name"]
|
||||
self._serial = device["serial"]
|
||||
self._attr_unique_id = f"{self._device_id}-{self._number}"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@ -97,8 +98,8 @@ class AladdinDevice(CoverEntity):
|
||||
"""Schedule a state update."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
self._acc.register_callback(update_callback, self._number)
|
||||
await self._acc.get_doors(self._number)
|
||||
self._acc.register_callback(update_callback, self._serial)
|
||||
await self._acc.get_doors(self._serial)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Close Aladdin Connect before removing."""
|
||||
@ -114,7 +115,7 @@ class AladdinDevice(CoverEntity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update status of cover."""
|
||||
await self._acc.get_doors(self._number)
|
||||
await self._acc.get_doors(self._serial)
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "aladdin_connect",
|
||||
"name": "Aladdin Connect",
|
||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||
"requirements": ["AIOAladdinConnect==0.1.25"],
|
||||
"requirements": ["AIOAladdinConnect==0.1.27"],
|
||||
"codeowners": ["@mkmer"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aladdin_connect"],
|
||||
|
@ -11,3 +11,4 @@ class DoorDevice(TypedDict):
|
||||
door_number: int
|
||||
name: str
|
||||
status: str
|
||||
serial: str
|
||||
|
@ -281,14 +281,14 @@ SENSOR_DESCRIPTIONS = (
|
||||
name="Lightning Strikes Per Day",
|
||||
icon="mdi:lightning-bolt",
|
||||
native_unit_of_measurement="strikes",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_LIGHTNING_PER_HOUR,
|
||||
name="Lightning Strikes Per Hour",
|
||||
icon="mdi:lightning-bolt",
|
||||
native_unit_of_measurement="strikes",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_MAXDAILYGUST,
|
||||
|
@ -87,7 +87,7 @@ class ArubaDeviceScanner(DeviceScanner):
|
||||
def get_aruba_data(self):
|
||||
"""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)
|
||||
query = ssh.expect(
|
||||
[
|
||||
|
@ -33,7 +33,8 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
entry.data[CONF_PASSWORD],
|
||||
get_region_from_name(entry.data[CONF_REGION]),
|
||||
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._entry = entry
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "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"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
|
@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_UNIT_SYSTEM_IMPERIAL,
|
||||
LENGTH_KILOMETERS,
|
||||
LENGTH_MILES,
|
||||
PERCENTAGE,
|
||||
@ -183,9 +182,7 @@ class BMWSensor(BMWBaseEntity, SensorEntity):
|
||||
self._attr_name = f"{vehicle.name} {description.key}"
|
||||
self._attr_unique_id = f"{vehicle.vin}-{description.key}"
|
||||
|
||||
if unit_system.name == CONF_UNIT_SYSTEM_IMPERIAL:
|
||||
self._attr_native_unit_of_measurement = description.unit_imperial
|
||||
else:
|
||||
# Force metric system as BMW API apparently only returns metric values now
|
||||
self._attr_native_unit_of_measurement = description.unit_metric
|
||||
|
||||
@callback
|
||||
|
@ -99,7 +99,7 @@ class ForecastSolarOptionFlowHandler(OptionsFlow):
|
||||
CONF_API_KEY,
|
||||
description={
|
||||
"suggested_value": self.config_entry.options.get(
|
||||
CONF_API_KEY
|
||||
CONF_API_KEY, ""
|
||||
)
|
||||
},
|
||||
): str,
|
||||
|
@ -44,13 +44,15 @@ class HiveDeviceLight(HiveEntity, LightEntity):
|
||||
super().__init__(hive, hive_device)
|
||||
if self.device["hiveType"] == "warmwhitelight":
|
||||
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
elif self.device["hiveType"] == "tuneablelight":
|
||||
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP}
|
||||
self._attr_color_mode = ColorMode.COLOR_TEMP
|
||||
elif self.device["hiveType"] == "colourtuneablelight":
|
||||
self._attr_supported_color_modes = {ColorMode.COLOR_TEMP, ColorMode.HS}
|
||||
|
||||
self._attr_min_mireds = self.device.get("min_mireds")
|
||||
self._attr_max_mireds = self.device.get("max_mireds")
|
||||
self._attr_min_mireds = 153
|
||||
self._attr_max_mireds = 370
|
||||
|
||||
@refresh_system
|
||||
async def async_turn_on(self, **kwargs):
|
||||
@ -94,6 +96,13 @@ class HiveDeviceLight(HiveEntity, LightEntity):
|
||||
if self._attr_available:
|
||||
self._attr_is_on = self.device["status"]["state"]
|
||||
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["status"]["mode"] == "COLOUR":
|
||||
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.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 .const import DOMAIN, KNOWN_DEVICES, TRIGGERS
|
||||
@ -86,13 +86,13 @@ class TriggerSource:
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
trigger_data = automation_info["trigger_data"]
|
||||
job = HassJob(action)
|
||||
|
||||
@callback
|
||||
def event_handler(char):
|
||||
if config[CONF_SUBTYPE] != HK_TO_HA_INPUT_EVENT_VALUES[char["value"]]:
|
||||
return
|
||||
self._hass.async_create_task(
|
||||
action({"trigger": {**trigger_data, **config}})
|
||||
)
|
||||
self._hass.async_run_hass_job(job, {"trigger": {**trigger_data, **config}})
|
||||
|
||||
trigger = self._triggers[config[CONF_TYPE], config[CONF_SUBTYPE]]
|
||||
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]):
|
||||
"""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():
|
||||
if aid in conn.devices:
|
||||
device_id = conn.devices[aid]
|
||||
if device_id in conn.hass.data[TRIGGERS]:
|
||||
source = conn.hass.data[TRIGGERS][device_id]
|
||||
if source := trigger_sources.get(device_id):
|
||||
source.fire(iid, ev)
|
||||
|
||||
|
||||
|
@ -99,7 +99,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
client = Client(
|
||||
host=host,
|
||||
port=port,
|
||||
loop=hass.loop,
|
||||
update_interval=scan_interval.total_seconds(),
|
||||
infer_arming_state=infer_arming_state,
|
||||
)
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "ness_alarm",
|
||||
"name": "Ness Alarm",
|
||||
"documentation": "https://www.home-assistant.io/integrations/ness_alarm",
|
||||
"requirements": ["nessclient==0.9.15"],
|
||||
"requirements": ["nessclient==0.10.0"],
|
||||
"codeowners": ["@nickw444"],
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["nessclient"]
|
||||
|
@ -59,7 +59,9 @@ class NetgearUpdateEntity(NetgearRouterEntity, UpdateEntity):
|
||||
"""Latest version available for install."""
|
||||
if self.coordinator.data is not None:
|
||||
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 self.installed_version
|
||||
|
||||
|
@ -416,7 +416,7 @@ class OpenThermGatewayDevice:
|
||||
self.status = {}
|
||||
self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update"
|
||||
self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update"
|
||||
self.gateway = pyotgw.pyotgw()
|
||||
self.gateway = pyotgw.OpenThermGateway()
|
||||
self.gw_version = None
|
||||
|
||||
async def cleanup(self, event=None):
|
||||
@ -427,7 +427,7 @@ class OpenThermGatewayDevice:
|
||||
|
||||
async def connect_and_subscribe(self):
|
||||
"""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)
|
||||
self.gw_version = version_string[18:] if version_string else None
|
||||
_LOGGER.debug(
|
||||
|
@ -59,8 +59,8 @@ class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def test_connection():
|
||||
"""Try to connect to the OpenTherm Gateway."""
|
||||
otgw = pyotgw.pyotgw()
|
||||
status = await otgw.connect(self.hass.loop, device)
|
||||
otgw = pyotgw.OpenThermGateway()
|
||||
status = await otgw.connect(device)
|
||||
await otgw.disconnect()
|
||||
return status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "opentherm_gw",
|
||||
"name": "OpenTherm Gateway",
|
||||
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
|
||||
"requirements": ["pyotgw==1.1b1"],
|
||||
"requirements": ["pyotgw==2.0.0"],
|
||||
"codeowners": ["@mvn23"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_push",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Shelly",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
||||
"requirements": ["aioshelly==2.0.0"],
|
||||
"requirements": ["aioshelly==2.0.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "SimpliSafe",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/simplisafe",
|
||||
"requirements": ["simplisafe-python==2022.06.1"],
|
||||
"requirements": ["simplisafe-python==2022.07.0"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "cloud_polling",
|
||||
"dhcp": [
|
||||
|
@ -187,14 +187,15 @@ def filter_libav_logging() -> None:
|
||||
return logging.getLogger(__name__).isEnabledFor(logging.DEBUG)
|
||||
|
||||
for logging_namespace in (
|
||||
"libav.mp4",
|
||||
"libav.NULL",
|
||||
"libav.h264",
|
||||
"libav.hevc",
|
||||
"libav.hls",
|
||||
"libav.mp4",
|
||||
"libav.mpegts",
|
||||
"libav.rtsp",
|
||||
"libav.tcp",
|
||||
"libav.tls",
|
||||
"libav.mpegts",
|
||||
"libav.NULL",
|
||||
):
|
||||
logging.getLogger(logging_namespace).addFilter(libav_filter)
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
"domain": "switchbot",
|
||||
"name": "SwitchBot",
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||
"requirements": ["PySwitchbot==0.14.0"],
|
||||
"requirements": ["PySwitchbot==0.14.1"],
|
||||
"config_flow": true,
|
||||
"codeowners": ["@danielhiversen", "@RenierM26"],
|
||||
"iot_class": "local_polling",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Tomorrow.io",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tomorrowio",
|
||||
"requirements": ["pytomorrowio==0.3.3"],
|
||||
"requirements": ["pytomorrowio==0.3.4"],
|
||||
"codeowners": ["@raman325", "@lymanepp"],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
@ -72,6 +72,7 @@ class ProtectData:
|
||||
self._pending_camera_ids: set[str] = set()
|
||||
self._unsub_interval: CALLBACK_TYPE | None = None
|
||||
self._unsub_websocket: CALLBACK_TYPE | None = None
|
||||
self._auth_failures = 0
|
||||
|
||||
self.last_update_success = False
|
||||
self.api = protect
|
||||
@ -117,6 +118,10 @@ class ProtectData:
|
||||
try:
|
||||
updates = await self.api.update(force=force)
|
||||
except NotAuthorized:
|
||||
if self._auth_failures < 10:
|
||||
_LOGGER.exception("Auth error while updating")
|
||||
self._auth_failures += 1
|
||||
else:
|
||||
await self.async_stop()
|
||||
_LOGGER.exception("Reauthentication required")
|
||||
self._entry.async_start_reauth(self._hass)
|
||||
@ -129,6 +134,7 @@ class ProtectData:
|
||||
self._async_process_updates(self.api.bootstrap)
|
||||
else:
|
||||
self.last_update_success = True
|
||||
self._auth_failures = 0
|
||||
self._async_process_updates(updates)
|
||||
|
||||
@callback
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Venstar",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/venstar",
|
||||
"requirements": ["venstarcolortouch==0.17"],
|
||||
"requirements": ["venstarcolortouch==0.18"],
|
||||
"codeowners": ["@garbled1"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["venstarcolortouch"]
|
||||
|
@ -73,7 +73,6 @@ CAPABILITIES_COLOR_LOOP = 0x4
|
||||
CAPABILITIES_COLOR_XY = 0x08
|
||||
CAPABILITIES_COLOR_TEMP = 0x10
|
||||
|
||||
DEFAULT_TRANSITION = 1
|
||||
DEFAULT_MIN_BRIGHTNESS = 2
|
||||
|
||||
UPDATE_COLORLOOP_ACTION = 0x1
|
||||
@ -119,7 +118,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
"""Operations common to all light entities."""
|
||||
|
||||
_FORCE_ON = False
|
||||
_DEFAULT_COLOR_FROM_OFF_TRANSITION = 0
|
||||
_DEFAULT_MIN_TRANSITION_TIME = 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize the light."""
|
||||
@ -140,7 +139,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
self._level_channel = None
|
||||
self._color_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
|
||||
|
||||
@property
|
||||
@ -216,33 +215,49 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
transition = kwargs.get(light.ATTR_TRANSITION)
|
||||
duration = (
|
||||
transition * 10
|
||||
if transition
|
||||
else self._default_transition * 10
|
||||
if self._default_transition
|
||||
else DEFAULT_TRANSITION
|
||||
)
|
||||
if transition is not None
|
||||
else self._zha_config_transition * 10
|
||||
) or self._DEFAULT_MIN_TRANSITION_TIME # if 0 is passed in some devices still need the minimum default
|
||||
brightness = kwargs.get(light.ATTR_BRIGHTNESS)
|
||||
effect = kwargs.get(light.ATTR_EFFECT)
|
||||
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,
|
||||
# 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.
|
||||
# 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.
|
||||
color_provided_from_off = (
|
||||
not self._state
|
||||
and brightness_supported(self._attr_supported_color_modes)
|
||||
and (light.ATTR_COLOR_TEMP in kwargs or light.ATTR_HS_COLOR in kwargs)
|
||||
# This can look especially bad with transitions longer than a second. We do not want to do this for
|
||||
# devices that need to be forced to use the on command because we would end up with 4 commands sent:
|
||||
# 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)
|
||||
)
|
||||
final_duration = duration
|
||||
if color_provided_from_off:
|
||||
# Set the duration for the color changing commands to 0.
|
||||
duration = 0
|
||||
|
||||
if (
|
||||
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
|
||||
):
|
||||
brightness = self._off_brightness
|
||||
@ -254,11 +269,11 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
|
||||
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.
|
||||
# After that, we set it to the desired color/temperature with no transition.
|
||||
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
|
||||
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
||||
@ -269,7 +284,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
|
||||
if (
|
||||
(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)
|
||||
):
|
||||
result = await self._level_channel.move_to_level_with_on_off(
|
||||
@ -285,7 +300,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
|
||||
if (
|
||||
brightness is None
|
||||
and not color_provided_from_off
|
||||
and not new_color_provided_while_off
|
||||
or (self._FORCE_ON and brightness)
|
||||
):
|
||||
# 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
|
||||
self._state = True
|
||||
|
||||
if light.ATTR_COLOR_TEMP in kwargs:
|
||||
temperature = kwargs[light.ATTR_COLOR_TEMP]
|
||||
result = await self._color_channel.move_to_color_temp(temperature, duration)
|
||||
if temperature is not None:
|
||||
result = await self._color_channel.move_to_color_temp(
|
||||
temperature,
|
||||
self._DEFAULT_MIN_TRANSITION_TIME
|
||||
if new_color_provided_while_off
|
||||
else duration,
|
||||
)
|
||||
t_log["move_to_color_temp"] = result
|
||||
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
||||
self.debug("turned on: %s", t_log)
|
||||
@ -308,11 +327,14 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
self._color_temp = temperature
|
||||
self._hs_color = None
|
||||
|
||||
if light.ATTR_HS_COLOR in kwargs:
|
||||
hs_color = kwargs[light.ATTR_HS_COLOR]
|
||||
if hs_color is not None:
|
||||
xy_color = color_util.color_hs_to_xy(*hs_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
|
||||
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._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.
|
||||
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
|
||||
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
||||
self.debug("turned on: %s", t_log)
|
||||
@ -371,12 +393,13 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""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)
|
||||
|
||||
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(
|
||||
0, duration * 10
|
||||
0, transition * 10
|
||||
)
|
||||
else:
|
||||
result = await self._on_off_channel.off()
|
||||
@ -387,7 +410,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
|
||||
if supports_level:
|
||||
# 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.async_write_ha_state()
|
||||
@ -460,7 +483,7 @@ class Light(BaseLight, ZhaEntity):
|
||||
if 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_OPTIONS,
|
||||
CONF_DEFAULT_LIGHT_TRANSITION,
|
||||
@ -472,6 +495,7 @@ class Light(BaseLight, ZhaEntity):
|
||||
"""Set the state."""
|
||||
self._state = bool(value)
|
||||
if value:
|
||||
self._off_with_transition = False
|
||||
self._off_brightness = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
@ -605,7 +629,7 @@ class HueLight(Light):
|
||||
@STRICT_MATCH(
|
||||
channel_names=CHANNEL_ON_OFF,
|
||||
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
|
||||
manufacturers={"Jasco", "Quotra-Vision"},
|
||||
manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"},
|
||||
)
|
||||
class ForceOnLight(Light):
|
||||
"""Representation of a light which does not respect move_to_level_with_on_off."""
|
||||
@ -621,7 +645,7 @@ class ForceOnLight(Light):
|
||||
class SengledLight(Light):
|
||||
"""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()
|
||||
@ -639,7 +663,7 @@ class LightGroup(BaseLight, ZhaGroupEntity):
|
||||
self._color_channel = group.endpoint[Color.cluster_id]
|
||||
self._identify_channel = group.endpoint[Identify.cluster_id]
|
||||
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_OPTIONS,
|
||||
CONF_DEFAULT_LIGHT_TRANSITION,
|
||||
|
@ -7,7 +7,7 @@ from .backports.enum import StrEnum
|
||||
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 7
|
||||
PATCH_VERSION: Final = "5"
|
||||
PATCH_VERSION: Final = "6"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
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.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
|
||||
|
||||
@ -45,10 +49,12 @@ def load_json(filename: str, default: list | dict | None = None) -> list | dict:
|
||||
return {} if default is None else default
|
||||
|
||||
|
||||
def _orjson_encoder(data: Any) -> str:
|
||||
"""JSON encoder that uses orjson."""
|
||||
def _orjson_default_encoder(data: Any) -> str:
|
||||
"""JSON encoder that uses orjson with hass defaults."""
|
||||
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")
|
||||
|
||||
|
||||
@ -64,13 +70,19 @@ def save_json(
|
||||
|
||||
Returns True on success.
|
||||
"""
|
||||
dump: Callable[[Any], Any] = json.dumps
|
||||
dump: Callable[[Any], Any]
|
||||
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)
|
||||
else:
|
||||
dump = _orjson_encoder
|
||||
json_data = _orjson_encoder(data)
|
||||
dump = _orjson_default_encoder
|
||||
json_data = _orjson_default_encoder(data)
|
||||
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))}"
|
||||
_LOGGER.error(msg)
|
||||
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
from collections import OrderedDict
|
||||
from collections.abc import Iterator
|
||||
import fnmatch
|
||||
from io import StringIO
|
||||
from io import StringIO, TextIOWrapper
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
@ -169,7 +169,7 @@ def parse_yaml(
|
||||
except yaml.YAMLError:
|
||||
# Loading failed, so we now load with the slow line loader
|
||||
# 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
|
||||
content.seek(0, 0)
|
||||
return _parse_yaml_pure_python(content, secrets)
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2022.7.5"
|
||||
version = "2022.7.6"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -5,7 +5,7 @@
|
||||
AEMET-OpenData==0.2.1
|
||||
|
||||
# homeassistant.components.aladdin_connect
|
||||
AIOAladdinConnect==0.1.25
|
||||
AIOAladdinConnect==0.1.27
|
||||
|
||||
# homeassistant.components.adax
|
||||
Adax-local==0.1.4
|
||||
@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.14.0
|
||||
PySwitchbot==0.14.1
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@ -244,7 +244,7 @@ aiosenseme==0.6.1
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==2.0.0
|
||||
aioshelly==2.0.1
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@ -396,7 +396,7 @@ beautifulsoup4==4.11.1
|
||||
bellows==0.31.1
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.9.6
|
||||
bimmer_connected==0.10.1
|
||||
|
||||
# homeassistant.components.bizkaibus
|
||||
bizkaibus==0.1.1
|
||||
@ -1068,7 +1068,7 @@ nad_receiver==0.3.0
|
||||
ndms2_client==0.1.1
|
||||
|
||||
# homeassistant.components.ness_alarm
|
||||
nessclient==0.9.15
|
||||
nessclient==0.10.0
|
||||
|
||||
# homeassistant.components.netdata
|
||||
netdata==1.0.1
|
||||
@ -1366,7 +1366,7 @@ pyaftership==21.11.0
|
||||
pyairnow==1.1.0
|
||||
|
||||
# homeassistant.components.airvisual
|
||||
pyairvisual==5.0.9
|
||||
pyairvisual==2022.07.0
|
||||
|
||||
# homeassistant.components.almond
|
||||
pyalmond==0.0.2
|
||||
@ -1715,7 +1715,7 @@ pyopnsense==0.2.0
|
||||
pyoppleio==1.0.5
|
||||
|
||||
# homeassistant.components.opentherm_gw
|
||||
pyotgw==1.1b1
|
||||
pyotgw==2.0.0
|
||||
|
||||
# homeassistant.auth.mfa_modules.notify
|
||||
# homeassistant.auth.mfa_modules.totp
|
||||
@ -1973,7 +1973,7 @@ pythonegardia==1.0.40
|
||||
pytile==2022.02.0
|
||||
|
||||
# homeassistant.components.tomorrowio
|
||||
pytomorrowio==0.3.3
|
||||
pytomorrowio==0.3.4
|
||||
|
||||
# homeassistant.components.touchline
|
||||
pytouchline==0.7
|
||||
@ -2168,7 +2168,7 @@ simplehound==0.3
|
||||
simplepush==1.1.4
|
||||
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==2022.06.1
|
||||
simplisafe-python==2022.07.0
|
||||
|
||||
# homeassistant.components.sisyphus
|
||||
sisyphus-control==3.1.2
|
||||
@ -2387,7 +2387,7 @@ vehicle==0.4.0
|
||||
velbus-aio==2022.6.2
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.17
|
||||
venstarcolortouch==0.18
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.3.2
|
||||
|
@ -7,7 +7,7 @@
|
||||
AEMET-OpenData==0.2.1
|
||||
|
||||
# homeassistant.components.aladdin_connect
|
||||
AIOAladdinConnect==0.1.25
|
||||
AIOAladdinConnect==0.1.27
|
||||
|
||||
# homeassistant.components.adax
|
||||
Adax-local==0.1.4
|
||||
@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.14.0
|
||||
PySwitchbot==0.14.1
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@ -213,7 +213,7 @@ aiosenseme==0.6.1
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==2.0.0
|
||||
aioshelly==2.0.1
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@ -311,7 +311,7 @@ beautifulsoup4==4.11.1
|
||||
bellows==0.31.1
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.9.6
|
||||
bimmer_connected==0.10.1
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==2.0.1
|
||||
@ -742,7 +742,7 @@ mutesync==0.0.1
|
||||
ndms2_client==0.1.1
|
||||
|
||||
# homeassistant.components.ness_alarm
|
||||
nessclient==0.9.15
|
||||
nessclient==0.10.0
|
||||
|
||||
# homeassistant.components.discovery
|
||||
netdisco==3.0.0
|
||||
@ -929,7 +929,7 @@ pyaehw4a1==0.3.9
|
||||
pyairnow==1.1.0
|
||||
|
||||
# homeassistant.components.airvisual
|
||||
pyairvisual==5.0.9
|
||||
pyairvisual==2022.07.0
|
||||
|
||||
# homeassistant.components.almond
|
||||
pyalmond==0.0.2
|
||||
@ -1167,7 +1167,7 @@ pyopenuv==2022.04.0
|
||||
pyopnsense==0.2.0
|
||||
|
||||
# homeassistant.components.opentherm_gw
|
||||
pyotgw==1.1b1
|
||||
pyotgw==2.0.0
|
||||
|
||||
# homeassistant.auth.mfa_modules.notify
|
||||
# homeassistant.auth.mfa_modules.totp
|
||||
@ -1314,7 +1314,7 @@ python_awair==0.2.3
|
||||
pytile==2022.02.0
|
||||
|
||||
# homeassistant.components.tomorrowio
|
||||
pytomorrowio==0.3.3
|
||||
pytomorrowio==0.3.4
|
||||
|
||||
# homeassistant.components.traccar
|
||||
pytraccar==0.10.0
|
||||
@ -1440,7 +1440,7 @@ simplehound==0.3
|
||||
simplepush==1.1.4
|
||||
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==2022.06.1
|
||||
simplisafe-python==2022.07.0
|
||||
|
||||
# homeassistant.components.slack
|
||||
slackclient==2.5.0
|
||||
@ -1590,7 +1590,7 @@ vehicle==0.4.0
|
||||
velbus-aio==2022.6.2
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.17
|
||||
venstarcolortouch==0.18
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.3.2
|
||||
|
@ -4,8 +4,10 @@ from unittest.mock import patch
|
||||
from pyairvisual.errors import (
|
||||
AirVisualError,
|
||||
InvalidKeyError,
|
||||
KeyExpiredError,
|
||||
NodeProError,
|
||||
NotFoundError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
import pytest
|
||||
|
||||
@ -84,6 +86,28 @@ async def test_duplicate_error(hass, config, config_entry, data):
|
||||
{CONF_API_KEY: "invalid_api_key"},
|
||||
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",
|
||||
|
@ -10,6 +10,7 @@ DEVICE_CONFIG_OPEN = {
|
||||
"name": "home",
|
||||
"status": "open",
|
||||
"link_status": "Connected",
|
||||
"serial": "12345",
|
||||
}
|
||||
|
||||
|
||||
|
@ -33,6 +33,7 @@ DEVICE_CONFIG_OPEN = {
|
||||
"name": "home",
|
||||
"status": "open",
|
||||
"link_status": "Connected",
|
||||
"serial": "12345",
|
||||
}
|
||||
|
||||
DEVICE_CONFIG_OPENING = {
|
||||
@ -41,6 +42,7 @@ DEVICE_CONFIG_OPENING = {
|
||||
"name": "home",
|
||||
"status": "opening",
|
||||
"link_status": "Connected",
|
||||
"serial": "12345",
|
||||
}
|
||||
|
||||
DEVICE_CONFIG_CLOSED = {
|
||||
@ -49,6 +51,7 @@ DEVICE_CONFIG_CLOSED = {
|
||||
"name": "home",
|
||||
"status": "closed",
|
||||
"link_status": "Connected",
|
||||
"serial": "12345",
|
||||
}
|
||||
|
||||
DEVICE_CONFIG_CLOSING = {
|
||||
@ -57,6 +60,7 @@ DEVICE_CONFIG_CLOSING = {
|
||||
"name": "home",
|
||||
"status": "closing",
|
||||
"link_status": "Connected",
|
||||
"serial": "12345",
|
||||
}
|
||||
|
||||
DEVICE_CONFIG_DISCONNECTED = {
|
||||
@ -65,6 +69,7 @@ DEVICE_CONFIG_DISCONNECTED = {
|
||||
"name": "home",
|
||||
"status": "open",
|
||||
"link_status": "Disconnected",
|
||||
"serial": "12345",
|
||||
}
|
||||
|
||||
DEVICE_CONFIG_BAD = {
|
||||
|
@ -43,10 +43,12 @@ async def test_form_user(hass):
|
||||
"homeassistant.components.opentherm_gw.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry, patch(
|
||||
"pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS
|
||||
"pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS
|
||||
) as mock_pyotgw_connect, patch(
|
||||
"pyotgw.pyotgw.disconnect", return_value=None
|
||||
) as mock_pyotgw_disconnect:
|
||||
"pyotgw.OpenThermGateway.disconnect", return_value=None
|
||||
) as mock_pyotgw_disconnect, patch(
|
||||
"pyotgw.status.StatusManager._process_updates", return_value=None
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
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",
|
||||
return_value=True,
|
||||
) as mock_setup_entry, patch(
|
||||
"pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS
|
||||
"pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS
|
||||
) as mock_pyotgw_connect, patch(
|
||||
"pyotgw.pyotgw.disconnect", return_value=None
|
||||
) as mock_pyotgw_disconnect:
|
||||
"pyotgw.OpenThermGateway.disconnect", return_value=None
|
||||
) as mock_pyotgw_disconnect, patch(
|
||||
"pyotgw.status.StatusManager._process_updates", return_value=None
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
@ -117,10 +121,12 @@ async def test_form_duplicate_entries(hass):
|
||||
"homeassistant.components.opentherm_gw.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry, patch(
|
||||
"pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS
|
||||
"pyotgw.OpenThermGateway.connect", return_value=MINIMAL_STATUS
|
||||
) as mock_pyotgw_connect, patch(
|
||||
"pyotgw.pyotgw.disconnect", return_value=None
|
||||
) as mock_pyotgw_disconnect:
|
||||
"pyotgw.OpenThermGateway.disconnect", return_value=None
|
||||
) as mock_pyotgw_disconnect, patch(
|
||||
"pyotgw.status.StatusManager._process_updates", return_value=None
|
||||
):
|
||||
result1 = await hass.config_entries.flow.async_configure(
|
||||
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(
|
||||
"pyotgw.pyotgw.connect", side_effect=(asyncio.TimeoutError)
|
||||
) as mock_connect:
|
||||
"pyotgw.OpenThermGateway.connect", side_effect=(asyncio.TimeoutError)
|
||||
) as mock_connect, patch(
|
||||
"pyotgw.status.StatusManager._process_updates", return_value=None
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{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}
|
||||
)
|
||||
|
||||
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(
|
||||
result["flow_id"], {CONF_NAME: "Test Entry 1", CONF_DEVICE: "/dev/ttyUSB0"}
|
||||
)
|
||||
@ -196,7 +208,11 @@ async def test_options_migration(hass):
|
||||
with patch(
|
||||
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.connect_and_subscribe",
|
||||
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.async_block_till_done()
|
||||
|
||||
|
@ -34,7 +34,7 @@ async def test_device_registry_insert(hass):
|
||||
with patch(
|
||||
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup",
|
||||
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 hass.async_block_till_done()
|
||||
@ -62,7 +62,7 @@ async def test_device_registry_update(hass):
|
||||
with patch(
|
||||
"homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup",
|
||||
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 hass.async_block_till_done()
|
||||
|
@ -9,14 +9,18 @@ import aiohttp
|
||||
from pyunifiprotect import NotAuthorized, NvrError, ProtectApiClient
|
||||
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.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import _patch_discovery
|
||||
from .utils import MockUFPFixture, init_entry
|
||||
from .utils import MockUFPFixture, init_entry, time_changed
|
||||
|
||||
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):
|
||||
"""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.async_block_till_done()
|
||||
assert ufp.entry.state == ConfigEntryState.SETUP_RETRY
|
||||
assert ufp.api.update.called
|
||||
assert ufp.entry.state == ConfigEntryState.LOADED
|
||||
|
||||
# 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):
|
||||
|
@ -15,7 +15,11 @@ from homeassistant.components.light import (
|
||||
ColorMode,
|
||||
)
|
||||
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
|
||||
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,
|
||||
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
|
||||
@ -167,8 +175,13 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined):
|
||||
}
|
||||
},
|
||||
ieee=IEEE_GROUPABLE_DEVICE2,
|
||||
manufacturer="Sengled",
|
||||
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.available = True
|
||||
return zha_device
|
||||
@ -201,6 +214,38 @@ async def device_light_3(hass, zigpy_device_mock, zha_device_joined):
|
||||
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):
|
||||
"""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)
|
||||
|
||||
|
||||
@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):
|
||||
"""Test on off functionality from the light."""
|
||||
# turn on at light
|
||||
@ -463,7 +1260,7 @@ async def async_test_level_on_off_from_hass(
|
||||
4,
|
||||
level_cluster.commands_by_name["move_to_level_with_on_off"].schema,
|
||||
10,
|
||||
1,
|
||||
0,
|
||||
expect_reply=True,
|
||||
manufacturer=None,
|
||||
tries=1,
|
||||
@ -601,7 +1398,10 @@ async def test_zha_group_light_entity(
|
||||
# test that the lights were created and are off
|
||||
group_state = hass.states.get(group_entity_id)
|
||||
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
|
||||
assert "color_mode" not in group_state.attributes
|
||||
|
||||
@ -629,7 +1429,10 @@ async def test_zha_group_light_entity(
|
||||
# Check state
|
||||
group_state = hass.states.get(group_entity_id)
|
||||
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
|
||||
|
||||
# test long flashing the lights from the HA
|
||||
|
@ -2,6 +2,7 @@
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import json
|
||||
from typing import NamedTuple
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
@ -13,8 +14,9 @@ from homeassistant.const import (
|
||||
from homeassistant.core import CoreState
|
||||
from homeassistant.helpers import storage
|
||||
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_2 = 2
|
||||
@ -460,3 +462,47 @@ async def test_changing_delayed_written_data(hass, store, hass_storage):
|
||||
"key": MOCK_KEY,
|
||||
"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 os
|
||||
from tempfile import mkdtemp
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.core import Event, State
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.template import TupleWrapper
|
||||
from homeassistant.helpers.json import JSONEncoder as DefaultHASSJSONEncoder
|
||||
from homeassistant.util.json import (
|
||||
SerializationError,
|
||||
find_paths_unserializable_data,
|
||||
@ -82,23 +82,15 @@ def test_overwrite_and_reload(atomic_writes):
|
||||
|
||||
def test_save_bad_data():
|
||||
"""Test error from trying to save unserializable data."""
|
||||
|
||||
class CannotSerializeMe:
|
||||
"""Cannot serialize this."""
|
||||
|
||||
with pytest.raises(SerializationError) as excinfo:
|
||||
save_json("test4", {"hello": set()})
|
||||
save_json("test4", {"hello": CannotSerializeMe()})
|
||||
|
||||
assert (
|
||||
"Failed to serialize to JSON: test4. Bad data at $.hello=set()(<class 'set'>"
|
||||
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)
|
||||
assert "Failed to serialize to JSON: test4. Bad data at $.hello=" in str(
|
||||
excinfo.value
|
||||
)
|
||||
|
||||
|
||||
@ -127,6 +119,21 @@ def test_custom_encoder():
|
||||
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():
|
||||
"""Find unserializeable data."""
|
||||
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 io
|
||||
import os
|
||||
import pathlib
|
||||
import unittest
|
||||
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():
|
||||
"""Verify we are testing the C loader in the CI."""
|
||||
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