Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bd7ea78f0 | |||
| e42038742a | |||
| ff58c4e564 | |||
| 5ba71a4675 | |||
| 7fb7bc8e50 | |||
| afa30be64b | |||
| 789eb029fa |
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 4
|
||||
CACHE_VERSION: 3
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.8"
|
||||
|
||||
@@ -381,7 +381,6 @@ homeassistant.components.openai_conversation.*
|
||||
homeassistant.components.openexchangerates.*
|
||||
homeassistant.components.opensky.*
|
||||
homeassistant.components.openuv.*
|
||||
homeassistant.components.opower.*
|
||||
homeassistant.components.oralb.*
|
||||
homeassistant.components.otbr.*
|
||||
homeassistant.components.overkiz.*
|
||||
|
||||
@@ -76,7 +76,6 @@ from .exceptions import HomeAssistantError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
category_registry,
|
||||
condition,
|
||||
config_validation as cv,
|
||||
device_registry,
|
||||
entity,
|
||||
@@ -453,7 +452,6 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||
create_eager_task(restore_state.async_load(hass)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(condition.async_setup(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.2.3"]
|
||||
"requirements": ["aioamazondevices==3.2.2"]
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ SERVICE_UPLOAD = "upload"
|
||||
ANDROIDTV_STATES = {
|
||||
"off": MediaPlayerState.OFF,
|
||||
"idle": MediaPlayerState.IDLE,
|
||||
"standby": MediaPlayerState.IDLE,
|
||||
"standby": MediaPlayerState.STANDBY,
|
||||
"playing": MediaPlayerState.PLAYING,
|
||||
"paused": MediaPlayerState.PAUSED,
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ class AppleTvMediaPlayer(
|
||||
self._is_feature_available(FeatureName.PowerState)
|
||||
and self.atv.power.power_state == PowerState.Off
|
||||
):
|
||||
return MediaPlayerState.OFF
|
||||
return MediaPlayerState.STANDBY
|
||||
if self._playing:
|
||||
state = self._playing.device_state
|
||||
if state in (DeviceState.Idle, DeviceState.Loading):
|
||||
@@ -200,7 +200,7 @@ class AppleTvMediaPlayer(
|
||||
return MediaPlayerState.PLAYING
|
||||
if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
|
||||
return MediaPlayerState.PAUSED
|
||||
return MediaPlayerState.IDLE # Bad or unknown state?
|
||||
return MediaPlayerState.STANDBY # Bad or unknown state?
|
||||
return None
|
||||
|
||||
@callback
|
||||
|
||||
@@ -107,7 +107,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
"""Return the state of the device."""
|
||||
media_state = self.client.play_state.state
|
||||
if media_state == "NETWORK":
|
||||
return MediaPlayerState.OFF
|
||||
return MediaPlayerState.STANDBY
|
||||
if self.client.state.power:
|
||||
if media_state == "play":
|
||||
return MediaPlayerState.PLAYING
|
||||
|
||||
@@ -94,7 +94,6 @@ async def _get_options_dict(handler: SchemaCommonFlowHandler | None) -> dict:
|
||||
max=6,
|
||||
mode=selector.NumberSelectorMode.BOX,
|
||||
unit_of_measurement="decimals",
|
||||
translation_key="round",
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_TIME_WINDOW): selector.DurationSelector(),
|
||||
|
||||
@@ -52,11 +52,6 @@
|
||||
"h": "Hours",
|
||||
"d": "Days"
|
||||
}
|
||||
},
|
||||
"round": {
|
||||
"unit_of_measurement": {
|
||||
"decimals": "decimals"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,7 @@ from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.hub import EheimDigitalHub
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
SOURCE_USER,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
@@ -131,52 +126,3 @@ class EheimDigitalConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=CONFIG_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of the config entry."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id=SOURCE_RECONFIGURE, data_schema=CONFIG_SCHEMA
|
||||
)
|
||||
|
||||
self._async_abort_entries_match(user_input)
|
||||
errors: dict[str, str] = {}
|
||||
hub = EheimDigitalHub(
|
||||
host=user_input[CONF_HOST],
|
||||
session=async_get_clientsession(self.hass),
|
||||
loop=self.hass.loop,
|
||||
main_device_added_event=self.main_device_added_event,
|
||||
)
|
||||
|
||||
try:
|
||||
await hub.connect()
|
||||
|
||||
async with asyncio.timeout(2):
|
||||
# This event gets triggered when the first message is received from
|
||||
# the device, it contains the data necessary to create the main device.
|
||||
# This removes the race condition where the main device is accessed
|
||||
# before the response from the device is parsed.
|
||||
await self.main_device_added_event.wait()
|
||||
if TYPE_CHECKING:
|
||||
# At this point the main device is always set
|
||||
assert isinstance(hub.main, EheimDigitalDevice)
|
||||
await self.async_set_unique_id(hub.main.mac_address)
|
||||
await hub.close()
|
||||
except (ClientError, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
errors["base"] = "unknown"
|
||||
LOGGER.exception("Unknown exception occurred")
|
||||
else:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates=user_input,
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id=SOURCE_RECONFIGURE,
|
||||
data_schema=CONFIG_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -60,7 +60,7 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: done
|
||||
|
||||
|
||||
@@ -4,14 +4,6 @@
|
||||
"discovery_confirm": {
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::eheimdigital::config::step::user::data_description::host%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
@@ -23,9 +15,7 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unique_id_mismatch": "The identifier does not match the previous identifier"
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
|
||||
@@ -126,7 +126,6 @@ class EnvoyEnchargeBinarySensorEntity(EnvoyBaseBinarySensorEntity):
|
||||
name=f"Encharge {serial_number}",
|
||||
sw_version=str(encharge_inventory[self._serial_number].firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -159,7 +158,6 @@ class EnvoyEnpowerBinarySensorEntity(EnvoyBaseBinarySensorEntity):
|
||||
name=f"Enpower {enpower.serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=enpower.serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -165,7 +165,6 @@ class EnvoyStorageSettingsNumberEntity(EnvoyBaseEntity, NumberEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
else:
|
||||
# If no enpower device assign numbers to Envoy itself
|
||||
|
||||
@@ -223,7 +223,6 @@ class EnvoyStorageSettingsSelectEntity(EnvoyBaseEntity, SelectEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
else:
|
||||
# If no enpower device assign selects to Envoy itself
|
||||
|
||||
@@ -1313,7 +1313,6 @@ class EnvoyInverterEntity(EnvoySensorBaseEntity):
|
||||
manufacturer="Enphase",
|
||||
model="Inverter",
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -1357,7 +1356,6 @@ class EnvoyEnchargeEntity(EnvoySensorBaseEntity):
|
||||
name=f"Encharge {serial_number}",
|
||||
sw_version=str(encharge_inventory[self._serial_number].firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
|
||||
@@ -1422,7 +1420,6 @@ class EnvoyEnpowerEntity(EnvoySensorBaseEntity):
|
||||
name=f"Enpower {enpower_data.serial_number}",
|
||||
sw_version=str(enpower_data.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=enpower_data.serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -138,7 +138,6 @@ class EnvoyEnpowerSwitchEntity(EnvoyBaseEntity, SwitchEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -236,7 +235,6 @@ class EnvoyStorageSettingsSwitchEntity(EnvoyBaseEntity, SwitchEntity):
|
||||
name=f"Enpower {self._serial_number}",
|
||||
sw_version=str(enpower.firmware_version),
|
||||
via_device=(DOMAIN, self.envoy_serial_num),
|
||||
serial_number=self._serial_number,
|
||||
)
|
||||
else:
|
||||
# If no enpower device assign switches to Envoy itself
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250702.1"]
|
||||
"requirements": ["home-assistant-frontend==20250702.0"]
|
||||
}
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
|
||||
from homeassistant.components.application_credentials import AuthorizationServer
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
AUTH_CALLBACK_PATH,
|
||||
MY_AUTH_CALLBACK_PATH,
|
||||
)
|
||||
|
||||
|
||||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
||||
@@ -18,14 +14,12 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe
|
||||
|
||||
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
|
||||
"""Return description placeholders for the credentials dialog."""
|
||||
if "my" in hass.config.components:
|
||||
redirect_url = MY_AUTH_CALLBACK_PATH
|
||||
else:
|
||||
ha_host = hass.config.external_url or "https://YOUR_DOMAIN:PORT"
|
||||
redirect_url = f"{ha_host}{AUTH_CALLBACK_PATH}"
|
||||
return {
|
||||
"oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent",
|
||||
"more_info_url": "https://www.home-assistant.io/integrations/google_assistant_sdk/",
|
||||
"oauth_consent_url": (
|
||||
"https://console.cloud.google.com/apis/credentials/consent"
|
||||
),
|
||||
"more_info_url": (
|
||||
"https://www.home-assistant.io/integrations/google_assistant_sdk/"
|
||||
),
|
||||
"oauth_creds_url": "https://console.cloud.google.com/apis/credentials",
|
||||
"redirect_url": redirect_url,
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
}
|
||||
},
|
||||
"application_credentials": {
|
||||
"description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Assistant SDK. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and select **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type.\n1. Add `{redirect_url}` under *Authorized redirect URI*."
|
||||
"description": "Follow the [instructions]({more_info_url}) for [OAuth consent screen]({oauth_consent_url}) to give Home Assistant access to your Google Assistant SDK. You also need to create Application Credentials linked to your account:\n1. Go to [Credentials]({oauth_creds_url}) and select **Create Credentials**.\n1. From the drop-down list select **OAuth client ID**.\n1. Select **Web application** for the Application Type."
|
||||
},
|
||||
"services": {
|
||||
"send_text_command": {
|
||||
|
||||
@@ -95,16 +95,21 @@ def get_recurrence_rule(recurrence: rrule) -> str:
|
||||
|
||||
'DTSTART:YYYYMMDDTHHMMSS\nRRULE:FREQ=YEARLY;INTERVAL=2'
|
||||
|
||||
Args:
|
||||
recurrence: An RRULE object.
|
||||
Parameters
|
||||
----------
|
||||
recurrence : rrule
|
||||
An RRULE object.
|
||||
|
||||
Returns:
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The recurrence rule portion of the RRULE string, starting with 'FREQ='.
|
||||
|
||||
Example:
|
||||
>>> rule = get_recurrence_rule(task)
|
||||
>>> print(rule)
|
||||
'FREQ=YEARLY;INTERVAL=2'
|
||||
Example
|
||||
-------
|
||||
>>> rule = get_recurrence_rule(task)
|
||||
>>> print(rule)
|
||||
'FREQ=YEARLY;INTERVAL=2'
|
||||
|
||||
"""
|
||||
return str(recurrence).split("RRULE:")[1]
|
||||
|
||||
@@ -64,7 +64,7 @@ def setup_bans(hass: HomeAssistant, app: Application, login_threshold: int) -> N
|
||||
"""Initialize bans when app starts up."""
|
||||
await app[KEY_BAN_MANAGER].async_load()
|
||||
|
||||
app.on_startup.append(ban_startup) # type: ignore[arg-type]
|
||||
app.on_startup.append(ban_startup)
|
||||
|
||||
|
||||
@middleware
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"binary_sensor": {
|
||||
"leaving_dock": {
|
||||
"default": "mdi:debug-step-out"
|
||||
},
|
||||
"returning_to_dock": {
|
||||
"default": "mdi:debug-step-into"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -45,26 +48,6 @@
|
||||
"work_area_progress": {
|
||||
"default": "mdi:collage"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"my_lawn_work_area": {
|
||||
"default": "mdi:square-outline",
|
||||
"state": {
|
||||
"on": "mdi:square"
|
||||
}
|
||||
},
|
||||
"work_area_work_area": {
|
||||
"default": "mdi:square-outline",
|
||||
"state": {
|
||||
"on": "mdi:square"
|
||||
}
|
||||
},
|
||||
"stay_out_zones": {
|
||||
"default": "mdi:rhombus-outline",
|
||||
"state": {
|
||||
"on": "mdi:rhombus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==1.2.0"]
|
||||
"requirements": ["aioautomower==1.0.1"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2025.7.0"]
|
||||
"requirements": ["pydrawise==2025.6.0"]
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity):
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the media player off."""
|
||||
await self._async_send_command(self._power_off_command)
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
self._attr_state = MediaPlayerState.STANDBY
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
@@ -159,5 +159,7 @@ class LookinMedia(LookinPowerPushRemoteEntity, MediaPlayerEntity):
|
||||
state = status[0]
|
||||
mute = status[2]
|
||||
|
||||
self._attr_state = MediaPlayerState.ON if state == "1" else MediaPlayerState.OFF
|
||||
self._attr_state = (
|
||||
MediaPlayerState.ON if state == "1" else MediaPlayerState.STANDBY
|
||||
)
|
||||
self._attr_is_volume_muted = mute == "0"
|
||||
|
||||
@@ -54,7 +54,7 @@ class MatterBinarySensor(MatterEntity, BinarySensorEntity):
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value in (None, NullValue):
|
||||
value = None
|
||||
elif value_convert := self.entity_description.device_to_ha:
|
||||
elif value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
if TYPE_CHECKING:
|
||||
value = cast(bool | None, value)
|
||||
@@ -70,7 +70,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="HueMotionSensor",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
device_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.OccupancySensing.Attributes.Occupancy,),
|
||||
@@ -83,7 +83,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="OccupancySensor",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
# The first bit = if occupied
|
||||
device_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
measurement_to_ha=lambda x: (x & 1 == 1) if x is not None else None,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.OccupancySensing.Attributes.Occupancy,),
|
||||
@@ -94,7 +94,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="BatteryChargeLevel",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_to_ha=lambda x: x
|
||||
measurement_to_ha=lambda x: x
|
||||
!= clusters.PowerSource.Enums.BatChargeLevelEnum.kOk,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
@@ -109,7 +109,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="ContactSensor",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
# value is inverted on matter to what we expect
|
||||
device_to_ha=lambda x: not x,
|
||||
measurement_to_ha=lambda x: not x,
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.BooleanState.Attributes.StateValue,),
|
||||
@@ -153,7 +153,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="LockDoorStateSensor",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorForcedOpen: True,
|
||||
@@ -168,7 +168,7 @@ DISCOVERY_SCHEMAS = [
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmDeviceMutedSensor",
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x == clusters.SmokeCoAlarm.Enums.MuteStateEnum.kMuted
|
||||
),
|
||||
translation_key="muted",
|
||||
@@ -181,7 +181,7 @@ DISCOVERY_SCHEMAS = [
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmEndfOfServiceSensor",
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x == clusters.SmokeCoAlarm.Enums.EndOfServiceEnum.kExpired
|
||||
),
|
||||
translation_key="end_of_service",
|
||||
@@ -195,7 +195,7 @@ DISCOVERY_SCHEMAS = [
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmBatteryAlertSensor",
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
translation_key="battery_alert",
|
||||
@@ -232,7 +232,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmSmokeStateSensor",
|
||||
device_class=BinarySensorDeviceClass.SMOKE,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
),
|
||||
@@ -244,7 +244,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmInterconnectSmokeAlarmSensor",
|
||||
device_class=BinarySensorDeviceClass.SMOKE,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
translation_key="interconnected_smoke_alarm",
|
||||
@@ -257,7 +257,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="SmokeCoAlarmInterconnectCOAlarmSensor",
|
||||
device_class=BinarySensorDeviceClass.CO,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x != clusters.SmokeCoAlarm.Enums.AlarmStateEnum.kNormal
|
||||
),
|
||||
translation_key="interconnected_co_alarm",
|
||||
@@ -271,7 +271,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EnergyEvseChargingStatusSensor",
|
||||
translation_key="evse_charging_status",
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn: False,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand: False,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDemand: False,
|
||||
@@ -291,7 +291,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EnergyEvsePlugStateSensor",
|
||||
translation_key="evse_plug_state",
|
||||
device_class=BinarySensorDeviceClass.PLUG,
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
clusters.EnergyEvse.Enums.StateEnum.kNotPluggedIn: False,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInNoDemand: True,
|
||||
clusters.EnergyEvse.Enums.StateEnum.kPluggedInDemand: True,
|
||||
@@ -311,7 +311,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EnergyEvseSupplyStateSensor",
|
||||
translation_key="evse_supply_charging_state",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
clusters.EnergyEvse.Enums.SupplyStateEnum.kDisabled: False,
|
||||
clusters.EnergyEvse.Enums.SupplyStateEnum.kChargingEnabled: True,
|
||||
clusters.EnergyEvse.Enums.SupplyStateEnum.kDischargingEnabled: False,
|
||||
@@ -327,7 +327,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="WaterHeaterManagementBoostStateSensor",
|
||||
translation_key="boost_state",
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive
|
||||
),
|
||||
),
|
||||
@@ -342,7 +342,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
# DeviceFault or SupplyFault bit enabled
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kDeviceFault: True,
|
||||
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSupplyFault: True,
|
||||
clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kSpeedLow: False,
|
||||
@@ -366,7 +366,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="PumpStatusRunning",
|
||||
translation_key="pump_running",
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x
|
||||
== clusters.PumpConfigurationAndControl.Bitmaps.PumpStatusBitmap.kRunning
|
||||
),
|
||||
@@ -384,7 +384,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="dishwasher_alarm_inflow",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x == clusters.DishwasherAlarm.Bitmaps.AlarmBitmap.kInflowError
|
||||
),
|
||||
),
|
||||
@@ -399,7 +399,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="dishwasher_alarm_door",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_to_ha=lambda x: (
|
||||
measurement_to_ha=lambda x: (
|
||||
x == clusters.DishwasherAlarm.Bitmaps.AlarmBitmap.kDoorError
|
||||
),
|
||||
),
|
||||
|
||||
@@ -59,8 +59,8 @@ class MatterEntityDescription(EntityDescription):
|
||||
"""Describe the Matter entity."""
|
||||
|
||||
# convert the value from the primary attribute to the value used by HA
|
||||
device_to_ha: Callable[[Any], Any] | None = None
|
||||
ha_to_device: Callable[[Any], Any] | None = None
|
||||
measurement_to_ha: Callable[[Any], Any] | None = None
|
||||
ha_to_native_value: Callable[[Any], Any] | None = None
|
||||
command_timeout: int | None = None
|
||||
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class MatterRangeNumberEntityDescription(
|
||||
):
|
||||
"""Describe Matter Number Input entities with min and max values."""
|
||||
|
||||
ha_to_device: Callable[[Any], Any]
|
||||
ha_to_native_value: Callable[[Any], Any]
|
||||
|
||||
# attribute descriptors to get the min and max value
|
||||
min_attribute: type[ClusterAttributeDescriptor]
|
||||
@@ -74,7 +74,7 @@ class MatterNumber(MatterEntity, NumberEntity):
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
sendvalue = int(value)
|
||||
if value_convert := self.entity_description.ha_to_device:
|
||||
if value_convert := self.entity_description.ha_to_native_value:
|
||||
sendvalue = value_convert(value)
|
||||
await self.write_attribute(
|
||||
value=sendvalue,
|
||||
@@ -84,7 +84,7 @@ class MatterNumber(MatterEntity, NumberEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
@@ -96,7 +96,7 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
send_value = self.entity_description.ha_to_device(value)
|
||||
send_value = self.entity_description.ha_to_native_value(value)
|
||||
# custom command defined to set the new value
|
||||
await self.send_device_command(
|
||||
self.entity_description.command(send_value),
|
||||
@@ -106,7 +106,7 @@ class MatterRangeNumber(MatterEntity, NumberEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
self._attr_native_min_value = (
|
||||
@@ -133,7 +133,7 @@ class MatterLevelControlNumber(MatterEntity, NumberEntity):
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set level value."""
|
||||
send_value = int(value)
|
||||
if value_convert := self.entity_description.ha_to_device:
|
||||
if value_convert := self.entity_description.ha_to_native_value:
|
||||
send_value = value_convert(value)
|
||||
await self.send_device_command(
|
||||
clusters.LevelControl.Commands.MoveToLevel(
|
||||
@@ -145,7 +145,7 @@ class MatterLevelControlNumber(MatterEntity, NumberEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
@@ -162,8 +162,8 @@ DISCOVERY_SCHEMAS = [
|
||||
native_min_value=0,
|
||||
mode=NumberMode.BOX,
|
||||
# use 255 to indicate that the value should revert to the default
|
||||
device_to_ha=lambda x: 255 if x is None else x,
|
||||
ha_to_device=lambda x: None if x == 255 else int(x),
|
||||
measurement_to_ha=lambda x: 255 if x is None else x,
|
||||
ha_to_native_value=lambda x: None if x == 255 else int(x),
|
||||
native_step=1,
|
||||
native_unit_of_measurement=None,
|
||||
),
|
||||
@@ -180,8 +180,8 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="on_transition_time",
|
||||
native_max_value=65534,
|
||||
native_min_value=0,
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
native_step=0.1,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
@@ -199,8 +199,8 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="off_transition_time",
|
||||
native_max_value=65534,
|
||||
native_min_value=0,
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
native_step=0.1,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
@@ -218,8 +218,8 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="on_off_transition_time",
|
||||
native_max_value=65534,
|
||||
native_min_value=0,
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
native_step=0.1,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
@@ -256,8 +256,8 @@ DISCOVERY_SCHEMAS = [
|
||||
native_min_value=-50,
|
||||
native_step=0.5,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_device=lambda x: round(x * 10),
|
||||
measurement_to_ha=lambda x: None if x is None else x / 10,
|
||||
ha_to_native_value=lambda x: round(x * 10),
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
@@ -275,10 +275,10 @@ DISCOVERY_SCHEMAS = [
|
||||
native_max_value=100,
|
||||
native_min_value=0.5,
|
||||
native_step=0.5,
|
||||
device_to_ha=(
|
||||
measurement_to_ha=(
|
||||
lambda x: None if x is None else x / 2 # Matter range (1-200)
|
||||
),
|
||||
ha_to_device=lambda x: round(x * 2), # HA range 0.5–100.0%
|
||||
ha_to_native_value=lambda x: round(x * 2), # HA range 0.5–100.0%
|
||||
mode=NumberMode.SLIDER,
|
||||
),
|
||||
entity_class=MatterLevelControlNumber,
|
||||
@@ -326,8 +326,8 @@ DISCOVERY_SCHEMAS = [
|
||||
targetTemperature=value
|
||||
),
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_to_ha=lambda x: None if x is None else x / 100,
|
||||
ha_to_device=lambda x: round(x * 100),
|
||||
measurement_to_ha=lambda x: None if x is None else x / 100,
|
||||
ha_to_native_value=lambda x: round(x * 100),
|
||||
min_attribute=clusters.TemperatureControl.Attributes.MinTemperature,
|
||||
max_attribute=clusters.TemperatureControl.Attributes.MaxTemperature,
|
||||
mode=NumberMode.SLIDER,
|
||||
|
||||
@@ -71,8 +71,8 @@ class MatterSelectEntityDescription(SelectEntityDescription, MatterEntityDescrip
|
||||
class MatterMapSelectEntityDescription(MatterSelectEntityDescription):
|
||||
"""Describe Matter select entities for MatterMapSelectEntityDescription."""
|
||||
|
||||
device_to_ha: Callable[[int], str | None]
|
||||
ha_to_device: Callable[[str], int | None]
|
||||
measurement_to_ha: Callable[[int], str | None]
|
||||
ha_to_native_value: Callable[[str], int | None]
|
||||
|
||||
# list attribute: the attribute descriptor to get the list of values (= list of integers)
|
||||
list_attribute: type[ClusterAttributeDescriptor]
|
||||
@@ -97,7 +97,7 @@ class MatterAttributeSelectEntity(MatterEntity, SelectEntity):
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected mode."""
|
||||
value_convert = self.entity_description.ha_to_device
|
||||
value_convert = self.entity_description.ha_to_native_value
|
||||
if TYPE_CHECKING:
|
||||
assert value_convert is not None
|
||||
await self.write_attribute(
|
||||
@@ -109,7 +109,7 @@ class MatterAttributeSelectEntity(MatterEntity, SelectEntity):
|
||||
"""Update from device."""
|
||||
value: Nullable | int | None
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
value_convert = self.entity_description.device_to_ha
|
||||
value_convert = self.entity_description.measurement_to_ha
|
||||
if TYPE_CHECKING:
|
||||
assert value_convert is not None
|
||||
self._attr_current_option = value_convert(value)
|
||||
@@ -132,7 +132,7 @@ class MatterMapSelectEntity(MatterAttributeSelectEntity):
|
||||
self._attr_options = [
|
||||
mapped_value
|
||||
for value in available_values
|
||||
if (mapped_value := self.entity_description.device_to_ha(value))
|
||||
if (mapped_value := self.entity_description.measurement_to_ha(value))
|
||||
]
|
||||
# use base implementation from MatterAttributeSelectEntity to set the current option
|
||||
super()._update_from_device()
|
||||
@@ -333,13 +333,13 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="startup_on_off",
|
||||
options=["on", "off", "toggle", "previous"],
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
0: "off",
|
||||
1: "on",
|
||||
2: "toggle",
|
||||
None: "previous",
|
||||
}.get,
|
||||
ha_to_device={
|
||||
ha_to_native_value={
|
||||
"off": 0,
|
||||
"on": 1,
|
||||
"toggle": 2,
|
||||
@@ -358,12 +358,12 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="sensitivity_level",
|
||||
options=["high", "standard", "low"],
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
0: "high",
|
||||
1: "standard",
|
||||
2: "low",
|
||||
}.get,
|
||||
ha_to_device={
|
||||
ha_to_native_value={
|
||||
"high": 0,
|
||||
"standard": 1,
|
||||
"low": 2,
|
||||
@@ -379,11 +379,11 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="temperature_display_mode",
|
||||
options=["Celsius", "Fahrenheit"],
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
0: "Celsius",
|
||||
1: "Fahrenheit",
|
||||
}.get,
|
||||
ha_to_device={
|
||||
ha_to_native_value={
|
||||
"Celsius": 0,
|
||||
"Fahrenheit": 1,
|
||||
}.get,
|
||||
@@ -432,8 +432,8 @@ DISCOVERY_SCHEMAS = [
|
||||
key="MatterLaundryWasherNumberOfRinses",
|
||||
translation_key="laundry_washer_number_of_rinses",
|
||||
list_attribute=clusters.LaundryWasherControls.Attributes.SupportedRinses,
|
||||
device_to_ha=NUMBER_OF_RINSES_STATE_MAP.get,
|
||||
ha_to_device=NUMBER_OF_RINSES_STATE_MAP_REVERSE.get,
|
||||
measurement_to_ha=NUMBER_OF_RINSES_STATE_MAP.get,
|
||||
ha_to_native_value=NUMBER_OF_RINSES_STATE_MAP_REVERSE.get,
|
||||
),
|
||||
entity_class=MatterMapSelectEntity,
|
||||
required_attributes=(
|
||||
@@ -450,13 +450,13 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="door_lock_sound_volume",
|
||||
options=["silent", "low", "medium", "high"],
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
0: "silent",
|
||||
1: "low",
|
||||
3: "medium",
|
||||
2: "high",
|
||||
}.get,
|
||||
ha_to_device={
|
||||
ha_to_native_value={
|
||||
"silent": 0,
|
||||
"low": 1,
|
||||
"medium": 3,
|
||||
@@ -472,8 +472,8 @@ DISCOVERY_SCHEMAS = [
|
||||
key="PumpConfigurationAndControlOperationMode",
|
||||
translation_key="pump_operation_mode",
|
||||
options=list(PUMP_OPERATION_MODE_MAP.values()),
|
||||
device_to_ha=PUMP_OPERATION_MODE_MAP.get,
|
||||
ha_to_device=PUMP_OPERATION_MODE_MAP_REVERSE.get,
|
||||
measurement_to_ha=PUMP_OPERATION_MODE_MAP.get,
|
||||
ha_to_native_value=PUMP_OPERATION_MODE_MAP_REVERSE.get,
|
||||
),
|
||||
entity_class=MatterAttributeSelectEntity,
|
||||
required_attributes=(
|
||||
|
||||
@@ -194,7 +194,7 @@ class MatterSensor(MatterEntity, SensorEntity):
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value in (None, NullValue):
|
||||
value = None
|
||||
elif value_convert := self.entity_description.device_to_ha:
|
||||
elif value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_native_value = value
|
||||
|
||||
@@ -296,7 +296,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="TemperatureSensor",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
device_to_ha=lambda x: x / 100,
|
||||
measurement_to_ha=lambda x: x / 100,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -308,7 +308,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="PressureSensor",
|
||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
device_to_ha=lambda x: x / 10,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -320,7 +320,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="FlowSensor",
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||
translation_key="flow",
|
||||
device_to_ha=lambda x: x / 10,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -332,7 +332,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="HumiditySensor",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
device_to_ha=lambda x: x / 100,
|
||||
measurement_to_ha=lambda x: x / 100,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -346,7 +346,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="LightSensor",
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
device_to_ha=lambda x: round(pow(10, ((x - 1) / 10000)), 1),
|
||||
measurement_to_ha=lambda x: round(pow(10, ((x - 1) / 10000)), 1),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -360,7 +360,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
# value has double precision
|
||||
device_to_ha=lambda x: int(x / 2),
|
||||
measurement_to_ha=lambda x: int(x / 2),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -402,7 +402,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[state for state in CHARGE_STATE_MAP.values() if state is not None],
|
||||
device_to_ha=CHARGE_STATE_MAP.get,
|
||||
measurement_to_ha=CHARGE_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.PowerSource.Attributes.BatChargeState,),
|
||||
@@ -589,7 +589,7 @@ DISCOVERY_SCHEMAS = [
|
||||
state_class=None,
|
||||
# convert to set first to remove the duplicate unknown value
|
||||
options=[x for x in AIR_QUALITY_MAP.values() if x is not None],
|
||||
device_to_ha=lambda x: AIR_QUALITY_MAP[x],
|
||||
measurement_to_ha=lambda x: AIR_QUALITY_MAP[x],
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.AirQuality.Attributes.AirQuality,),
|
||||
@@ -668,7 +668,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_to_ha=lambda x: x / 1000,
|
||||
measurement_to_ha=lambda x: x / 1000,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -685,7 +685,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
device_to_ha=lambda x: x / 1000,
|
||||
measurement_to_ha=lambda x: x / 1000,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -702,7 +702,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_to_ha=lambda x: x / 10,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(NeoCluster.Attributes.Watt,),
|
||||
@@ -731,7 +731,7 @@ DISCOVERY_SCHEMAS = [
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
suggested_display_precision=0,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_to_ha=lambda x: x / 10,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(NeoCluster.Attributes.Voltage,),
|
||||
@@ -823,7 +823,7 @@ DISCOVERY_SCHEMAS = [
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
# id 0 of the EnergyMeasurementStruct is the cumulative energy (in mWh)
|
||||
device_to_ha=lambda x: x.energy,
|
||||
measurement_to_ha=lambda x: x.energy,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -842,7 +842,7 @@ DISCOVERY_SCHEMAS = [
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
# id 0 of the EnergyMeasurementStruct is the cumulative energy (in mWh)
|
||||
device_to_ha=lambda x: x.energy,
|
||||
measurement_to_ha=lambda x: x.energy,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
@@ -910,7 +910,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="contamination_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=list(CONTAMINATION_STATE_MAP.values()),
|
||||
device_to_ha=CONTAMINATION_STATE_MAP.get,
|
||||
measurement_to_ha=CONTAMINATION_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.SmokeCoAlarm.Attributes.ContaminationState,),
|
||||
@@ -922,7 +922,7 @@ DISCOVERY_SCHEMAS = [
|
||||
translation_key="expiry_date",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
# raw value is epoch seconds
|
||||
device_to_ha=datetime.fromtimestamp,
|
||||
measurement_to_ha=datetime.fromtimestamp,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.SmokeCoAlarm.Attributes.ExpiryDate,),
|
||||
@@ -993,7 +993,7 @@ DISCOVERY_SCHEMAS = [
|
||||
key="ThermostatLocalTemperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
device_to_ha=lambda x: x / 100,
|
||||
measurement_to_ha=lambda x: x / 100,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -1044,7 +1044,7 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
translation_key="window_covering_target_position",
|
||||
device_to_ha=lambda x: round((10000 - x) / 100),
|
||||
measurement_to_ha=lambda x: round((10000 - x) / 100),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
@@ -1060,7 +1060,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(EVSE_FAULT_STATE_MAP.values()),
|
||||
device_to_ha=EVSE_FAULT_STATE_MAP.get,
|
||||
measurement_to_ha=EVSE_FAULT_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.EnergyEvse.Attributes.FaultState,),
|
||||
@@ -1173,7 +1173,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(ESA_STATE_MAP.values()),
|
||||
device_to_ha=ESA_STATE_MAP.get,
|
||||
measurement_to_ha=ESA_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DeviceEnergyManagement.Attributes.ESAState,),
|
||||
@@ -1186,7 +1186,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=list(DEM_OPT_OUT_STATE_MAP.values()),
|
||||
device_to_ha=DEM_OPT_OUT_STATE_MAP.get,
|
||||
measurement_to_ha=DEM_OPT_OUT_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DeviceEnergyManagement.Attributes.OptOutState,),
|
||||
@@ -1200,7 +1200,7 @@ DISCOVERY_SCHEMAS = [
|
||||
options=[
|
||||
mode for mode in PUMP_CONTROL_MODE_MAP.values() if mode is not None
|
||||
],
|
||||
device_to_ha=PUMP_CONTROL_MODE_MAP.get,
|
||||
measurement_to_ha=PUMP_CONTROL_MODE_MAP.get,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
|
||||
@@ -95,7 +95,7 @@ class MatterGenericCommandSwitch(MatterSwitch):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_is_on = value
|
||||
|
||||
@@ -141,7 +141,7 @@ class MatterNumericSwitch(MatterSwitch):
|
||||
|
||||
async def _async_set_native_value(self, value: bool) -> None:
|
||||
"""Update the current value."""
|
||||
if value_convert := self.entity_description.ha_to_device:
|
||||
if value_convert := self.entity_description.ha_to_native_value:
|
||||
send_value = value_convert(value)
|
||||
await self.write_attribute(
|
||||
value=send_value,
|
||||
@@ -159,7 +159,7 @@ class MatterNumericSwitch(MatterSwitch):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
value = self.get_matter_attribute_value(self._entity_info.primary_attribute)
|
||||
if value_convert := self.entity_description.device_to_ha:
|
||||
if value_convert := self.entity_description.measurement_to_ha:
|
||||
value = value_convert(value)
|
||||
self._attr_is_on = value
|
||||
|
||||
@@ -248,11 +248,11 @@ DISCOVERY_SCHEMAS = [
|
||||
key="EveTrvChildLock",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="child_lock",
|
||||
device_to_ha={
|
||||
measurement_to_ha={
|
||||
0: False,
|
||||
1: True,
|
||||
}.get,
|
||||
ha_to_device={
|
||||
ha_to_native_value={
|
||||
False: 0,
|
||||
True: 1,
|
||||
}.get,
|
||||
@@ -275,7 +275,7 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
off_command=clusters.EnergyEvse.Commands.Disable,
|
||||
command_timeout=3000,
|
||||
device_to_ha=EVSE_SUPPLY_STATE_MAP.get,
|
||||
measurement_to_ha=EVSE_SUPPLY_STATE_MAP.get,
|
||||
),
|
||||
entity_class=MatterGenericCommandSwitch,
|
||||
required_attributes=(
|
||||
|
||||
@@ -45,16 +45,6 @@ class MediaSourceItem:
|
||||
identifier: str
|
||||
target_media_player: str | None
|
||||
|
||||
@property
|
||||
def media_source_id(self) -> str:
|
||||
"""Return the media source ID."""
|
||||
uri = URI_SCHEME
|
||||
if self.domain:
|
||||
uri += self.domain
|
||||
if self.identifier:
|
||||
uri += f"/{self.identifier}"
|
||||
return uri
|
||||
|
||||
async def async_browse(self) -> BrowseMediaSource:
|
||||
"""Browse this item."""
|
||||
if self.domain is None:
|
||||
|
||||
@@ -134,7 +134,7 @@ class MediaroomDevice(MediaPlayerEntity):
|
||||
|
||||
state_map = {
|
||||
State.OFF: MediaPlayerState.OFF,
|
||||
State.STANDBY: MediaPlayerState.IDLE,
|
||||
State.STANDBY: MediaPlayerState.STANDBY,
|
||||
State.PLAYING_LIVE_TV: MediaPlayerState.PLAYING,
|
||||
State.PLAYING_RECORDED_TV: MediaPlayerState.PLAYING,
|
||||
State.PLAYING_TIMESHIFT_TV: MediaPlayerState.PLAYING,
|
||||
@@ -155,7 +155,7 @@ class MediaroomDevice(MediaPlayerEntity):
|
||||
self._channel = None
|
||||
self._optimistic = optimistic
|
||||
self._attr_state = (
|
||||
MediaPlayerState.PLAYING if optimistic else MediaPlayerState.IDLE
|
||||
MediaPlayerState.PLAYING if optimistic else MediaPlayerState.STANDBY
|
||||
)
|
||||
self._name = f"Mediaroom {device_id if device_id else host}"
|
||||
self._available = True
|
||||
@@ -254,7 +254,7 @@ class MediaroomDevice(MediaPlayerEntity):
|
||||
try:
|
||||
self.set_state(await self.stb.turn_off())
|
||||
if self._optimistic:
|
||||
self._attr_state = MediaPlayerState.IDLE
|
||||
self._attr_state = MediaPlayerState.STANDBY
|
||||
self._available = True
|
||||
except PyMediaroomError:
|
||||
self._available = False
|
||||
|
||||
@@ -6,24 +6,12 @@
|
||||
"utility": "Utility name",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"utility": "The name of your utility provider",
|
||||
"username": "The username for your utility account",
|
||||
"password": "The password for your utility account"
|
||||
}
|
||||
},
|
||||
"mfa": {
|
||||
"description": "The TOTP secret below is not one of the 6-digit time-based numeric codes. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation.",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"totp_secret": "TOTP secret"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::opower::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::opower::config::step::user::data_description::password%]",
|
||||
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -32,11 +20,6 @@
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"totp_secret": "[%key:component::opower::config::step::mfa::data::totp_secret%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::opower::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::opower::config::step::user::data_description::password%]",
|
||||
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,13 +6,7 @@ from collections.abc import Mapping
|
||||
import platform
|
||||
from typing import Any
|
||||
|
||||
from haphilipsjs import (
|
||||
DEFAULT_API_VERSION,
|
||||
ConnectionFailure,
|
||||
GeneralFailure,
|
||||
PairingFailure,
|
||||
PhilipsTV,
|
||||
)
|
||||
from haphilipsjs import ConnectionFailure, PairingFailure, PhilipsTV
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
@@ -24,18 +18,16 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import (
|
||||
CONF_API_VERSION,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PIN,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaFlowFormStep,
|
||||
SchemaOptionsFlowHandler,
|
||||
)
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from . import LOGGER
|
||||
from .const import CONF_ALLOW_NOTIFY, CONF_SYSTEM, CONST_APP_ID, CONST_APP_NAME, DOMAIN
|
||||
@@ -62,6 +54,21 @@ OPTIONS_FLOW = {
|
||||
}
|
||||
|
||||
|
||||
async def _validate_input(
|
||||
hass: HomeAssistant, host: str, api_version: int
|
||||
) -> PhilipsTV:
|
||||
"""Validate the user input allows us to connect."""
|
||||
hub = PhilipsTV(host, api_version)
|
||||
|
||||
await hub.getSystem()
|
||||
await hub.setTransport(hub.secured_transport)
|
||||
|
||||
if not hub.system:
|
||||
raise ConnectionFailure("System data is empty")
|
||||
|
||||
return hub
|
||||
|
||||
|
||||
class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Philips TV."""
|
||||
|
||||
@@ -74,38 +81,6 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._hub: PhilipsTV | None = None
|
||||
self._pair_state: Any = None
|
||||
|
||||
async def _async_attempt_prepare(
|
||||
self, host: str, api_version: int, secured_transport: bool
|
||||
) -> None:
|
||||
hub = PhilipsTV(
|
||||
host, api_version=api_version, secured_transport=secured_transport
|
||||
)
|
||||
|
||||
await hub.getSystem()
|
||||
await hub.setTransport(hub.secured_transport)
|
||||
|
||||
if not hub.system or not hub.name:
|
||||
raise ConnectionFailure("System data or name is empty")
|
||||
|
||||
self._hub = hub
|
||||
self._current[CONF_HOST] = host
|
||||
self._current[CONF_SYSTEM] = hub.system
|
||||
self._current[CONF_API_VERSION] = hub.api_version
|
||||
self.context.update({"title_placeholders": {CONF_NAME: hub.name}})
|
||||
|
||||
if serialnumber := hub.system.get("serialnumber"):
|
||||
await self.async_set_unique_id(serialnumber)
|
||||
if self.source != SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_configured(
|
||||
updates=self._current, reload_on_update=True
|
||||
)
|
||||
|
||||
async def _async_attempt_add(self) -> ConfigFlowResult:
|
||||
assert self._hub
|
||||
if self._hub.pairing_type == "digest_auth_pairing":
|
||||
return await self.async_step_pair()
|
||||
return await self._async_create_current()
|
||||
|
||||
async def _async_create_current(self) -> ConfigFlowResult:
|
||||
system = self._current[CONF_SYSTEM]
|
||||
if self.source == SOURCE_REAUTH:
|
||||
@@ -179,43 +154,6 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._current[CONF_API_VERSION] = entry_data[CONF_API_VERSION]
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle zeroconf discovery."""
|
||||
|
||||
LOGGER.debug(
|
||||
"Checking discovered device: {discovery_info.name} on {discovery_info.host}"
|
||||
)
|
||||
|
||||
secured_transport = discovery_info.type == "_philipstv_s_rpc._tcp.local."
|
||||
api_version = 6 if secured_transport else DEFAULT_API_VERSION
|
||||
|
||||
try:
|
||||
await self._async_attempt_prepare(
|
||||
discovery_info.host, api_version, secured_transport
|
||||
)
|
||||
except GeneralFailure:
|
||||
LOGGER.debug("Failed to get system info from discovery", exc_info=True)
|
||||
return self.async_abort(reason="discovery_failure")
|
||||
|
||||
return await self.async_step_zeroconf_confirm()
|
||||
|
||||
async def async_step_zeroconf_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by zeroconf."""
|
||||
if user_input is not None:
|
||||
return await self._async_attempt_add()
|
||||
|
||||
name = self.context.get("title_placeholders", {CONF_NAME: "Philips TV"})[
|
||||
CONF_NAME
|
||||
]
|
||||
return self.async_show_form(
|
||||
step_id="zeroconf_confirm",
|
||||
description_placeholders={CONF_NAME: name},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -224,14 +162,28 @@ class PhilipsJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input:
|
||||
self._current = user_input
|
||||
try:
|
||||
await self._async_attempt_prepare(
|
||||
user_input[CONF_HOST], user_input[CONF_API_VERSION], False
|
||||
hub = await _validate_input(
|
||||
self.hass, user_input[CONF_HOST], user_input[CONF_API_VERSION]
|
||||
)
|
||||
except GeneralFailure as exc:
|
||||
except ConnectionFailure as exc:
|
||||
LOGGER.error(exc)
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return await self._async_attempt_add()
|
||||
if serialnumber := hub.system.get("serialnumber"):
|
||||
await self.async_set_unique_id(serialnumber)
|
||||
if self.source != SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
self._current[CONF_SYSTEM] = hub.system
|
||||
self._current[CONF_API_VERSION] = hub.api_version
|
||||
self._hub = hub
|
||||
|
||||
if hub.pairing_type == "digest_auth_pairing":
|
||||
return await self.async_step_pair()
|
||||
return await self._async_create_current()
|
||||
|
||||
schema = self.add_suggested_values_to_schema(USER_SCHEMA, self._current)
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
|
||||
@@ -6,6 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/philips_js",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["haphilipsjs"],
|
||||
"requirements": ["ha-philipsjs==3.2.2"],
|
||||
"zeroconf": ["_philipstv_s_rpc._tcp.local.", "_philipstv_rpc._tcp.local."]
|
||||
"requirements": ["ha-philipsjs==3.2.2"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
@@ -8,10 +7,6 @@
|
||||
"api_version": "API Version"
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"title": "Discovered Philips TV",
|
||||
"description": "Do you want to add the TV ({name}) to Home Assistant? It will turn on and a pairing code will be displayed on it that you will need to enter in the next screen."
|
||||
},
|
||||
"pair": {
|
||||
"title": "Pair",
|
||||
"description": "Enter the PIN displayed on your TV",
|
||||
|
||||
@@ -191,7 +191,7 @@ class PS4Device(MediaPlayerEntity):
|
||||
)
|
||||
elif self.state != MediaPlayerState.IDLE:
|
||||
self.idle()
|
||||
elif self.state != MediaPlayerState.OFF:
|
||||
elif self.state != MediaPlayerState.STANDBY:
|
||||
self.state_standby()
|
||||
|
||||
elif self._retry > DEFAULT_RETRIES:
|
||||
@@ -223,7 +223,7 @@ class PS4Device(MediaPlayerEntity):
|
||||
def state_standby(self) -> None:
|
||||
"""Set states for state standby."""
|
||||
self.reset_title()
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
self._attr_state = MediaPlayerState.STANDBY
|
||||
|
||||
def state_unknown(self) -> None:
|
||||
"""Set states for state unknown."""
|
||||
|
||||
@@ -50,8 +50,6 @@ class RadioMediaSource(MediaSource):
|
||||
@property
|
||||
def radios(self) -> RadioBrowser:
|
||||
"""Return the radio browser."""
|
||||
if not hasattr(self.entry, "runtime_data") or self.entry.runtime_data is None:
|
||||
raise Unresolvable("Radio Browser integration not properly loaded")
|
||||
return self.entry.runtime_data
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
|
||||
@@ -142,7 +142,7 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the device."""
|
||||
if self.coordinator.data.state.standby:
|
||||
return MediaPlayerState.OFF
|
||||
return MediaPlayerState.STANDBY
|
||||
|
||||
if self.coordinator.data.app is None:
|
||||
return None
|
||||
@@ -308,21 +308,21 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity):
|
||||
@roku_exception_handler()
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
if self.state not in {MediaPlayerState.OFF, MediaPlayerState.PAUSED}:
|
||||
if self.state not in {MediaPlayerState.STANDBY, MediaPlayerState.PAUSED}:
|
||||
await self.coordinator.roku.remote("play")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler()
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
if self.state not in {MediaPlayerState.OFF, MediaPlayerState.PLAYING}:
|
||||
if self.state not in {MediaPlayerState.STANDBY, MediaPlayerState.PLAYING}:
|
||||
await self.coordinator.roku.remote("play")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@roku_exception_handler()
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Send play/pause command."""
|
||||
if self.state != MediaPlayerState.OFF:
|
||||
if self.state != MediaPlayerState.STANDBY:
|
||||
await self.coordinator.roku.remote("play")
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ async def async_setup_entry(
|
||||
if dev_id in event_entities:
|
||||
return
|
||||
# new player!
|
||||
event_entity = RoonEventEntity(roon_server, player_data, config_entry.entry_id)
|
||||
event_entity = RoonEventEntity(roon_server, player_data)
|
||||
event_entities.add(dev_id)
|
||||
async_add_entities([event_entity])
|
||||
|
||||
@@ -50,14 +50,13 @@ class RoonEventEntity(EventEntity):
|
||||
_attr_event_types = ["volume_up", "volume_down", "mute_toggle"]
|
||||
_attr_translation_key = "volume"
|
||||
|
||||
def __init__(self, server, player_data, entry_id):
|
||||
def __init__(self, server, player_data):
|
||||
"""Initialize the entity."""
|
||||
self._server = server
|
||||
self._player_data = player_data
|
||||
player_name = player_data["display_name"]
|
||||
self._attr_name = f"{player_name} roon volume"
|
||||
self._attr_unique_id = self._player_data["dev_id"]
|
||||
self._entry_id = entry_id
|
||||
|
||||
if self._player_data.get("source_controls"):
|
||||
dev_model = self._player_data["source_controls"][0].get("display_name")
|
||||
@@ -70,7 +69,7 @@ class RoonEventEntity(EventEntity):
|
||||
name=cast(str | None, self.name),
|
||||
manufacturer="RoonLabs",
|
||||
model=dev_model,
|
||||
via_device=(DOMAIN, self._entry_id),
|
||||
via_device=(DOMAIN, self._server.roon_id),
|
||||
)
|
||||
|
||||
def _roonapi_volume_callback(
|
||||
|
||||
@@ -72,7 +72,7 @@ async def async_setup_entry(
|
||||
dev_id = player_data["dev_id"]
|
||||
if dev_id not in media_players:
|
||||
# new player!
|
||||
media_player = RoonDevice(roon_server, player_data, config_entry.entry_id)
|
||||
media_player = RoonDevice(roon_server, player_data)
|
||||
media_players.add(dev_id)
|
||||
async_add_entities([media_player])
|
||||
else:
|
||||
@@ -106,7 +106,7 @@ class RoonDevice(MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
)
|
||||
|
||||
def __init__(self, server, player_data, entry_id):
|
||||
def __init__(self, server, player_data):
|
||||
"""Initialize Roon device object."""
|
||||
self._remove_signal_status = None
|
||||
self._server = server
|
||||
@@ -125,7 +125,6 @@ class RoonDevice(MediaPlayerEntity):
|
||||
self._attr_volume_level = 0
|
||||
self._volume_fixed = True
|
||||
self._volume_incremental = False
|
||||
self._entry_id = entry_id
|
||||
self.update_data(player_data)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -167,7 +166,7 @@ class RoonDevice(MediaPlayerEntity):
|
||||
name=cast(str | None, self.name),
|
||||
manufacturer="RoonLabs",
|
||||
model=dev_model,
|
||||
via_device=(DOMAIN, self._entry_id),
|
||||
via_device=(DOMAIN, self._server.roon_id),
|
||||
)
|
||||
|
||||
def update_data(self, player_data=None):
|
||||
|
||||
@@ -343,7 +343,7 @@ class SnapcastClientDevice(SnapcastBaseDevice):
|
||||
if self.is_volume_muted or self._current_group.muted:
|
||||
return MediaPlayerState.IDLE
|
||||
return STREAM_STATUS.get(self._current_group.stream_status)
|
||||
return MediaPlayerState.OFF
|
||||
return MediaPlayerState.STANDBY
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Mapping[str, Any]:
|
||||
|
||||
@@ -374,7 +374,9 @@ class TelegramNotificationService:
|
||||
}
|
||||
if data is not None:
|
||||
if ATTR_PARSER in data:
|
||||
params[ATTR_PARSER] = data[ATTR_PARSER]
|
||||
params[ATTR_PARSER] = self._parsers.get(
|
||||
data[ATTR_PARSER], self.parse_mode
|
||||
)
|
||||
if ATTR_TIMEOUT in data:
|
||||
params[ATTR_TIMEOUT] = data[ATTR_TIMEOUT]
|
||||
if ATTR_DISABLE_NOTIF in data:
|
||||
@@ -406,8 +408,6 @@ class TelegramNotificationService:
|
||||
params[ATTR_REPLYMARKUP] = InlineKeyboardMarkup(
|
||||
[_make_row_inline_keyboard(row) for row in keys]
|
||||
)
|
||||
if params[ATTR_PARSER] == PARSER_PLAIN_TEXT:
|
||||
params[ATTR_PARSER] = None
|
||||
return params
|
||||
|
||||
async def _send_msg(
|
||||
|
||||
@@ -159,6 +159,8 @@ class OptionsFlowHandler(OptionsFlow):
|
||||
"""Manage the options."""
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[ATTR_PARSER] == PARSER_PLAIN_TEXT:
|
||||
user_input[ATTR_PARSER] = None
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -109,7 +109,6 @@ send_photo:
|
||||
- "markdown"
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
disable_notification:
|
||||
selector:
|
||||
boolean:
|
||||
@@ -262,7 +261,6 @@ send_animation:
|
||||
- "markdown"
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
disable_notification:
|
||||
selector:
|
||||
boolean:
|
||||
@@ -343,7 +341,6 @@ send_video:
|
||||
- "markdown"
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
disable_notification:
|
||||
selector:
|
||||
boolean:
|
||||
@@ -496,7 +493,6 @@ send_document:
|
||||
- "markdown"
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
disable_notification:
|
||||
selector:
|
||||
boolean:
|
||||
@@ -674,7 +670,6 @@ edit_message:
|
||||
- "markdown"
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
disable_web_page_preview:
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
@@ -31,7 +31,7 @@ from .const import (
|
||||
EVENTS,
|
||||
LOGGER,
|
||||
)
|
||||
from .helpers import get_device, get_first_geofence, get_geofence_ids
|
||||
from .helpers import get_device, get_first_geofence
|
||||
|
||||
|
||||
class TraccarServerCoordinatorDataDevice(TypedDict):
|
||||
@@ -131,7 +131,7 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat
|
||||
"device": device,
|
||||
"geofence": get_first_geofence(
|
||||
geofences,
|
||||
get_geofence_ids(device, position),
|
||||
position["geofenceIds"] or [],
|
||||
),
|
||||
"position": position,
|
||||
"attributes": attr,
|
||||
@@ -187,7 +187,7 @@ class TraccarServerCoordinator(DataUpdateCoordinator[TraccarServerCoordinatorDat
|
||||
self.data[device_id]["attributes"] = attr
|
||||
self.data[device_id]["geofence"] = get_first_geofence(
|
||||
self._geofences,
|
||||
get_geofence_ids(self.data[device_id]["device"], position),
|
||||
position["geofenceIds"] or [],
|
||||
)
|
||||
update_devices.add(device_id)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pytraccar import DeviceModel, GeofenceModel, PositionModel
|
||||
from pytraccar import DeviceModel, GeofenceModel
|
||||
|
||||
|
||||
def get_device(device_id: int, devices: list[DeviceModel]) -> DeviceModel | None:
|
||||
@@ -22,17 +22,3 @@ def get_first_geofence(
|
||||
(geofence for geofence in geofences if geofence["id"] in target),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
def get_geofence_ids(
|
||||
device: DeviceModel,
|
||||
position: PositionModel,
|
||||
) -> list[int]:
|
||||
"""Compatibility helper to return a list of geofence IDs."""
|
||||
# For Traccar >=5.8 https://github.com/traccar/traccar/commit/30bafaed42e74863c5ca68a33c87f39d1e2de93d
|
||||
if "geofenceIds" in position:
|
||||
return position["geofenceIds"] or []
|
||||
# For Traccar <5.8
|
||||
if "geofenceIds" in device:
|
||||
return device["geofenceIds"] or []
|
||||
return []
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/venstar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["venstarcolortouch"],
|
||||
"requirements": ["venstarcolortouch==0.21"]
|
||||
"requirements": ["venstarcolortouch==0.19"]
|
||||
}
|
||||
|
||||
@@ -35,10 +35,6 @@ from homeassistant.exceptions import (
|
||||
Unauthorized,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, entity, template
|
||||
from homeassistant.helpers.condition import (
|
||||
async_get_all_descriptions as async_get_all_condition_descriptions,
|
||||
async_subscribe_platform_events as async_subscribe_condition_platform_events,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entityfilter import (
|
||||
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
|
||||
@@ -80,7 +76,6 @@ from . import const, decorators, messages
|
||||
from .connection import ActiveConnection
|
||||
from .messages import construct_event_message, construct_result_message
|
||||
|
||||
ALL_CONDITION_DESCRIPTIONS_JSON_CACHE = "websocket_api_all_condition_descriptions_json"
|
||||
ALL_SERVICE_DESCRIPTIONS_JSON_CACHE = "websocket_api_all_service_descriptions_json"
|
||||
ALL_TRIGGER_DESCRIPTIONS_JSON_CACHE = "websocket_api_all_trigger_descriptions_json"
|
||||
|
||||
@@ -106,7 +101,6 @@ def async_register_commands(
|
||||
async_reg(hass, handle_ping)
|
||||
async_reg(hass, handle_render_template)
|
||||
async_reg(hass, handle_subscribe_bootstrap_integrations)
|
||||
async_reg(hass, handle_subscribe_condition_platforms)
|
||||
async_reg(hass, handle_subscribe_events)
|
||||
async_reg(hass, handle_subscribe_trigger)
|
||||
async_reg(hass, handle_subscribe_trigger_platforms)
|
||||
@@ -507,53 +501,6 @@ def _send_handle_entities_init_response(
|
||||
)
|
||||
|
||||
|
||||
async def _async_get_all_condition_descriptions_json(hass: HomeAssistant) -> bytes:
|
||||
"""Return JSON of descriptions (i.e. user documentation) for all condition."""
|
||||
descriptions = await async_get_all_condition_descriptions(hass)
|
||||
if ALL_CONDITION_DESCRIPTIONS_JSON_CACHE in hass.data:
|
||||
cached_descriptions, cached_json_payload = hass.data[
|
||||
ALL_CONDITION_DESCRIPTIONS_JSON_CACHE
|
||||
]
|
||||
# If the descriptions are the same, return the cached JSON payload
|
||||
if cached_descriptions is descriptions:
|
||||
return cast(bytes, cached_json_payload)
|
||||
json_payload = json_bytes(
|
||||
{
|
||||
condition: description
|
||||
for condition, description in descriptions.items()
|
||||
if description is not None
|
||||
}
|
||||
)
|
||||
hass.data[ALL_CONDITION_DESCRIPTIONS_JSON_CACHE] = (descriptions, json_payload)
|
||||
return json_payload
|
||||
|
||||
|
||||
@decorators.websocket_command({vol.Required("type"): "condition_platforms/subscribe"})
|
||||
@decorators.async_response
|
||||
async def handle_subscribe_condition_platforms(
|
||||
hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Handle subscribe conditions command."""
|
||||
|
||||
async def on_new_conditions(new_conditions: set[str]) -> None:
|
||||
"""Forward new conditions to websocket."""
|
||||
descriptions = await async_get_all_condition_descriptions(hass)
|
||||
new_condition_descriptions = {}
|
||||
for condition in new_conditions:
|
||||
if (description := descriptions[condition]) is not None:
|
||||
new_condition_descriptions[condition] = description
|
||||
if not new_condition_descriptions:
|
||||
return
|
||||
connection.send_event(msg["id"], new_condition_descriptions)
|
||||
|
||||
connection.subscriptions[msg["id"]] = async_subscribe_condition_platform_events(
|
||||
hass, on_new_conditions
|
||||
)
|
||||
connection.send_result(msg["id"])
|
||||
conditions_json = await _async_get_all_condition_descriptions_json(hass)
|
||||
connection.send_message(construct_event_message(msg["id"], conditions_json))
|
||||
|
||||
|
||||
async def _async_get_all_service_descriptions_json(hass: HomeAssistant) -> bytes:
|
||||
"""Return JSON of descriptions (i.e. user documentation) for all service calls."""
|
||||
descriptions = await async_get_all_service_descriptions(hass)
|
||||
|
||||
Generated
-10
@@ -771,16 +771,6 @@ ZEROCONF = {
|
||||
"domain": "onewire",
|
||||
},
|
||||
],
|
||||
"_philipstv_rpc._tcp.local.": [
|
||||
{
|
||||
"domain": "philips_js",
|
||||
},
|
||||
],
|
||||
"_philipstv_s_rpc._tcp.local.": [
|
||||
{
|
||||
"domain": "philips_js",
|
||||
},
|
||||
],
|
||||
"_plexmediasvr._tcp.local.": [
|
||||
{
|
||||
"domain": "plex",
|
||||
|
||||
@@ -5,17 +5,19 @@ from __future__ import annotations
|
||||
import abc
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from collections.abc import Callable, Container, Coroutine, Generator, Iterable
|
||||
from collections.abc import Callable, Container, Generator
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, time as dt_time, timedelta
|
||||
import functools as ft
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any, Protocol, cast
|
||||
from typing import Any, Protocol, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zone as zone_cmp
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_GPS_ACCURACY,
|
||||
@@ -52,20 +54,11 @@ from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
TemplateError,
|
||||
)
|
||||
from homeassistant.loader import (
|
||||
Integration,
|
||||
IntegrationNotFound,
|
||||
async_get_integration,
|
||||
async_get_integrations,
|
||||
)
|
||||
from homeassistant.loader import IntegrationNotFound, async_get_integration
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.async_ import run_callback_threadsafe
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.yaml import load_yaml_dict
|
||||
from homeassistant.util.yaml.loader import JSON_TYPE
|
||||
|
||||
from . import config_validation as cv, entity_registry as er
|
||||
from .integration_platform import async_process_integration_platforms
|
||||
from .template import Template, render_complex
|
||||
from .trace import (
|
||||
TraceElement,
|
||||
@@ -83,8 +76,6 @@ ASYNC_FROM_CONFIG_FORMAT = "async_{}_from_config"
|
||||
FROM_CONFIG_FORMAT = "{}_from_config"
|
||||
VALIDATE_CONFIG_FORMAT = "{}_validate_config"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
_PLATFORM_ALIASES: dict[str | None, str | None] = {
|
||||
"and": None,
|
||||
"device": "device_automation",
|
||||
@@ -103,99 +94,6 @@ INPUT_ENTITY_ID = re.compile(
|
||||
)
|
||||
|
||||
|
||||
CONDITION_DESCRIPTION_CACHE: HassKey[dict[str, dict[str, Any] | None]] = HassKey(
|
||||
"condition_description_cache"
|
||||
)
|
||||
CONDITION_PLATFORM_SUBSCRIPTIONS: HassKey[
|
||||
list[Callable[[set[str]], Coroutine[Any, Any, None]]]
|
||||
] = HassKey("condition_platform_subscriptions")
|
||||
CONDITIONS: HassKey[dict[str, str]] = HassKey("conditions")
|
||||
|
||||
|
||||
# Basic schemas to sanity check the condition descriptions,
|
||||
# full validation is done by hassfest.conditions
|
||||
_FIELD_SCHEMA = vol.Schema(
|
||||
{},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
_CONDITION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("fields"): vol.Schema({str: _FIELD_SCHEMA}),
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def starts_with_dot(key: str) -> str:
|
||||
"""Check if key starts with dot."""
|
||||
if not key.startswith("."):
|
||||
raise vol.Invalid("Key does not start with .")
|
||||
return key
|
||||
|
||||
|
||||
_CONDITIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Remove(vol.All(str, starts_with_dot)): object,
|
||||
cv.slug: vol.Any(None, _CONDITION_SCHEMA),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the condition helper."""
|
||||
hass.data[CONDITION_DESCRIPTION_CACHE] = {}
|
||||
hass.data[CONDITION_PLATFORM_SUBSCRIPTIONS] = []
|
||||
hass.data[CONDITIONS] = {}
|
||||
await async_process_integration_platforms(
|
||||
hass, "condition", _register_condition_platform, wait_for_platforms=True
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_subscribe_platform_events(
|
||||
hass: HomeAssistant,
|
||||
on_event: Callable[[set[str]], Coroutine[Any, Any, None]],
|
||||
) -> Callable[[], None]:
|
||||
"""Subscribe to condition platform events."""
|
||||
condition_platform_event_subscriptions = hass.data[CONDITION_PLATFORM_SUBSCRIPTIONS]
|
||||
|
||||
def remove_subscription() -> None:
|
||||
condition_platform_event_subscriptions.remove(on_event)
|
||||
|
||||
condition_platform_event_subscriptions.append(on_event)
|
||||
return remove_subscription
|
||||
|
||||
|
||||
async def _register_condition_platform(
|
||||
hass: HomeAssistant, integration_domain: str, platform: ConditionProtocol
|
||||
) -> None:
|
||||
"""Register a condition platform."""
|
||||
|
||||
new_conditions: set[str] = set()
|
||||
|
||||
if hasattr(platform, "async_get_conditions"):
|
||||
for condition_key in await platform.async_get_conditions(hass):
|
||||
hass.data[CONDITIONS][condition_key] = integration_domain
|
||||
new_conditions.add(condition_key)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Integration %s does not provide condition support, skipping",
|
||||
integration_domain,
|
||||
)
|
||||
return
|
||||
|
||||
# We don't use gather here because gather adds additional overhead
|
||||
# when wrapping each coroutine in a task, and we expect our listeners
|
||||
# to call condition.async_get_all_descriptions which will only yield
|
||||
# the first time it's called, after that it returns cached data.
|
||||
for listener in hass.data[CONDITION_PLATFORM_SUBSCRIPTIONS]:
|
||||
try:
|
||||
await listener(new_conditions)
|
||||
except Exception:
|
||||
_LOGGER.exception("Error while notifying condition platform listener")
|
||||
|
||||
|
||||
class Condition(abc.ABC):
|
||||
"""Condition class."""
|
||||
|
||||
@@ -819,8 +717,6 @@ def time(
|
||||
for the opposite. "(23:59 <= now < 00:01)" would be the same as
|
||||
"not (00:01 <= now < 23:59)".
|
||||
"""
|
||||
from homeassistant.components.sensor import SensorDeviceClass # noqa: PLC0415
|
||||
|
||||
now = dt_util.now()
|
||||
now_time = now.time()
|
||||
|
||||
@@ -928,8 +824,6 @@ def zone(
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
from homeassistant.components import zone as zone_cmp # noqa: PLC0415
|
||||
|
||||
if zone_ent is None:
|
||||
raise ConditionErrorMessage("zone", "no zone specified")
|
||||
|
||||
@@ -1186,109 +1080,3 @@ def async_extract_devices(config: ConfigType | Template) -> set[str]:
|
||||
referenced.add(device_id)
|
||||
|
||||
return referenced
|
||||
|
||||
|
||||
def _load_conditions_file(hass: HomeAssistant, integration: Integration) -> JSON_TYPE:
|
||||
"""Load conditions file for an integration."""
|
||||
try:
|
||||
return cast(
|
||||
JSON_TYPE,
|
||||
_CONDITIONS_SCHEMA(
|
||||
load_yaml_dict(str(integration.file_path / "conditions.yaml"))
|
||||
),
|
||||
)
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning(
|
||||
"Unable to find conditions.yaml for the %s integration", integration.domain
|
||||
)
|
||||
return {}
|
||||
except (HomeAssistantError, vol.Invalid) as ex:
|
||||
_LOGGER.warning(
|
||||
"Unable to parse conditions.yaml for the %s integration: %s",
|
||||
integration.domain,
|
||||
ex,
|
||||
)
|
||||
return {}
|
||||
|
||||
|
||||
def _load_conditions_files(
|
||||
hass: HomeAssistant, integrations: Iterable[Integration]
|
||||
) -> dict[str, JSON_TYPE]:
|
||||
"""Load condition files for multiple integrations."""
|
||||
return {
|
||||
integration.domain: _load_conditions_file(hass, integration)
|
||||
for integration in integrations
|
||||
}
|
||||
|
||||
|
||||
async def async_get_all_descriptions(
|
||||
hass: HomeAssistant,
|
||||
) -> dict[str, dict[str, Any] | None]:
|
||||
"""Return descriptions (i.e. user documentation) for all conditions."""
|
||||
descriptions_cache = hass.data[CONDITION_DESCRIPTION_CACHE]
|
||||
|
||||
conditions = hass.data[CONDITIONS]
|
||||
# See if there are new conditions not seen before.
|
||||
# Any condition that we saw before already has an entry in description_cache.
|
||||
all_conditions = set(conditions)
|
||||
previous_all_conditions = set(descriptions_cache)
|
||||
# If the conditions are the same, we can return the cache
|
||||
if previous_all_conditions == all_conditions:
|
||||
return descriptions_cache
|
||||
|
||||
# Files we loaded for missing descriptions
|
||||
new_conditions_descriptions: dict[str, JSON_TYPE] = {}
|
||||
# We try to avoid making a copy in the event the cache is good,
|
||||
# but now we must make a copy in case new conditions get added
|
||||
# while we are loading the missing ones so we do not
|
||||
# add the new ones to the cache without their descriptions
|
||||
conditions = conditions.copy()
|
||||
|
||||
if missing_conditions := all_conditions.difference(descriptions_cache):
|
||||
domains_with_missing_conditions = {
|
||||
conditions[missing_condition] for missing_condition in missing_conditions
|
||||
}
|
||||
ints_or_excs = await async_get_integrations(
|
||||
hass, domains_with_missing_conditions
|
||||
)
|
||||
integrations: list[Integration] = []
|
||||
for domain, int_or_exc in ints_or_excs.items():
|
||||
if type(int_or_exc) is Integration and int_or_exc.has_conditions:
|
||||
integrations.append(int_or_exc)
|
||||
continue
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(int_or_exc, Exception)
|
||||
_LOGGER.debug(
|
||||
"Failed to load conditions.yaml for integration: %s",
|
||||
domain,
|
||||
exc_info=int_or_exc,
|
||||
)
|
||||
|
||||
if integrations:
|
||||
new_conditions_descriptions = await hass.async_add_executor_job(
|
||||
_load_conditions_files, hass, integrations
|
||||
)
|
||||
|
||||
# Make a copy of the old cache and add missing descriptions to it
|
||||
new_descriptions_cache = descriptions_cache.copy()
|
||||
for missing_condition in missing_conditions:
|
||||
domain = conditions[missing_condition]
|
||||
|
||||
if (
|
||||
yaml_description := new_conditions_descriptions.get(domain, {}).get( # type: ignore[union-attr]
|
||||
missing_condition
|
||||
)
|
||||
) is None:
|
||||
_LOGGER.debug(
|
||||
"No condition descriptions found for condition %s, skipping",
|
||||
missing_condition,
|
||||
)
|
||||
new_descriptions_cache[missing_condition] = None
|
||||
continue
|
||||
|
||||
description = {"fields": yaml_description.get("fields", {})}
|
||||
|
||||
new_descriptions_cache[missing_condition] = description
|
||||
|
||||
hass.data[CONDITION_DESCRIPTION_CACHE] = new_descriptions_cache
|
||||
return new_descriptions_cache
|
||||
|
||||
@@ -1537,6 +1537,22 @@ def STATE_CONDITION_SCHEMA(value: Any) -> dict[str, Any]:
|
||||
return key_dependency("for", "state")(validated)
|
||||
|
||||
|
||||
SUN_CONDITION_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
**CONDITION_BASE_SCHEMA,
|
||||
vol.Required(CONF_CONDITION): "sun",
|
||||
vol.Optional("before"): sun_event,
|
||||
vol.Optional("before_offset"): time_period,
|
||||
vol.Optional("after"): vol.All(
|
||||
vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)
|
||||
),
|
||||
vol.Optional("after_offset"): time_period,
|
||||
}
|
||||
),
|
||||
has_at_least_one_key("before", "after"),
|
||||
)
|
||||
|
||||
TEMPLATE_CONDITION_SCHEMA = vol.Schema(
|
||||
{
|
||||
**CONDITION_BASE_SCHEMA,
|
||||
|
||||
@@ -866,17 +866,19 @@ def async_track_state_change_filtered(
|
||||
) -> _TrackStateChangeFiltered:
|
||||
"""Track state changes with a TrackStates filter that can be updated.
|
||||
|
||||
Args:
|
||||
hass:
|
||||
Home assistant object.
|
||||
track_states:
|
||||
A TrackStates data class.
|
||||
action:
|
||||
Callable to call with results.
|
||||
Parameters
|
||||
----------
|
||||
hass
|
||||
Home assistant object.
|
||||
track_states
|
||||
A TrackStates data class.
|
||||
action
|
||||
Callable to call with results.
|
||||
|
||||
Returns:
|
||||
Object used to update the listeners (async_update_listeners) with a new
|
||||
TrackStates or cancel the tracking (async_remove).
|
||||
Returns
|
||||
-------
|
||||
Object used to update the listeners (async_update_listeners) with a new
|
||||
TrackStates or cancel the tracking (async_remove).
|
||||
|
||||
"""
|
||||
tracker = _TrackStateChangeFiltered(hass, track_states, action)
|
||||
@@ -905,26 +907,29 @@ def async_track_template(
|
||||
exception, the listener will still be registered but will only
|
||||
fire if the template result becomes true without an exception.
|
||||
|
||||
Action args:
|
||||
entity_id:
|
||||
ID of the entity that triggered the state change.
|
||||
old_state:
|
||||
The old state of the entity that changed.
|
||||
new_state:
|
||||
New state of the entity that changed.
|
||||
Action arguments
|
||||
----------------
|
||||
entity_id
|
||||
ID of the entity that triggered the state change.
|
||||
old_state
|
||||
The old state of the entity that changed.
|
||||
new_state
|
||||
New state of the entity that changed.
|
||||
|
||||
Args:
|
||||
hass:
|
||||
Home assistant object.
|
||||
template:
|
||||
The template to calculate.
|
||||
action:
|
||||
Callable to call with results. See above for arguments.
|
||||
variables:
|
||||
Variables to pass to the template.
|
||||
Parameters
|
||||
----------
|
||||
hass
|
||||
Home assistant object.
|
||||
template
|
||||
The template to calculate.
|
||||
action
|
||||
Callable to call with results. See above for arguments.
|
||||
variables
|
||||
Variables to pass to the template.
|
||||
|
||||
Returns:
|
||||
Callable to unregister the listener.
|
||||
Returns
|
||||
-------
|
||||
Callable to unregister the listener.
|
||||
|
||||
"""
|
||||
job = HassJob(action, f"track template {template}")
|
||||
@@ -1356,24 +1361,26 @@ def async_track_template_result(
|
||||
Once the template returns to a non-error condition the result is sent
|
||||
to the action as usual.
|
||||
|
||||
Args:
|
||||
hass:
|
||||
Home assistant object.
|
||||
track_templates:
|
||||
An iterable of TrackTemplate.
|
||||
action:
|
||||
Callable to call with results.
|
||||
strict:
|
||||
When set to True, raise on undefined variables.
|
||||
log_fn:
|
||||
If not None, template error messages will logging by calling log_fn
|
||||
instead of the normal logging facility.
|
||||
has_super_template:
|
||||
When set to True, the first template will block rendering of other
|
||||
templates if it doesn't render as True.
|
||||
Parameters
|
||||
----------
|
||||
hass
|
||||
Home assistant object.
|
||||
track_templates
|
||||
An iterable of TrackTemplate.
|
||||
action
|
||||
Callable to call with results.
|
||||
strict
|
||||
When set to True, raise on undefined variables.
|
||||
log_fn
|
||||
If not None, template error messages will logging by calling log_fn
|
||||
instead of the normal logging facility.
|
||||
has_super_template
|
||||
When set to True, the first template will block rendering of other
|
||||
templates if it doesn't render as True.
|
||||
|
||||
Returns:
|
||||
Info object used to unregister the listener, and refresh the template.
|
||||
Returns
|
||||
-------
|
||||
Info object used to unregister the listener, and refresh the template.
|
||||
|
||||
"""
|
||||
tracker = TrackTemplateResultInfo(hass, track_templates, action, has_super_template)
|
||||
|
||||
@@ -193,11 +193,6 @@ def report_usage(
|
||||
exclude_integrations=exclude_integrations
|
||||
)
|
||||
except MissingIntegrationFrame as err:
|
||||
# We need to be careful with assigning the error here as it affects the
|
||||
# cleanup of objects referenced from the stack trace as seen in
|
||||
# https://github.com/home-assistant/core/pull/148021#discussion_r2182379834
|
||||
# When core_behavior is ReportBehavior.ERROR, we will re-raise the error,
|
||||
# so we can safely assign it to integration_frame_err.
|
||||
if core_behavior is ReportBehavior.ERROR:
|
||||
integration_frame_err = err
|
||||
_report_usage_partial = functools.partial(
|
||||
|
||||
@@ -1066,7 +1066,6 @@ class NumberSelectorConfig(BaseSelectorConfig, total=False):
|
||||
step: float | Literal["any"]
|
||||
unit_of_measurement: str
|
||||
mode: NumberSelectorMode
|
||||
translation_key: str
|
||||
|
||||
|
||||
class NumberSelectorMode(StrEnum):
|
||||
@@ -1107,7 +1106,6 @@ class NumberSelector(Selector[NumberSelectorConfig]):
|
||||
vol.Optional(CONF_MODE, default=NumberSelectorMode.SLIDER): vol.All(
|
||||
vol.Coerce(NumberSelectorMode), lambda val: val.value
|
||||
),
|
||||
vol.Optional("translation_key"): str,
|
||||
}
|
||||
),
|
||||
validate_slider,
|
||||
|
||||
@@ -67,7 +67,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
#
|
||||
BASE_PRELOAD_PLATFORMS = [
|
||||
"backup",
|
||||
"condition",
|
||||
"config",
|
||||
"config_flow",
|
||||
"diagnostics",
|
||||
@@ -858,11 +857,6 @@ class Integration:
|
||||
# True.
|
||||
return self.manifest.get("import_executor", True)
|
||||
|
||||
@cached_property
|
||||
def has_conditions(self) -> bool:
|
||||
"""Return if the integration has conditions."""
|
||||
return "conditions.yaml" in self._top_level_files
|
||||
|
||||
@cached_property
|
||||
def has_services(self) -> bool:
|
||||
"""Return if the integration has services."""
|
||||
|
||||
@@ -38,7 +38,7 @@ habluetooth==3.49.0
|
||||
hass-nabucasa==0.105.0
|
||||
hassil==2.2.3
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250702.1
|
||||
home-assistant-frontend==20250702.0
|
||||
home-assistant-intents==2025.6.23
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
|
||||
@@ -3566,16 +3566,6 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.opower.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.oralb.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
||||
@@ -913,5 +913,4 @@ split-on-trailing-comma = false
|
||||
max-complexity = 25
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "google"
|
||||
property-decorators = ["propcache.api.cached_property"]
|
||||
|
||||
Generated
+5
-5
@@ -185,7 +185,7 @@ aioairzone-cloud==0.6.12
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.2.3
|
||||
aioamazondevices==3.2.2
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -204,7 +204,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==1.2.0
|
||||
aioautomower==1.0.1
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.1
|
||||
@@ -1168,7 +1168,7 @@ hole==0.8.0
|
||||
holidays==0.75
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250702.1
|
||||
home-assistant-frontend==20250702.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.6.23
|
||||
@@ -1929,7 +1929,7 @@ pydiscovergy==3.0.2
|
||||
pydoods==1.0.2
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
pydrawise==2025.7.0
|
||||
pydrawise==2025.6.0
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==3.0.0
|
||||
@@ -3041,7 +3041,7 @@ vehicle==2.2.2
|
||||
velbus-aio==2025.5.0
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.21
|
||||
venstarcolortouch==0.19
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.5.0
|
||||
|
||||
Generated
+5
-5
@@ -173,7 +173,7 @@ aioairzone-cloud==0.6.12
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
aioamazondevices==3.2.3
|
||||
aioamazondevices==3.2.2
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
# homeassistant.components.ambient_station
|
||||
@@ -192,7 +192,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==1.2.0
|
||||
aioautomower==1.0.1
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.1
|
||||
@@ -1017,7 +1017,7 @@ hole==0.8.0
|
||||
holidays==0.75
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250702.1
|
||||
home-assistant-frontend==20250702.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.6.23
|
||||
@@ -1610,7 +1610,7 @@ pydexcom==0.2.3
|
||||
pydiscovergy==3.0.2
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
pydrawise==2025.7.0
|
||||
pydrawise==2025.6.0
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==3.0.0
|
||||
@@ -2509,7 +2509,7 @@ vehicle==2.2.2
|
||||
velbus-aio==2025.5.0
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.21
|
||||
venstarcolortouch==0.19
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.5.0
|
||||
|
||||
@@ -12,7 +12,6 @@ from . import (
|
||||
application_credentials,
|
||||
bluetooth,
|
||||
codeowners,
|
||||
conditions,
|
||||
config_flow,
|
||||
config_schema,
|
||||
dependencies,
|
||||
@@ -39,7 +38,6 @@ INTEGRATION_PLUGINS = [
|
||||
application_credentials,
|
||||
bluetooth,
|
||||
codeowners,
|
||||
conditions,
|
||||
config_schema,
|
||||
dependencies,
|
||||
dhcp,
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
"""Validate conditions."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import pathlib
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.const import CONF_SELECTOR
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import condition, config_validation as cv, selector
|
||||
from homeassistant.util.yaml import load_yaml_dict
|
||||
|
||||
from .model import Config, Integration
|
||||
|
||||
|
||||
def exists(value: Any) -> Any:
|
||||
"""Check if value exists."""
|
||||
if value is None:
|
||||
raise vol.Invalid("Value cannot be None")
|
||||
return value
|
||||
|
||||
|
||||
FIELD_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("example"): exists,
|
||||
vol.Optional("default"): exists,
|
||||
vol.Optional("required"): bool,
|
||||
vol.Optional(CONF_SELECTOR): selector.validate_selector,
|
||||
}
|
||||
)
|
||||
|
||||
CONDITION_SCHEMA = vol.Any(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional("fields"): vol.Schema({str: FIELD_SCHEMA}),
|
||||
}
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
CONDITIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Remove(vol.All(str, condition.starts_with_dot)): object,
|
||||
cv.slug: CONDITION_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
NON_MIGRATED_INTEGRATIONS = {
|
||||
"device_automation",
|
||||
"sun",
|
||||
}
|
||||
|
||||
|
||||
def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> bool:
|
||||
"""Recursively go through a dir and it's children and find the regex."""
|
||||
pattern = re.compile(search_pattern)
|
||||
|
||||
for fil in path.glob(glob_pattern):
|
||||
if not fil.is_file():
|
||||
continue
|
||||
|
||||
if pattern.search(fil.read_text()):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def validate_conditions(config: Config, integration: Integration) -> None: # noqa: C901
|
||||
"""Validate conditions."""
|
||||
try:
|
||||
data = load_yaml_dict(str(integration.path / "conditions.yaml"))
|
||||
except FileNotFoundError:
|
||||
# Find if integration uses conditions
|
||||
has_conditions = grep_dir(
|
||||
integration.path,
|
||||
"**/condition.py",
|
||||
r"async_get_conditions",
|
||||
)
|
||||
|
||||
if has_conditions and integration.domain not in NON_MIGRATED_INTEGRATIONS:
|
||||
integration.add_error(
|
||||
"conditions", "Registers conditions but has no conditions.yaml"
|
||||
)
|
||||
return
|
||||
except HomeAssistantError:
|
||||
integration.add_error("conditions", "Invalid conditions.yaml")
|
||||
return
|
||||
|
||||
try:
|
||||
conditions = CONDITIONS_SCHEMA(data)
|
||||
except vol.Invalid as err:
|
||||
integration.add_error(
|
||||
"conditions", f"Invalid conditions.yaml: {humanize_error(data, err)}"
|
||||
)
|
||||
return
|
||||
|
||||
icons_file = integration.path / "icons.json"
|
||||
icons = {}
|
||||
if icons_file.is_file():
|
||||
with contextlib.suppress(ValueError):
|
||||
icons = json.loads(icons_file.read_text())
|
||||
condition_icons = icons.get("conditions", {})
|
||||
|
||||
# Try loading translation strings
|
||||
if integration.core:
|
||||
strings_file = integration.path / "strings.json"
|
||||
else:
|
||||
# For custom integrations, use the en.json file
|
||||
strings_file = integration.path / "translations/en.json"
|
||||
|
||||
strings = {}
|
||||
if strings_file.is_file():
|
||||
with contextlib.suppress(ValueError):
|
||||
strings = json.loads(strings_file.read_text())
|
||||
|
||||
error_msg_suffix = "in the translations file"
|
||||
if not integration.core:
|
||||
error_msg_suffix = f"and is not {error_msg_suffix}"
|
||||
|
||||
# For each condition in the integration:
|
||||
# 1. Check if the condition description is set, if not,
|
||||
# check if it's in the strings file else add an error.
|
||||
# 2. Check if the condition has an icon set in icons.json.
|
||||
# raise an error if not.,
|
||||
for condition_name, condition_schema in conditions.items():
|
||||
if integration.core and condition_name not in condition_icons:
|
||||
# This is enforced for Core integrations only
|
||||
integration.add_error(
|
||||
"conditions",
|
||||
f"Condition {condition_name} has no icon in icons.json.",
|
||||
)
|
||||
if condition_schema is None:
|
||||
continue
|
||||
if "name" not in condition_schema and integration.core:
|
||||
try:
|
||||
strings["conditions"][condition_name]["name"]
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"conditions",
|
||||
f"Condition {condition_name} has no name {error_msg_suffix}",
|
||||
)
|
||||
|
||||
if "description" not in condition_schema and integration.core:
|
||||
try:
|
||||
strings["conditions"][condition_name]["description"]
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"conditions",
|
||||
f"Condition {condition_name} has no description {error_msg_suffix}",
|
||||
)
|
||||
|
||||
# The same check is done for the description in each of the fields of the
|
||||
# condition schema.
|
||||
for field_name, field_schema in condition_schema.get("fields", {}).items():
|
||||
if "fields" in field_schema:
|
||||
# This is a section
|
||||
continue
|
||||
if "name" not in field_schema and integration.core:
|
||||
try:
|
||||
strings["conditions"][condition_name]["fields"][field_name]["name"]
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"conditions",
|
||||
(
|
||||
f"Condition {condition_name} has a field {field_name} with no "
|
||||
f"name {error_msg_suffix}"
|
||||
),
|
||||
)
|
||||
|
||||
if "description" not in field_schema and integration.core:
|
||||
try:
|
||||
strings["conditions"][condition_name]["fields"][field_name][
|
||||
"description"
|
||||
]
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"conditions",
|
||||
(
|
||||
f"Condition {condition_name} has a field {field_name} with no "
|
||||
f"description {error_msg_suffix}"
|
||||
),
|
||||
)
|
||||
|
||||
if "selector" in field_schema:
|
||||
with contextlib.suppress(KeyError):
|
||||
translation_key = field_schema["selector"]["select"][
|
||||
"translation_key"
|
||||
]
|
||||
try:
|
||||
strings["selector"][translation_key]
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"conditions",
|
||||
f"Condition {condition_name} has a field {field_name} with a selector with a translation key {translation_key} that is not in the translations file",
|
||||
)
|
||||
|
||||
# The same check is done for the description in each of the sections of the
|
||||
# condition schema.
|
||||
for section_name, section_schema in condition_schema.get("fields", {}).items():
|
||||
if "fields" not in section_schema:
|
||||
# This is not a section
|
||||
continue
|
||||
if "name" not in section_schema and integration.core:
|
||||
try:
|
||||
strings["conditions"][condition_name]["sections"][section_name][
|
||||
"name"
|
||||
]
|
||||
except KeyError:
|
||||
integration.add_error(
|
||||
"conditions",
|
||||
f"Condition {condition_name} has a section {section_name} with no name {error_msg_suffix}",
|
||||
)
|
||||
|
||||
|
||||
def validate(integrations: dict[str, Integration], config: Config) -> None:
|
||||
"""Handle dependencies for integrations."""
|
||||
# check conditions.yaml is valid
|
||||
for integration in integrations.values():
|
||||
validate_conditions(config, integration)
|
||||
@@ -120,16 +120,6 @@ CUSTOM_INTEGRATION_SERVICE_ICONS_SCHEMA = cv.schema_with_slug_keys(
|
||||
)
|
||||
|
||||
|
||||
CONDITION_ICONS_SCHEMA = cv.schema_with_slug_keys(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional("condition"): icon_value_validator,
|
||||
}
|
||||
),
|
||||
slug_validator=translation_key_validator,
|
||||
)
|
||||
|
||||
|
||||
TRIGGER_ICONS_SCHEMA = cv.schema_with_slug_keys(
|
||||
vol.Schema(
|
||||
{
|
||||
@@ -176,7 +166,6 @@ def icon_schema(
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Optional("conditions"): CONDITION_ICONS_SCHEMA,
|
||||
vol.Optional("config"): DATA_ENTRY_ICONS_SCHEMA,
|
||||
vol.Optional("issues"): vol.Schema(
|
||||
{str: {"fix_flow": DATA_ENTRY_ICONS_SCHEMA}}
|
||||
|
||||
@@ -310,10 +310,6 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
|
||||
translation_value_validator,
|
||||
slug_validator=translation_key_validator,
|
||||
),
|
||||
vol.Optional("unit_of_measurement"): cv.schema_with_slug_keys(
|
||||
translation_value_validator,
|
||||
slug_validator=translation_key_validator,
|
||||
),
|
||||
vol.Optional("fields"): cv.schema_with_slug_keys(str),
|
||||
},
|
||||
slug_validator=vol.Any("_", cv.slug),
|
||||
@@ -420,22 +416,6 @@ def gen_strings_schema(config: Config, integration: Integration) -> vol.Schema:
|
||||
},
|
||||
slug_validator=translation_key_validator,
|
||||
),
|
||||
vol.Optional("conditions"): cv.schema_with_slug_keys(
|
||||
{
|
||||
vol.Required("name"): translation_value_validator,
|
||||
vol.Required("description"): translation_value_validator,
|
||||
vol.Required("description_configured"): translation_value_validator,
|
||||
vol.Optional("fields"): cv.schema_with_slug_keys(
|
||||
{
|
||||
vol.Required("name"): str,
|
||||
vol.Required("description"): translation_value_validator,
|
||||
vol.Optional("example"): translation_value_validator,
|
||||
},
|
||||
slug_validator=translation_key_validator,
|
||||
),
|
||||
},
|
||||
slug_validator=translation_key_validator,
|
||||
),
|
||||
vol.Optional("triggers"): cv.schema_with_slug_keys(
|
||||
{
|
||||
vol.Required("name"): translation_value_validator,
|
||||
|
||||
@@ -75,7 +75,6 @@ from homeassistant.core import (
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
category_registry as cr,
|
||||
condition,
|
||||
device_registry as dr,
|
||||
entity,
|
||||
entity_platform,
|
||||
@@ -297,7 +296,6 @@ async def async_test_home_assistant(
|
||||
# Load the registries
|
||||
entity.async_setup(hass)
|
||||
loader.async_setup(hass)
|
||||
await condition.async_setup(hass)
|
||||
await trigger.async_setup(hass)
|
||||
|
||||
# setup translation cache instead of calling translation.async_setup(hass)
|
||||
|
||||
@@ -93,7 +93,7 @@ async def test_generate_data_service_structure_fields(
|
||||
init_components: None,
|
||||
mock_ai_task_entity: MockAITaskEntity,
|
||||
) -> None:
|
||||
"""Test the entity can generate structured data with a top level object schema."""
|
||||
"""Test the entity can generate structured data with a top level object schemea."""
|
||||
result = await hass.services.async_call(
|
||||
"ai_task",
|
||||
"generate_data",
|
||||
|
||||
@@ -54,9 +54,9 @@ from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_PLAYING,
|
||||
STATE_STANDBY,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -163,7 +163,7 @@ async def test_reconnect(
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_IDLE
|
||||
assert state.state == STATE_STANDBY
|
||||
assert MSG_RECONNECT[patch_key] in caplog.record_tuples[2]
|
||||
|
||||
|
||||
@@ -672,7 +672,7 @@ async def test_update_lock_not_acquired(hass: HomeAssistant) -> None:
|
||||
await async_update_entity(hass, entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_IDLE
|
||||
assert state.state == STATE_STANDBY
|
||||
|
||||
|
||||
async def test_download(hass: HomeAssistant) -> None:
|
||||
|
||||
@@ -45,6 +45,7 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
STATE_STANDBY,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
@@ -155,8 +156,8 @@ async def test_entity_supported_features_with_control_bus(
|
||||
@pytest.mark.parametrize(
|
||||
("power_state", "play_state", "media_player_state"),
|
||||
[
|
||||
(True, "NETWORK", STATE_OFF),
|
||||
(False, "NETWORK", STATE_OFF),
|
||||
(True, "NETWORK", STATE_STANDBY),
|
||||
(False, "NETWORK", STATE_STANDBY),
|
||||
(False, "play", STATE_OFF),
|
||||
(True, "play", STATE_PLAYING),
|
||||
(True, "pause", STATE_PAUSED),
|
||||
|
||||
@@ -7,20 +7,12 @@ from aiohttp import ClientConnectionError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.eheimdigital.const import DOMAIN
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
SOURCE_USER,
|
||||
SOURCE_ZEROCONF,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .conftest import init_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
ZEROCONF_DISCOVERY = ZeroconfServiceInfo(
|
||||
ip_address=ip_address("192.0.2.1"),
|
||||
ip_addresses=[ip_address("192.0.2.1")],
|
||||
@@ -218,74 +210,3 @@ async def test_abort(hass: HomeAssistant, eheimdigital_hub_mock: AsyncMock) -> N
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "already_configured"
|
||||
|
||||
|
||||
@patch("homeassistant.components.eheimdigital.config_flow.asyncio.Event", new=AsyncMock)
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error_value"),
|
||||
[(ClientConnectionError(), "cannot_connect"), (Exception(), "unknown")],
|
||||
)
|
||||
async def test_reconfigure(
|
||||
hass: HomeAssistant,
|
||||
eheimdigital_hub_mock: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
side_effect: Exception,
|
||||
error_value: str,
|
||||
) -> None:
|
||||
"""Test reconfigure flow."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == SOURCE_RECONFIGURE
|
||||
|
||||
eheimdigital_hub_mock.return_value.connect.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error_value}
|
||||
|
||||
eheimdigital_hub_mock.return_value.connect.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert (
|
||||
mock_config_entry.unique_id
|
||||
== eheimdigital_hub_mock.return_value.main.mac_address
|
||||
)
|
||||
|
||||
|
||||
@patch("homeassistant.components.eheimdigital.config_flow.asyncio.Event", new=AsyncMock)
|
||||
async def test_reconfigure_different_device(
|
||||
hass: HomeAssistant,
|
||||
eheimdigital_hub_mock: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reconfigure flow."""
|
||||
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
# Simulate a different device
|
||||
eheimdigital_hub_mock.return_value.main.mac_address = "00:00:00:00:00:02"
|
||||
|
||||
result = await mock_config_entry.start_reconfigure_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == SOURCE_RECONFIGURE
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
USER_INPUT,
|
||||
)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "unique_id_mismatch"
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.components.emulated_roku.binding import (
|
||||
ROKU_COMMAND_LAUNCH,
|
||||
EmulatedRoku,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
|
||||
|
||||
async def test_events_fired_properly(hass: HomeAssistant) -> None:
|
||||
@@ -43,7 +43,6 @@ async def test_events_fired_properly(hass: HomeAssistant) -> None:
|
||||
|
||||
return Mock(start=AsyncMock(), close=AsyncMock())
|
||||
|
||||
@callback
|
||||
def listener(event: Event) -> None:
|
||||
if event.data[ATTR_SOURCE_NAME] == random_name:
|
||||
events.append(event)
|
||||
|
||||
@@ -307,7 +307,7 @@
|
||||
'name': 'Inverter 1',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72',
|
||||
'serial_number': '1',
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': None,
|
||||
}),
|
||||
@@ -1186,7 +1186,7 @@
|
||||
'name': 'Inverter 1',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72',
|
||||
'serial_number': '1',
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': None,
|
||||
}),
|
||||
@@ -2109,7 +2109,7 @@
|
||||
'name': 'Inverter 1',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72',
|
||||
'serial_number': '1',
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': None,
|
||||
}),
|
||||
@@ -2805,7 +2805,7 @@
|
||||
'name': 'Inverter 1',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': '45a36e55aaddb2007c5f6602e0c38e72',
|
||||
'serial_number': '1',
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': None,
|
||||
}),
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
"""Test the Google Assistant SDK application_credentials."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant import setup
|
||||
from homeassistant.components.google_assistant_sdk.application_credentials import (
|
||||
async_get_description_placeholders,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("additional_components", "external_url", "expected_redirect_uri"),
|
||||
[
|
||||
([], "https://example.com", "https://example.com/auth/external/callback"),
|
||||
([], None, "https://YOUR_DOMAIN:PORT/auth/external/callback"),
|
||||
(["my"], "https://example.com", "https://my.home-assistant.io/redirect/oauth"),
|
||||
],
|
||||
)
|
||||
async def test_description_placeholders(
|
||||
hass: HomeAssistant,
|
||||
additional_components: list[str],
|
||||
external_url: str | None,
|
||||
expected_redirect_uri: str,
|
||||
) -> None:
|
||||
"""Test description placeholders."""
|
||||
for component in additional_components:
|
||||
assert await setup.async_setup_component(hass, component, {})
|
||||
hass.config.external_url = external_url
|
||||
placeholders = await async_get_description_placeholders(hass)
|
||||
assert placeholders == {
|
||||
"oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent",
|
||||
"more_info_url": "https://www.home-assistant.io/integrations/google_assistant_sdk/",
|
||||
"oauth_creds_url": "https://console.cloud.google.com/apis/credentials",
|
||||
"redirect_url": expected_redirect_uri,
|
||||
}
|
||||
@@ -63,7 +63,6 @@
|
||||
'stay_out_zones': True,
|
||||
'work_areas': True,
|
||||
}),
|
||||
'messages': None,
|
||||
'metadata': dict({
|
||||
'connected': True,
|
||||
'status_dateteime': '2023-06-05T00:00:00+00:00',
|
||||
@@ -81,7 +80,7 @@
|
||||
'work_area_name': 'Front lawn',
|
||||
}),
|
||||
'planner': dict({
|
||||
'external_reason': 'ifttt',
|
||||
'external_reason': 'ifttt_wildlife',
|
||||
'next_start_datetime': '2023-06-05T19:00:00+02:00',
|
||||
'override': dict({
|
||||
'action': 'not_active',
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
"""Test constants for the media source component."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.media_source.const import URI_SCHEME_REGEX
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("uri", "expected_domain", "expected_identifier"),
|
||||
[
|
||||
("media-source://", None, None),
|
||||
("media-source://local_media", "local_media", None),
|
||||
(
|
||||
"media-source://local_media/some/path/file.mp3",
|
||||
"local_media",
|
||||
"some/path/file.mp3",
|
||||
),
|
||||
("media-source://a/b", "a", "b"),
|
||||
(
|
||||
"media-source://domain/file with spaces.mp4",
|
||||
"domain",
|
||||
"file with spaces.mp4",
|
||||
),
|
||||
(
|
||||
"media-source://domain/file-with-dashes.mp3",
|
||||
"domain",
|
||||
"file-with-dashes.mp3",
|
||||
),
|
||||
("media-source://domain/file.with.dots.mp3", "domain", "file.with.dots.mp3"),
|
||||
(
|
||||
"media-source://domain/special!@#$%^&*()chars",
|
||||
"domain",
|
||||
"special!@#$%^&*()chars",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_valid_uri_patterns(
|
||||
uri: str, expected_domain: str | None, expected_identifier: str | None
|
||||
) -> None:
|
||||
"""Test various valid URI patterns."""
|
||||
match = URI_SCHEME_REGEX.match(uri)
|
||||
assert match is not None
|
||||
assert match.group("domain") == expected_domain
|
||||
assert match.group("identifier") == expected_identifier
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"uri",
|
||||
[
|
||||
"media-source:", # missing //
|
||||
"media-source:/", # missing second /
|
||||
"media-source:///", # extra /
|
||||
"media-source://domain/", # trailing slash after domain
|
||||
"invalid-scheme://domain", # wrong scheme
|
||||
"media-source//domain", # missing :
|
||||
"MEDIA-SOURCE://domain", # uppercase scheme
|
||||
"media_source://domain", # underscore in scheme
|
||||
"", # empty string
|
||||
"media-source", # scheme only
|
||||
"media-source://domain extra", # extra content
|
||||
"prefix media-source://domain", # prefix content
|
||||
"media-source://domain suffix", # suffix content
|
||||
# Invalid domain names
|
||||
"media-source://_test", # starts with underscore
|
||||
"media-source://test_", # ends with underscore
|
||||
"media-source://_test_", # starts and ends with underscore
|
||||
"media-source://_", # single underscore
|
||||
"media-source://test-123", # contains hyphen
|
||||
"media-source://test.123", # contains dot
|
||||
"media-source://test 123", # contains space
|
||||
"media-source://TEST", # uppercase letters
|
||||
"media-source://Test", # mixed case
|
||||
# Identifier cannot start with slash
|
||||
"media-source://domain//invalid", # identifier starts with slash
|
||||
],
|
||||
)
|
||||
def test_invalid_uris(uri: str) -> None:
|
||||
"""Test invalid URI formats."""
|
||||
match = URI_SCHEME_REGEX.match(uri)
|
||||
assert match is None, f"URI '{uri}' should be invalid"
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from homeassistant.components.media_player import MediaClass, MediaType
|
||||
from homeassistant.components.media_source import const, models
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
async def test_browse_media_as_dict() -> None:
|
||||
@@ -69,18 +68,3 @@ async def test_media_source_default_name() -> None:
|
||||
"""Test MediaSource uses domain as default name."""
|
||||
source = models.MediaSource(const.DOMAIN)
|
||||
assert source.name == const.DOMAIN
|
||||
|
||||
|
||||
async def test_media_source_item_media_source_id(hass: HomeAssistant) -> None:
|
||||
"""Test MediaSourceItem media_source_id property."""
|
||||
# Test with domain and identifier
|
||||
item = models.MediaSourceItem(hass, "test_domain", "test/identifier", None)
|
||||
assert item.media_source_id == "media-source://test_domain/test/identifier"
|
||||
|
||||
# Test with domain only
|
||||
item = models.MediaSourceItem(hass, "test_domain", "", None)
|
||||
assert item.media_source_id == "media-source://test_domain"
|
||||
|
||||
# Test with no domain (root)
|
||||
item = models.MediaSourceItem(hass, None, "", None)
|
||||
assert item.media_source_id == "media-source://"
|
||||
|
||||
@@ -5,7 +5,6 @@ MOCK_NAME = "Philips TV"
|
||||
|
||||
MOCK_USERNAME = "mock_user"
|
||||
MOCK_PASSWORD = "mock_password"
|
||||
MOCK_HOSTNAME = "mock_hostname"
|
||||
|
||||
MOCK_SYSTEM = {
|
||||
"menulanguage": "English",
|
||||
|
||||
@@ -38,7 +38,6 @@ def mock_tv():
|
||||
tv.application = None
|
||||
tv.applications = {}
|
||||
tv.system = MOCK_SYSTEM
|
||||
tv.name = MOCK_NAME
|
||||
tv.api_version = 1
|
||||
tv.api_version_detected = None
|
||||
tv.on = True
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Test the Philips TV config flow."""
|
||||
|
||||
from ipaddress import ip_address
|
||||
from unittest.mock import ANY
|
||||
|
||||
from haphilipsjs import PairingFailure
|
||||
@@ -10,13 +9,10 @@ from homeassistant import config_entries
|
||||
from homeassistant.components.philips_js.const import CONF_ALLOW_NOTIFY, DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from . import (
|
||||
MOCK_CONFIG,
|
||||
MOCK_CONFIG_PAIRED,
|
||||
MOCK_HOSTNAME,
|
||||
MOCK_NAME,
|
||||
MOCK_PASSWORD,
|
||||
MOCK_SYSTEM,
|
||||
MOCK_SYSTEM_UNPAIRED,
|
||||
@@ -37,7 +33,6 @@ async def mock_tv_pairable(mock_tv):
|
||||
mock_tv.api_version = 6
|
||||
mock_tv.api_version_detected = 6
|
||||
mock_tv.secured_transport = True
|
||||
mock_tv.name = MOCK_NAME
|
||||
|
||||
mock_tv.pairRequest.return_value = {}
|
||||
mock_tv.pairGrant.return_value = MOCK_USERNAME, MOCK_PASSWORD
|
||||
@@ -107,6 +102,21 @@ async def test_form_cannot_connect(hass: HomeAssistant, mock_tv) -> None:
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_unexpected_error(hass: HomeAssistant, mock_tv) -> None:
|
||||
"""Test we handle unexpected exceptions."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
mock_tv.getSystem.side_effect = Exception("Unexpected exception")
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], MOCK_USERINPUT
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry) -> None:
|
||||
"""Test we get the form."""
|
||||
mock_tv = mock_tv_pairable
|
||||
@@ -133,13 +143,7 @@ async def test_pairing(hass: HomeAssistant, mock_tv_pairable, mock_setup_entry)
|
||||
)
|
||||
|
||||
assert result == {
|
||||
"context": {
|
||||
"source": "user",
|
||||
"unique_id": "ABCDEFGHIJKLF",
|
||||
"title_placeholders": {
|
||||
"name": "Philips TV",
|
||||
},
|
||||
},
|
||||
"context": {"source": "user", "unique_id": "ABCDEFGHIJKLF"},
|
||||
"flow_id": ANY,
|
||||
"type": "create_entry",
|
||||
"description": None,
|
||||
@@ -254,67 +258,3 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert config_entry.options == {CONF_ALLOW_NOTIFY: True}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("secured_transport", "discovery_type"),
|
||||
[(True, "_philipstv_s_rpc._tcp.local."), (False, "_philipstv_rpc._tcp.local.")],
|
||||
)
|
||||
async def test_zeroconf_discovery(
|
||||
hass: HomeAssistant, mock_tv_pairable, secured_transport, discovery_type
|
||||
) -> None:
|
||||
"""Test we can setup from zeroconf discovery."""
|
||||
|
||||
mock_tv_pairable.secured_transport = secured_transport
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
hostname=MOCK_HOSTNAME,
|
||||
name=MOCK_NAME,
|
||||
port=None,
|
||||
properties={},
|
||||
type=discovery_type,
|
||||
),
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
mock_tv_pairable.setTransport.assert_called_with(secured_transport)
|
||||
mock_tv_pairable.pairRequest.assert_called()
|
||||
|
||||
|
||||
async def test_zeroconf_probe_failed(
|
||||
hass: HomeAssistant,
|
||||
mock_tv_pairable,
|
||||
) -> None:
|
||||
"""Test we can setup from zeroconf discovery."""
|
||||
|
||||
mock_tv_pairable.system = None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
data=ZeroconfServiceInfo(
|
||||
ip_address=ip_address("127.0.0.1"),
|
||||
ip_addresses=[ip_address("127.0.0.1")],
|
||||
hostname=MOCK_HOSTNAME,
|
||||
name=MOCK_NAME,
|
||||
port=None,
|
||||
properties={},
|
||||
type="_philipstv_s_rpc._tcp.local.",
|
||||
),
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "discovery_failure"
|
||||
|
||||
@@ -33,8 +33,8 @@ from homeassistant.const import (
|
||||
CONF_REGION,
|
||||
CONF_TOKEN,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_PLAYING,
|
||||
STATE_STANDBY,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -188,7 +188,7 @@ async def test_state_standby_is_set(hass: HomeAssistant) -> None:
|
||||
|
||||
await mock_ddp_response(hass, MOCK_STATUS_STANDBY)
|
||||
|
||||
assert hass.states.get(mock_entity_id).state == STATE_OFF
|
||||
assert hass.states.get(mock_entity_id).state == STATE_STANDBY
|
||||
|
||||
|
||||
async def test_state_playing_is_set(hass: HomeAssistant) -> None:
|
||||
@@ -308,7 +308,7 @@ async def test_device_info_is_set_from_status_correctly(
|
||||
|
||||
mock_d_entries = device_registry.devices
|
||||
mock_entry = device_registry.async_get_device(identifiers={(DOMAIN, MOCK_HOST_ID)})
|
||||
assert mock_state == STATE_OFF
|
||||
assert mock_state == STATE_STANDBY
|
||||
|
||||
assert len(mock_d_entries) == 1
|
||||
assert mock_entry.name == MOCK_HOST_NAME
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
"""Test the Radio Browser media source."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from radios import RadioBrowser
|
||||
|
||||
from homeassistant.components.media_source import MediaSourceItem, Unresolvable
|
||||
from homeassistant.components.radio_browser.const import DOMAIN
|
||||
from homeassistant.components.radio_browser.media_source import (
|
||||
RadioMediaSource,
|
||||
async_get_media_source,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_radio_browser() -> AsyncMock:
|
||||
"""Mock RadioBrowser."""
|
||||
radio_browser = AsyncMock(spec=RadioBrowser)
|
||||
# Mock station without full Station object to avoid constructor complexity
|
||||
mock_station = AsyncMock()
|
||||
mock_station.uuid = "test-uuid"
|
||||
mock_station.name = "Test Station"
|
||||
mock_station.url = "https://example.com/stream"
|
||||
mock_station.codec = "MP3"
|
||||
mock_station.favicon = "https://example.com/favicon.ico"
|
||||
radio_browser.station.return_value = mock_station
|
||||
return radio_browser
|
||||
|
||||
|
||||
async def test_media_source_without_runtime_data(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test media source raises error when runtime_data is missing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
# Don't set runtime_data to simulate the error condition
|
||||
media_source = RadioMediaSource(hass, mock_config_entry)
|
||||
|
||||
with pytest.raises(
|
||||
Unresolvable, match="Radio Browser integration not properly loaded"
|
||||
):
|
||||
_ = media_source.radios
|
||||
|
||||
|
||||
async def test_media_source_with_none_runtime_data(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test media source raises error when runtime_data is None."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_config_entry.runtime_data = None
|
||||
|
||||
media_source = RadioMediaSource(hass, mock_config_entry)
|
||||
|
||||
with pytest.raises(
|
||||
Unresolvable, match="Radio Browser integration not properly loaded"
|
||||
):
|
||||
_ = media_source.radios
|
||||
|
||||
|
||||
async def test_media_source_with_runtime_data(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_radio_browser: AsyncMock,
|
||||
) -> None:
|
||||
"""Test media source works correctly with runtime_data."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_config_entry.runtime_data = mock_radio_browser
|
||||
|
||||
media_source = RadioMediaSource(hass, mock_config_entry)
|
||||
|
||||
# Should not raise an error
|
||||
radios = media_source.radios
|
||||
assert radios is mock_radio_browser
|
||||
|
||||
|
||||
async def test_async_get_media_source(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_radio_browser: AsyncMock,
|
||||
) -> None:
|
||||
"""Test async_get_media_source function."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_config_entry.runtime_data = mock_radio_browser
|
||||
|
||||
media_source = await async_get_media_source(hass)
|
||||
|
||||
assert isinstance(media_source, RadioMediaSource)
|
||||
assert media_source.entry is mock_config_entry
|
||||
assert media_source.radios is mock_radio_browser
|
||||
|
||||
|
||||
async def test_async_resolve_media_with_missing_runtime_data(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test async_resolve_media raises error when runtime_data is missing."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
# Don't set runtime_data to simulate the error condition
|
||||
|
||||
media_source = RadioMediaSource(hass, mock_config_entry)
|
||||
item = MediaSourceItem(
|
||||
hass=hass,
|
||||
domain=DOMAIN,
|
||||
identifier="test-uuid",
|
||||
target_media_player=None,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
Unresolvable, match="Radio Browser integration not properly loaded"
|
||||
):
|
||||
await media_source.async_resolve_media(item)
|
||||
|
||||
|
||||
async def test_async_browse_media_with_missing_runtime_data(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test async_browse_media raises error when runtime_data is missing."""
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
# Don't set runtime_data to simulate the error condition
|
||||
|
||||
media_source = RadioMediaSource(hass, mock_config_entry)
|
||||
item = MediaSourceItem(
|
||||
hass=hass,
|
||||
domain=DOMAIN,
|
||||
identifier="",
|
||||
target_media_player=None,
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
Unresolvable, match="Radio Browser integration not properly loaded"
|
||||
):
|
||||
await media_source.async_browse_media(item)
|
||||
@@ -52,10 +52,10 @@ from homeassistant.const import (
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_UP,
|
||||
STATE_IDLE,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_PAUSED,
|
||||
STATE_PLAYING,
|
||||
STATE_STANDBY,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -112,7 +112,7 @@ async def test_idle_setup(
|
||||
"""Test setup with idle device."""
|
||||
state = hass.states.get(MAIN_ENTITY_ID)
|
||||
assert state
|
||||
assert state.state == STATE_OFF
|
||||
assert state.state == STATE_STANDBY
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_device", ["roku/rokutv-7820x.json"], indirect=True)
|
||||
|
||||
@@ -63,7 +63,7 @@ async def test_options_flow(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"][ATTR_PARSER] == PARSER_PLAIN_TEXT
|
||||
assert result["data"][ATTR_PARSER] is None
|
||||
|
||||
|
||||
async def test_reconfigure_flow_broadcast(
|
||||
|
||||
@@ -50,7 +50,6 @@ from homeassistant.components.telegram_bot.const import (
|
||||
ATTR_VERIFY_SSL,
|
||||
CONF_CONFIG_ENTRY_ID,
|
||||
DOMAIN,
|
||||
PARSER_PLAIN_TEXT,
|
||||
PLATFORM_BROADCAST,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
SERVICE_ANSWER_CALLBACK_QUERY,
|
||||
@@ -184,7 +183,6 @@ async def test_send_message(
|
||||
(
|
||||
{
|
||||
ATTR_MESSAGE: "test_message",
|
||||
ATTR_PARSER: PARSER_PLAIN_TEXT,
|
||||
ATTR_KEYBOARD_INLINE: "command1:/cmd1,/cmd2,mock_link:https://mock_link",
|
||||
},
|
||||
InlineKeyboardMarkup(
|
||||
@@ -201,7 +199,6 @@ async def test_send_message(
|
||||
(
|
||||
{
|
||||
ATTR_MESSAGE: "test_message",
|
||||
ATTR_PARSER: PARSER_PLAIN_TEXT,
|
||||
ATTR_KEYBOARD_INLINE: [
|
||||
[["command1", "/cmd1"]],
|
||||
[["mock_link", "https://mock_link"]],
|
||||
@@ -253,7 +250,7 @@ async def test_send_message_with_inline_keyboard(
|
||||
mock_send_message.assert_called_once_with(
|
||||
12345678,
|
||||
"test_message",
|
||||
parse_mode=None,
|
||||
parse_mode=ParseMode.MARKDOWN,
|
||||
disable_web_page_preview=None,
|
||||
disable_notification=False,
|
||||
reply_to_message_id=None,
|
||||
|
||||
@@ -1 +1,387 @@
|
||||
"""Tests for the Wallbox integration."""
|
||||
|
||||
from http import HTTPStatus
|
||||
|
||||
import requests
|
||||
import requests_mock
|
||||
|
||||
from homeassistant.components.wallbox.const import (
|
||||
CHARGER_ADDED_ENERGY_KEY,
|
||||
CHARGER_ADDED_RANGE_KEY,
|
||||
CHARGER_CHARGING_POWER_KEY,
|
||||
CHARGER_CHARGING_SPEED_KEY,
|
||||
CHARGER_CURRENCY_KEY,
|
||||
CHARGER_CURRENT_VERSION_KEY,
|
||||
CHARGER_DATA_KEY,
|
||||
CHARGER_ECO_SMART_KEY,
|
||||
CHARGER_ECO_SMART_MODE_KEY,
|
||||
CHARGER_ECO_SMART_STATUS_KEY,
|
||||
CHARGER_ENERGY_PRICE_KEY,
|
||||
CHARGER_FEATURES_KEY,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY,
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY,
|
||||
CHARGER_MAX_ICP_CURRENT_KEY,
|
||||
CHARGER_NAME_KEY,
|
||||
CHARGER_PART_NUMBER_KEY,
|
||||
CHARGER_PLAN_KEY,
|
||||
CHARGER_POWER_BOOST_KEY,
|
||||
CHARGER_SERIAL_NUMBER_KEY,
|
||||
CHARGER_SOFTWARE_KEY,
|
||||
CHARGER_STATUS_ID_KEY,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import ERROR, REFRESH_TOKEN_TTL, STATUS, TTL, USER_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
test_response = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
CHARGER_NAME_KEY: "WallboxName",
|
||||
CHARGER_DATA_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: 24,
|
||||
CHARGER_ENERGY_PRICE_KEY: 0.4,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY: False,
|
||||
CHARGER_SERIAL_NUMBER_KEY: "20000",
|
||||
CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E",
|
||||
CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"},
|
||||
CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"},
|
||||
CHARGER_MAX_ICP_CURRENT_KEY: 20,
|
||||
CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]},
|
||||
CHARGER_ECO_SMART_KEY: {
|
||||
CHARGER_ECO_SMART_STATUS_KEY: False,
|
||||
CHARGER_ECO_SMART_MODE_KEY: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test_response_bidir = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
CHARGER_NAME_KEY: "WallboxName",
|
||||
CHARGER_DATA_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: 24,
|
||||
CHARGER_ENERGY_PRICE_KEY: 0.4,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY: False,
|
||||
CHARGER_SERIAL_NUMBER_KEY: "20000",
|
||||
CHARGER_PART_NUMBER_KEY: "QSP1-0-2-4-9-002-E",
|
||||
CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"},
|
||||
CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"},
|
||||
CHARGER_MAX_ICP_CURRENT_KEY: 20,
|
||||
CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]},
|
||||
CHARGER_ECO_SMART_KEY: {
|
||||
CHARGER_ECO_SMART_STATUS_KEY: False,
|
||||
CHARGER_ECO_SMART_MODE_KEY: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test_response_eco_mode = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
CHARGER_NAME_KEY: "WallboxName",
|
||||
CHARGER_DATA_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: 24,
|
||||
CHARGER_ENERGY_PRICE_KEY: 0.4,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY: False,
|
||||
CHARGER_SERIAL_NUMBER_KEY: "20000",
|
||||
CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E",
|
||||
CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"},
|
||||
CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"},
|
||||
CHARGER_MAX_ICP_CURRENT_KEY: 20,
|
||||
CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]},
|
||||
CHARGER_ECO_SMART_KEY: {
|
||||
CHARGER_ECO_SMART_STATUS_KEY: True,
|
||||
CHARGER_ECO_SMART_MODE_KEY: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
test_response_full_solar = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
CHARGER_NAME_KEY: "WallboxName",
|
||||
CHARGER_DATA_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: 24,
|
||||
CHARGER_ENERGY_PRICE_KEY: 0.4,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY: False,
|
||||
CHARGER_SERIAL_NUMBER_KEY: "20000",
|
||||
CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E",
|
||||
CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"},
|
||||
CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"},
|
||||
CHARGER_MAX_ICP_CURRENT_KEY: 20,
|
||||
CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]},
|
||||
CHARGER_ECO_SMART_KEY: {
|
||||
CHARGER_ECO_SMART_STATUS_KEY: True,
|
||||
CHARGER_ECO_SMART_MODE_KEY: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test_response_no_power_boost = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
CHARGER_NAME_KEY: "WallboxName",
|
||||
CHARGER_DATA_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: 24,
|
||||
CHARGER_ENERGY_PRICE_KEY: 0.4,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY: False,
|
||||
CHARGER_SERIAL_NUMBER_KEY: "20000",
|
||||
CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E",
|
||||
CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"},
|
||||
CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"},
|
||||
CHARGER_MAX_ICP_CURRENT_KEY: 20,
|
||||
CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: []},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
http_404_error = requests.exceptions.HTTPError()
|
||||
http_404_error.response = requests.Response()
|
||||
http_404_error.response.status_code = HTTPStatus.NOT_FOUND
|
||||
http_429_error = requests.exceptions.HTTPError()
|
||||
http_429_error.response = requests.Response()
|
||||
http_429_error.response.status_code = HTTPStatus.TOO_MANY_REQUESTS
|
||||
|
||||
authorisation_response = {
|
||||
"data": {
|
||||
"attributes": {
|
||||
"token": "fakekeyhere",
|
||||
"refresh_token": "refresh_fakekeyhere",
|
||||
USER_ID: 12345,
|
||||
TTL: 145656758,
|
||||
REFRESH_TOKEN_TTL: 145756758,
|
||||
ERROR: "false",
|
||||
STATUS: 200,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
authorisation_response_unauthorised = {
|
||||
"data": {
|
||||
"attributes": {
|
||||
"token": "fakekeyhere",
|
||||
"refresh_token": "refresh_fakekeyhere",
|
||||
USER_ID: 12345,
|
||||
TTL: 145656758,
|
||||
REFRESH_TOKEN_TTL: 145756758,
|
||||
ERROR: "false",
|
||||
STATUS: 404,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invalid_reauth_response = {
|
||||
"jwt": "fakekeyhere",
|
||||
"refresh_token": "refresh_fakekeyhere",
|
||||
"user_id": 12345,
|
||||
"ttl": 145656758,
|
||||
"refresh_token_ttl": 145756758,
|
||||
"error": False,
|
||||
"status": 200,
|
||||
}
|
||||
|
||||
http_403_error = requests.exceptions.HTTPError()
|
||||
http_403_error.response = requests.Response()
|
||||
http_403_error.response.status_code = HTTPStatus.FORBIDDEN
|
||||
|
||||
http_404_error = requests.exceptions.HTTPError()
|
||||
http_404_error.response = requests.Response()
|
||||
http_404_error.response.status_code = HTTPStatus.NOT_FOUND
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Test wallbox sensor class setup."""
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20},
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def setup_integration_no_eco_mode(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class setup."""
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response_no_power_boost,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20},
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def setup_integration_select(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, response
|
||||
) -> None:
|
||||
"""Test wallbox sensor class setup."""
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20},
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def setup_integration_bidir(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Test wallbox sensor class setup."""
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response_bidir,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20},
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def setup_integration_connection_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class setup with a connection error."""
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response,
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20},
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def setup_integration_read_only(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class setup for read only."""
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json=test_response,
|
||||
status_code=HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def setup_integration_platform_not_ready(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class setup for read only."""
|
||||
|
||||
with requests_mock.Mocker() as mock_request:
|
||||
mock_request.get(
|
||||
"https://user-api.wall-box.com/users/signin",
|
||||
json=authorisation_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.get(
|
||||
"https://api.wall-box.com/chargers/status/12345",
|
||||
json=test_response,
|
||||
status_code=HTTPStatus.OK,
|
||||
)
|
||||
mock_request.put(
|
||||
"https://api.wall-box.com/v2/charger/12345",
|
||||
json=test_response,
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1,220 +1,13 @@
|
||||
"""Test fixtures for the Wallbox integration."""
|
||||
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from homeassistant.components.wallbox.const import (
|
||||
CHARGER_ADDED_ENERGY_KEY,
|
||||
CHARGER_ADDED_RANGE_KEY,
|
||||
CHARGER_CHARGING_POWER_KEY,
|
||||
CHARGER_CHARGING_SPEED_KEY,
|
||||
CHARGER_CURRENCY_KEY,
|
||||
CHARGER_CURRENT_VERSION_KEY,
|
||||
CHARGER_DATA_KEY,
|
||||
CHARGER_DATA_POST_L1_KEY,
|
||||
CHARGER_DATA_POST_L2_KEY,
|
||||
CHARGER_ECO_SMART_KEY,
|
||||
CHARGER_ECO_SMART_MODE_KEY,
|
||||
CHARGER_ECO_SMART_STATUS_KEY,
|
||||
CHARGER_ENERGY_PRICE_KEY,
|
||||
CHARGER_FEATURES_KEY,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY,
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY,
|
||||
CHARGER_MAX_CHARGING_CURRENT_POST_KEY,
|
||||
CHARGER_MAX_ICP_CURRENT_KEY,
|
||||
CHARGER_NAME_KEY,
|
||||
CHARGER_PART_NUMBER_KEY,
|
||||
CHARGER_PLAN_KEY,
|
||||
CHARGER_POWER_BOOST_KEY,
|
||||
CHARGER_SERIAL_NUMBER_KEY,
|
||||
CHARGER_SOFTWARE_KEY,
|
||||
CHARGER_STATUS_ID_KEY,
|
||||
CONF_STATION,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import ERROR, REFRESH_TOKEN_TTL, STATUS, TTL, USER_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
test_response = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
CHARGER_NAME_KEY: "WallboxName",
|
||||
CHARGER_DATA_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: 24,
|
||||
CHARGER_ENERGY_PRICE_KEY: 0.4,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY: False,
|
||||
CHARGER_SERIAL_NUMBER_KEY: "20000",
|
||||
CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E",
|
||||
CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"},
|
||||
CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"},
|
||||
CHARGER_MAX_ICP_CURRENT_KEY: 20,
|
||||
CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]},
|
||||
CHARGER_ECO_SMART_KEY: {
|
||||
CHARGER_ECO_SMART_STATUS_KEY: False,
|
||||
CHARGER_ECO_SMART_MODE_KEY: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test_response_bidir = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
CHARGER_NAME_KEY: "WallboxName",
|
||||
CHARGER_DATA_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: 24,
|
||||
CHARGER_ENERGY_PRICE_KEY: 0.4,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY: False,
|
||||
CHARGER_SERIAL_NUMBER_KEY: "20000",
|
||||
CHARGER_PART_NUMBER_KEY: "QSP1-0-2-4-9-002-E",
|
||||
CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"},
|
||||
CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"},
|
||||
CHARGER_MAX_ICP_CURRENT_KEY: 20,
|
||||
CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]},
|
||||
CHARGER_ECO_SMART_KEY: {
|
||||
CHARGER_ECO_SMART_STATUS_KEY: False,
|
||||
CHARGER_ECO_SMART_MODE_KEY: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test_response_eco_mode = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
CHARGER_NAME_KEY: "WallboxName",
|
||||
CHARGER_DATA_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: 24,
|
||||
CHARGER_ENERGY_PRICE_KEY: 0.4,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY: False,
|
||||
CHARGER_SERIAL_NUMBER_KEY: "20000",
|
||||
CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E",
|
||||
CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"},
|
||||
CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"},
|
||||
CHARGER_MAX_ICP_CURRENT_KEY: 20,
|
||||
CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]},
|
||||
CHARGER_ECO_SMART_KEY: {
|
||||
CHARGER_ECO_SMART_STATUS_KEY: True,
|
||||
CHARGER_ECO_SMART_MODE_KEY: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
test_response_full_solar = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
CHARGER_NAME_KEY: "WallboxName",
|
||||
CHARGER_DATA_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: 24,
|
||||
CHARGER_ENERGY_PRICE_KEY: 0.4,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY: False,
|
||||
CHARGER_SERIAL_NUMBER_KEY: "20000",
|
||||
CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E",
|
||||
CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"},
|
||||
CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"},
|
||||
CHARGER_MAX_ICP_CURRENT_KEY: 20,
|
||||
CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: [CHARGER_POWER_BOOST_KEY]},
|
||||
CHARGER_ECO_SMART_KEY: {
|
||||
CHARGER_ECO_SMART_STATUS_KEY: True,
|
||||
CHARGER_ECO_SMART_MODE_KEY: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
test_response_no_power_boost = {
|
||||
CHARGER_CHARGING_POWER_KEY: 0,
|
||||
CHARGER_STATUS_ID_KEY: 193,
|
||||
CHARGER_MAX_AVAILABLE_POWER_KEY: 25.0,
|
||||
CHARGER_CHARGING_SPEED_KEY: 0,
|
||||
CHARGER_ADDED_RANGE_KEY: 150,
|
||||
CHARGER_ADDED_ENERGY_KEY: 44.697,
|
||||
CHARGER_NAME_KEY: "WallboxName",
|
||||
CHARGER_DATA_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY: 24,
|
||||
CHARGER_ENERGY_PRICE_KEY: 0.4,
|
||||
CHARGER_LOCKED_UNLOCKED_KEY: False,
|
||||
CHARGER_SERIAL_NUMBER_KEY: "20000",
|
||||
CHARGER_PART_NUMBER_KEY: "PLP1-0-2-4-9-002-E",
|
||||
CHARGER_SOFTWARE_KEY: {CHARGER_CURRENT_VERSION_KEY: "5.5.10"},
|
||||
CHARGER_CURRENCY_KEY: {"code": "EUR/kWh"},
|
||||
CHARGER_MAX_ICP_CURRENT_KEY: 20,
|
||||
CHARGER_PLAN_KEY: {CHARGER_FEATURES_KEY: []},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
http_403_error = requests.exceptions.HTTPError()
|
||||
http_403_error.response = requests.Response()
|
||||
http_403_error.response.status_code = HTTPStatus.FORBIDDEN
|
||||
http_404_error = requests.exceptions.HTTPError()
|
||||
http_404_error.response = requests.Response()
|
||||
http_404_error.response.status_code = HTTPStatus.NOT_FOUND
|
||||
http_429_error = requests.exceptions.HTTPError()
|
||||
http_429_error.response = requests.Response()
|
||||
http_429_error.response.status_code = HTTPStatus.TOO_MANY_REQUESTS
|
||||
|
||||
authorisation_response = {
|
||||
"data": {
|
||||
"attributes": {
|
||||
"token": "fakekeyhere",
|
||||
"refresh_token": "refresh_fakekeyhere",
|
||||
USER_ID: 12345,
|
||||
TTL: 145656758,
|
||||
REFRESH_TOKEN_TTL: 145756758,
|
||||
ERROR: "false",
|
||||
STATUS: 200,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
authorisation_response_unauthorised = {
|
||||
"data": {
|
||||
"attributes": {
|
||||
"token": "fakekeyhere",
|
||||
"refresh_token": "refresh_fakekeyhere",
|
||||
USER_ID: 12345,
|
||||
TTL: 145656758,
|
||||
REFRESH_TOKEN_TTL: 145756758,
|
||||
ERROR: "false",
|
||||
STATUS: 404,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
invalid_reauth_response = {
|
||||
"jwt": "fakekeyhere",
|
||||
"refresh_token": "refresh_fakekeyhere",
|
||||
"user_id": 12345,
|
||||
"ttl": 145656758,
|
||||
"refresh_token_ttl": 145756758,
|
||||
"error": False,
|
||||
"status": 200,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
@@ -230,46 +23,3 @@ def entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
return entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_wallbox():
|
||||
"""Patch Wallbox class for tests."""
|
||||
with patch("homeassistant.components.wallbox.Wallbox") as mock:
|
||||
wallbox = MagicMock()
|
||||
wallbox.authenticate = Mock(return_value=authorisation_response)
|
||||
wallbox.lockCharger = Mock(
|
||||
return_value={
|
||||
CHARGER_DATA_POST_L1_KEY: {
|
||||
CHARGER_DATA_POST_L2_KEY: {CHARGER_LOCKED_UNLOCKED_KEY: True}
|
||||
}
|
||||
}
|
||||
)
|
||||
wallbox.unlockCharger = Mock(
|
||||
return_value={
|
||||
CHARGER_DATA_POST_L1_KEY: {
|
||||
CHARGER_DATA_POST_L2_KEY: {CHARGER_LOCKED_UNLOCKED_KEY: True}
|
||||
}
|
||||
}
|
||||
)
|
||||
wallbox.setEnergyCost = Mock(return_value={CHARGER_ENERGY_PRICE_KEY: 0.25})
|
||||
wallbox.setMaxChargingCurrent = Mock(
|
||||
return_value={
|
||||
CHARGER_DATA_POST_L1_KEY: {
|
||||
CHARGER_DATA_POST_L2_KEY: {
|
||||
CHARGER_MAX_CHARGING_CURRENT_POST_KEY: True
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
wallbox.setIcpMaxCurrent = Mock(return_value={CHARGER_MAX_ICP_CURRENT_KEY: 25})
|
||||
wallbox.getChargerStatus = Mock(return_value=test_response)
|
||||
mock.return_value = wallbox
|
||||
yield wallbox
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Test wallbox sensor class setup."""
|
||||
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .conftest import (
|
||||
from . import (
|
||||
authorisation_response,
|
||||
authorisation_response_unauthorised,
|
||||
http_403_error,
|
||||
@@ -38,7 +38,7 @@ test_response = {
|
||||
}
|
||||
|
||||
|
||||
async def test_show_set_form(hass: HomeAssistant, mock_wallbox) -> None:
|
||||
async def test_show_set_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the setup form is served."""
|
||||
flow = config_flow.WallboxConfigFlow()
|
||||
flow.hass = hass
|
||||
@@ -53,6 +53,7 @@ async def test_form_cannot_authenticate(hass: HomeAssistant) -> None:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
@@ -72,8 +73,8 @@ async def test_form_cannot_authenticate(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
@@ -81,6 +82,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
@@ -100,8 +102,8 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_validate_input(hass: HomeAssistant) -> None:
|
||||
@@ -109,14 +111,15 @@ async def test_form_validate_input(hass: HomeAssistant) -> None:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
return_value=authorisation_response,
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
|
||||
return_value=test_response,
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(return_value=test_response),
|
||||
),
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
@@ -132,20 +135,20 @@ async def test_form_validate_input(hass: HomeAssistant) -> None:
|
||||
assert result2["data"]["station"] == "12345"
|
||||
|
||||
|
||||
async def test_form_reauth(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
) -> None:
|
||||
async def test_form_reauth(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Test we handle reauth flow."""
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
mock_wallbox,
|
||||
"authenticate",
|
||||
return_value=authorisation_response_unauthorised,
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response_unauthorised),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(return_value=test_response),
|
||||
),
|
||||
patch.object(mock_wallbox, "getChargerStatus", return_value=test_response),
|
||||
):
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
|
||||
@@ -158,27 +161,27 @@ async def test_form_reauth(
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
|
||||
await hass.async_block_till_done()
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_form_reauth_invalid(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
) -> None:
|
||||
async def test_form_reauth_invalid(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Test we handle reauth invalid flow."""
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
mock_wallbox,
|
||||
"authenticate",
|
||||
return_value=authorisation_response_unauthorised,
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response_unauthorised),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(return_value=test_response),
|
||||
),
|
||||
patch.object(mock_wallbox, "getChargerStatus", return_value=test_response),
|
||||
):
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
"""Test Wallbox Init Component."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant.components.wallbox.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import (
|
||||
from . import (
|
||||
authorisation_response,
|
||||
http_403_error,
|
||||
http_429_error,
|
||||
setup_integration,
|
||||
test_response_no_power_boost,
|
||||
setup_integration_connection_error,
|
||||
setup_integration_no_eco_mode,
|
||||
setup_integration_read_only,
|
||||
test_response,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_wallbox_setup_unload_entry(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox Unload."""
|
||||
|
||||
@@ -29,27 +33,37 @@ async def test_wallbox_setup_unload_entry(
|
||||
|
||||
|
||||
async def test_wallbox_unload_entry_connection_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox Unload Connection Error."""
|
||||
with patch.object(mock_wallbox, "authenticate", side_effect=http_403_error):
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
await setup_integration_connection_error(hass, entry)
|
||||
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_wallbox_refresh_failed_connection_error_auth(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox setup with connection error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with patch.object(mock_wallbox, "authenticate", side_effect=http_429_error):
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
|
||||
new=Mock(return_value=test_response),
|
||||
),
|
||||
):
|
||||
wallbox = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
await wallbox.async_refresh()
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
@@ -57,7 +71,7 @@ async def test_wallbox_refresh_failed_connection_error_auth(
|
||||
|
||||
|
||||
async def test_wallbox_refresh_failed_invalid_auth(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox setup with authentication error."""
|
||||
|
||||
@@ -65,8 +79,14 @@ async def test_wallbox_refresh_failed_invalid_auth(
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "authenticate", side_effect=http_403_error),
|
||||
patch.object(mock_wallbox, "pauseChargingSession", side_effect=http_403_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
):
|
||||
wallbox = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
@@ -77,14 +97,23 @@ async def test_wallbox_refresh_failed_invalid_auth(
|
||||
|
||||
|
||||
async def test_wallbox_refresh_failed_http_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox setup with authentication error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with patch.object(mock_wallbox, "getChargerStatus", side_effect=http_403_error):
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
):
|
||||
wallbox = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
await wallbox.async_refresh()
|
||||
@@ -94,14 +123,23 @@ async def test_wallbox_refresh_failed_http_error(
|
||||
|
||||
|
||||
async def test_wallbox_refresh_failed_too_many_requests(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox setup with authentication error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with patch.object(mock_wallbox, "getChargerStatus", side_effect=http_429_error):
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
):
|
||||
wallbox = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
await wallbox.async_refresh()
|
||||
@@ -111,14 +149,23 @@ async def test_wallbox_refresh_failed_too_many_requests(
|
||||
|
||||
|
||||
async def test_wallbox_refresh_failed_connection_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox setup with connection error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
with patch.object(mock_wallbox, "pauseChargingSession", side_effect=http_403_error):
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
):
|
||||
wallbox = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
await wallbox.async_refresh()
|
||||
@@ -127,15 +174,25 @@ async def test_wallbox_refresh_failed_connection_error(
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_wallbox_refresh_failed_read_only(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox setup for read-only user."""
|
||||
|
||||
await setup_integration_read_only(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_wallbox_setup_load_entry_no_eco_mode(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test Wallbox Unload."""
|
||||
with patch.object(
|
||||
mock_wallbox, "getChargerStatus", return_value=test_response_no_power_boost
|
||||
):
|
||||
await setup_integration(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
await setup_integration_no_eco_mode(hass, entry)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
"""Test Wallbox Lock component."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK
|
||||
from homeassistant.components.wallbox.coordinator import InvalidAuth
|
||||
from homeassistant.components.wallbox.const import CHARGER_LOCKED_UNLOCKED_KEY
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .conftest import http_403_error, http_404_error, http_429_error, setup_integration
|
||||
from . import (
|
||||
authorisation_response,
|
||||
http_403_error,
|
||||
http_404_error,
|
||||
http_429_error,
|
||||
setup_integration,
|
||||
)
|
||||
from .const import MOCK_LOCK_ENTITY_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_wallbox_lock_class(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
) -> None:
|
||||
async def test_wallbox_lock_class(hass: HomeAssistant, entry: MockConfigEntry) -> None:
|
||||
"""Test wallbox lock class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
@@ -27,35 +31,60 @@ async def test_wallbox_lock_class(
|
||||
assert state
|
||||
assert state.state == "unlocked"
|
||||
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_LOCK,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||
new=Mock(
|
||||
return_value={"data": {"chargerData": {CHARGER_LOCKED_UNLOCKED_KEY: 1}}}
|
||||
),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.unlockCharger",
|
||||
new=Mock(
|
||||
return_value={"data": {"chargerData": {CHARGER_LOCKED_UNLOCKED_KEY: 0}}}
|
||||
),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_LOCK,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_UNLOCK,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_UNLOCK,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_lock_class_error_handling(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
async def test_wallbox_lock_class_connection_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox lock class connection error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "lockCharger", side_effect=http_404_error),
|
||||
pytest.raises(HomeAssistantError),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||
new=Mock(side_effect=ConnectionError),
|
||||
),
|
||||
pytest.raises(ConnectionError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
@@ -67,8 +96,42 @@ async def test_wallbox_lock_class_error_handling(
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "lockCharger", side_effect=http_404_error),
|
||||
patch.object(mock_wallbox, "unlockCharger", side_effect=http_404_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||
new=Mock(side_effect=ConnectionError),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.unlockCharger",
|
||||
new=Mock(side_effect=ConnectionError),
|
||||
),
|
||||
pytest.raises(ConnectionError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_UNLOCK,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.unlockCharger",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
@@ -80,8 +143,18 @@ async def test_wallbox_lock_class_error_handling(
|
||||
blocking=True,
|
||||
)
|
||||
with (
|
||||
patch.object(mock_wallbox, "lockCharger", side_effect=http_404_error),
|
||||
patch.object(mock_wallbox, "unlockCharger", side_effect=http_404_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.unlockCharger",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
@@ -92,24 +165,19 @@ async def test_wallbox_lock_class_error_handling(
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "lockCharger", side_effect=http_403_error),
|
||||
patch.object(mock_wallbox, "unlockCharger", side_effect=http_403_error),
|
||||
pytest.raises(InvalidAuth),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"lock",
|
||||
SERVICE_UNLOCK,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "lockCharger", side_effect=http_429_error),
|
||||
patch.object(mock_wallbox, "unlockCharger", side_effect=http_429_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.lockCharger",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.unlockCharger",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
"""Test Wallbox Switch component."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.input_number import ATTR_VALUE, SERVICE_SET_VALUE
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
|
||||
from homeassistant.components.wallbox.const import (
|
||||
CHARGER_ENERGY_PRICE_KEY,
|
||||
CHARGER_MAX_CHARGING_CURRENT_KEY,
|
||||
CHARGER_MAX_ICP_CURRENT_KEY,
|
||||
)
|
||||
from homeassistant.components.wallbox.coordinator import InvalidAuth
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .conftest import (
|
||||
from . import (
|
||||
authorisation_response,
|
||||
http_403_error,
|
||||
http_404_error,
|
||||
http_429_error,
|
||||
setup_integration,
|
||||
test_response_bidir,
|
||||
setup_integration_bidir,
|
||||
)
|
||||
from .const import (
|
||||
MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
@@ -26,87 +32,105 @@ from .const import (
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
mock_wallbox = Mock()
|
||||
mock_wallbox.authenticate = Mock(return_value=authorisation_response)
|
||||
mock_wallbox.setEnergyCost = Mock(return_value={CHARGER_ENERGY_PRICE_KEY: 1.1})
|
||||
mock_wallbox.setMaxChargingCurrent = Mock(
|
||||
return_value={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}
|
||||
)
|
||||
mock_wallbox.setIcpMaxCurrent = Mock(return_value={CHARGER_MAX_ICP_CURRENT_KEY: 10})
|
||||
|
||||
async def test_wallbox_number_power_class(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
|
||||
async def test_wallbox_number_class(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
|
||||
assert state.attributes["min"] == 6
|
||||
assert state.attributes["max"] == 25
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
|
||||
new=Mock(
|
||||
return_value={"data": {"chargerData": {"maxChargingCurrent": 20}}}
|
||||
),
|
||||
),
|
||||
):
|
||||
state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
|
||||
assert state.attributes["min"] == 6
|
||||
assert state.attributes["max"] == 25
|
||||
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID,
|
||||
ATTR_VALUE: 20,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID,
|
||||
ATTR_VALUE: 20,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_power_class_bidir(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
async def test_wallbox_number_class_bidir(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
with patch.object(
|
||||
mock_wallbox, "getChargerStatus", return_value=test_response_bidir
|
||||
):
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
|
||||
assert state.attributes["min"] == -25
|
||||
assert state.attributes["max"] == 25
|
||||
await setup_integration_bidir(hass, entry)
|
||||
|
||||
state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
|
||||
assert state.attributes["min"] == -25
|
||||
assert state.attributes["max"] == 25
|
||||
|
||||
|
||||
async def test_wallbox_number_energy_class(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_icp_power_class(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||
ATTR_VALUE: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_power_class_error_handling(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "setMaxChargingCurrent", side_effect=http_404_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
|
||||
new=Mock(return_value={CHARGER_ENERGY_PRICE_KEY: 1.1}),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_connection_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
@@ -119,8 +143,23 @@ async def test_wallbox_number_power_class_error_handling(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_too_many_requests(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "setMaxChargingCurrent", side_effect=http_429_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
@@ -133,8 +172,167 @@ async def test_wallbox_number_power_class_error_handling(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_energy_price_update_failed(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "setMaxChargingCurrent", side_effect=http_403_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_energy_price_update_connection_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_energy_price_auth_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_icp_energy(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
|
||||
new=Mock(return_value={"icp_max_current": 20}),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||
ATTR_VALUE: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_icp_energy_auth_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
pytest.raises(InvalidAuth),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||
ATTR_VALUE: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_class_energy_auth_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
|
||||
new=Mock(side_effect=http_403_error),
|
||||
),
|
||||
pytest.raises(InvalidAuth),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
@@ -148,93 +346,51 @@ async def test_wallbox_number_power_class_error_handling(
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_energy_class_error_handling(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
async def test_wallbox_number_class_icp_energy_connection_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "setEnergyCost", side_effect=http_429_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "setEnergyCost", side_effect=http_404_error),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "setEnergyCost", side_effect=http_429_error),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"number",
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
|
||||
ATTR_VALUE: 1.1,
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||
ATTR_VALUE: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_number_icp_power_class_error_handling(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
async def test_wallbox_number_class_icp_energy_too_many_request(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "setIcpMaxCurrent", side_effect=http_403_error),
|
||||
pytest.raises(InvalidAuth),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||
ATTR_VALUE: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "setIcpMaxCurrent", side_effect=http_404_error),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
|
||||
ATTR_VALUE: 10,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "setIcpMaxCurrent", side_effect=http_429_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Test Wallbox Select component."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -9,14 +9,15 @@ from homeassistant.components.select import (
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.components.wallbox.const import EcoSmartMode
|
||||
from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY, EcoSmartMode
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, HomeAssistantError
|
||||
|
||||
from .conftest import (
|
||||
from . import (
|
||||
authorisation_response,
|
||||
http_404_error,
|
||||
http_429_error,
|
||||
setup_integration,
|
||||
setup_integration_select,
|
||||
test_response,
|
||||
test_response_eco_mode,
|
||||
test_response_full_solar,
|
||||
@@ -33,13 +34,44 @@ TEST_OPTIONS = [
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_authenticate():
|
||||
"""Fixture to patch Wallbox methods."""
|
||||
with patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("mode", "response"), TEST_OPTIONS)
|
||||
async def test_wallbox_select_solar_charging_class(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mode, response, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mode, response, mock_authenticate
|
||||
) -> None:
|
||||
"""Test wallbox select class."""
|
||||
with patch.object(mock_wallbox, "getChargerStatus", return_value=response):
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
if mode == EcoSmartMode.OFF:
|
||||
response = test_response
|
||||
elif mode == EcoSmartMode.ECO_MODE:
|
||||
response = test_response_eco_mode
|
||||
elif mode == EcoSmartMode.FULL_SOLAR:
|
||||
response = test_response_full_solar
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.enableEcoSmart",
|
||||
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.disableEcoSmart",
|
||||
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(return_value=response),
|
||||
),
|
||||
):
|
||||
await setup_integration_select(hass, entry, response)
|
||||
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
@@ -56,35 +88,43 @@ async def test_wallbox_select_solar_charging_class(
|
||||
|
||||
|
||||
async def test_wallbox_select_no_power_boost_class(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox select class."""
|
||||
|
||||
with patch.object(
|
||||
mock_wallbox, "getChargerStatus", return_value=test_response_no_power_boost
|
||||
):
|
||||
await setup_integration(hass, entry)
|
||||
await setup_integration_select(hass, entry, test_response_no_power_boost)
|
||||
|
||||
state = hass.states.get(MOCK_SELECT_ENTITY_ID)
|
||||
assert state is None
|
||||
state = hass.states.get(MOCK_SELECT_ENTITY_ID)
|
||||
assert state is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("mode", "response"), TEST_OPTIONS)
|
||||
@pytest.mark.parametrize("error", [http_404_error, ConnectionError])
|
||||
async def test_wallbox_select_class_error(
|
||||
hass: HomeAssistant,
|
||||
entry: MockConfigEntry,
|
||||
mode,
|
||||
response,
|
||||
mock_wallbox,
|
||||
error,
|
||||
mock_authenticate,
|
||||
) -> None:
|
||||
"""Test wallbox select class connection error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
await setup_integration_select(hass, entry, response)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "getChargerStatus", return_value=response),
|
||||
patch.object(mock_wallbox, "disableEcoSmart", side_effect=http_404_error),
|
||||
patch.object(mock_wallbox, "enableEcoSmart", side_effect=http_404_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.disableEcoSmart",
|
||||
new=Mock(side_effect=error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.enableEcoSmart",
|
||||
new=Mock(side_effect=error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(return_value=test_response_eco_mode),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
@@ -104,45 +144,25 @@ async def test_wallbox_select_too_many_requests_error(
|
||||
entry: MockConfigEntry,
|
||||
mode,
|
||||
response,
|
||||
mock_wallbox,
|
||||
mock_authenticate,
|
||||
) -> None:
|
||||
"""Test wallbox select class connection error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
await setup_integration_select(hass, entry, response)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "getChargerStatus", return_value=response),
|
||||
patch.object(mock_wallbox, "disableEcoSmart", side_effect=http_429_error),
|
||||
patch.object(mock_wallbox, "enableEcoSmart", side_effect=http_429_error),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SELECT_ENTITY_ID,
|
||||
ATTR_OPTION: mode,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("mode", "response"), TEST_OPTIONS)
|
||||
async def test_wallbox_select_connection_error(
|
||||
hass: HomeAssistant,
|
||||
entry: MockConfigEntry,
|
||||
mode,
|
||||
response,
|
||||
mock_wallbox,
|
||||
) -> None:
|
||||
"""Test wallbox select class connection error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "getChargerStatus", return_value=response),
|
||||
patch.object(mock_wallbox, "disableEcoSmart", side_effect=ConnectionError),
|
||||
patch.object(mock_wallbox, "enableEcoSmart", side_effect=ConnectionError),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.disableEcoSmart",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.enableEcoSmart",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(return_value=test_response_eco_mode),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import setup_integration
|
||||
from . import setup_integration
|
||||
from .const import (
|
||||
MOCK_SENSOR_CHARGING_POWER_ID,
|
||||
MOCK_SENSOR_CHARGING_SPEED_ID,
|
||||
@@ -14,7 +14,7 @@ from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_wallbox_sensor_class(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox sensor class."""
|
||||
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
"""Test Wallbox Lock component."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||
from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .conftest import http_404_error, http_429_error, setup_integration
|
||||
from . import (
|
||||
authorisation_response,
|
||||
http_404_error,
|
||||
http_429_error,
|
||||
setup_integration,
|
||||
test_response,
|
||||
)
|
||||
from .const import MOCK_SWITCH_ENTITY_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_wallbox_switch_class(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox switch class."""
|
||||
|
||||
@@ -26,34 +33,59 @@ async def test_wallbox_switch_class(
|
||||
assert state
|
||||
assert state.state == "on"
|
||||
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
|
||||
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.resumeChargingSession",
|
||||
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
|
||||
new=Mock(return_value=test_response),
|
||||
),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
SERVICE_TURN_ON,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
SERVICE_TURN_OFF,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
"switch",
|
||||
SERVICE_TURN_OFF,
|
||||
{
|
||||
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_switch_class_error_handling(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, mock_wallbox
|
||||
async def test_wallbox_switch_class_connection_error(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox switch class connection error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "resumeChargingSession", side_effect=http_404_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.resumeChargingSession",
|
||||
new=Mock(side_effect=http_404_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
# Test behavior when a connection error occurs
|
||||
@@ -66,8 +98,23 @@ async def test_wallbox_switch_class_error_handling(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_wallbox_switch_class_too_many_requests(
|
||||
hass: HomeAssistant, entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test wallbox switch class connection error."""
|
||||
|
||||
await setup_integration(hass, entry)
|
||||
|
||||
with (
|
||||
patch.object(mock_wallbox, "resumeChargingSession", side_effect=http_429_error),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.authenticate",
|
||||
new=Mock(return_value=authorisation_response),
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.wallbox.Wallbox.resumeChargingSession",
|
||||
new=Mock(side_effect=http_429_error),
|
||||
),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
# Test behavior when a connection error occurs
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.components.websocket_api.auth import (
|
||||
TYPE_AUTH_REQUIRED,
|
||||
)
|
||||
from homeassistant.components.websocket_api.commands import (
|
||||
ALL_CONDITION_DESCRIPTIONS_JSON_CACHE,
|
||||
ALL_SERVICE_DESCRIPTIONS_JSON_CACHE,
|
||||
ALL_TRIGGER_DESCRIPTIONS_JSON_CACHE,
|
||||
)
|
||||
@@ -711,91 +710,6 @@ async def test_get_services(
|
||||
assert hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE] is old_cache
|
||||
|
||||
|
||||
@patch("annotatedyaml.loader.load_yaml")
|
||||
@patch.object(Integration, "has_conditions", return_value=True)
|
||||
async def test_subscribe_conditions(
|
||||
mock_has_conditions: Mock,
|
||||
mock_load_yaml: Mock,
|
||||
hass: HomeAssistant,
|
||||
websocket_client: MockHAClientWebSocket,
|
||||
) -> None:
|
||||
"""Test condition_platforms/subscribe command."""
|
||||
sun_condition_descriptions = """
|
||||
sun: {}
|
||||
"""
|
||||
device_automation_condition_descriptions = """
|
||||
device: {}
|
||||
"""
|
||||
|
||||
def _load_yaml(fname, secrets=None):
|
||||
if fname.endswith("device_automation/conditions.yaml"):
|
||||
condition_descriptions = device_automation_condition_descriptions
|
||||
elif fname.endswith("sun/conditions.yaml"):
|
||||
condition_descriptions = sun_condition_descriptions
|
||||
else:
|
||||
raise FileNotFoundError
|
||||
with io.StringIO(condition_descriptions) as file:
|
||||
return parse_yaml(file)
|
||||
|
||||
mock_load_yaml.side_effect = _load_yaml
|
||||
|
||||
assert await async_setup_component(hass, "sun", {})
|
||||
assert await async_setup_component(hass, "system_health", {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert ALL_CONDITION_DESCRIPTIONS_JSON_CACHE not in hass.data
|
||||
|
||||
await websocket_client.send_json_auto_id({"type": "condition_platforms/subscribe"})
|
||||
|
||||
# Test start subscription with initial event
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg == {"id": 1, "result": None, "success": True, "type": "result"}
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg == {"event": {"sun": {"fields": {}}}, "id": 1, "type": "event"}
|
||||
|
||||
old_cache = hass.data[ALL_CONDITION_DESCRIPTIONS_JSON_CACHE]
|
||||
|
||||
# Test we receive an event when a new platform is loaded, if it has descriptions
|
||||
assert await async_setup_component(hass, "calendar", {})
|
||||
assert await async_setup_component(hass, "device_automation", {})
|
||||
await hass.async_block_till_done()
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg == {
|
||||
"event": {"device": {"fields": {}}},
|
||||
"id": 1,
|
||||
"type": "event",
|
||||
}
|
||||
|
||||
# Initiate a second subscription to check the cache is updated because of the new
|
||||
# condition
|
||||
await websocket_client.send_json_auto_id({"type": "condition_platforms/subscribe"})
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg == {"id": 2, "result": None, "success": True, "type": "result"}
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg == {
|
||||
"event": {"device": {"fields": {}}, "sun": {"fields": {}}},
|
||||
"id": 2,
|
||||
"type": "event",
|
||||
}
|
||||
|
||||
assert hass.data[ALL_CONDITION_DESCRIPTIONS_JSON_CACHE] is not old_cache
|
||||
|
||||
# Initiate a third subscription to check the cache is not updated because no new
|
||||
# condition was added
|
||||
old_cache = hass.data[ALL_CONDITION_DESCRIPTIONS_JSON_CACHE]
|
||||
await websocket_client.send_json_auto_id({"type": "condition_platforms/subscribe"})
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg == {"id": 3, "result": None, "success": True, "type": "result"}
|
||||
msg = await websocket_client.receive_json()
|
||||
assert msg == {
|
||||
"event": {"device": {"fields": {}}, "sun": {"fields": {}}},
|
||||
"id": 3,
|
||||
"type": "event",
|
||||
}
|
||||
|
||||
assert hass.data[ALL_CONDITION_DESCRIPTIONS_JSON_CACHE] is old_cache
|
||||
|
||||
|
||||
@patch("annotatedyaml.loader.load_yaml")
|
||||
@patch.object(Integration, "has_triggers", return_value=True)
|
||||
async def test_subscribe_triggers(
|
||||
|
||||
+1
-1
@@ -1724,7 +1724,7 @@ async def async_test_recorder(
|
||||
wait_recorder: bool = True,
|
||||
wait_recorder_setup: bool = True,
|
||||
) -> AsyncGenerator[recorder.Recorder]:
|
||||
"""Setup and return recorder instance."""
|
||||
"""Setup and return recorder instance.""" # noqa: D401
|
||||
await _async_init_recorder_component(
|
||||
hass,
|
||||
config,
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
"""Test the condition helper."""
|
||||
|
||||
from datetime import timedelta
|
||||
import io
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
import pytest
|
||||
from pytest_unordered import unordered
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_automation import (
|
||||
DOMAIN as DOMAIN_DEVICE_AUTOMATION,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.sun import DOMAIN as DOMAIN_SUN
|
||||
from homeassistant.components.system_health import DOMAIN as DOMAIN_SYSTEM_HEALTH
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
CONF_CONDITION,
|
||||
@@ -34,12 +27,10 @@ from homeassistant.helpers import (
|
||||
)
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import Integration, async_get_integration
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.yaml.loader import parse_yaml
|
||||
|
||||
from tests.common import MockModule, MockPlatform, mock_integration, mock_platform
|
||||
from tests.common import MockModule, mock_integration, mock_platform
|
||||
|
||||
|
||||
def assert_element(trace_element, expected_element, path):
|
||||
@@ -2526,280 +2517,3 @@ async def test_or_condition_with_disabled_condition(hass: HomeAssistant) -> None
|
||||
"conditions/1/entity_id/0": [{"result": {"result": True, "state": 100.0}}],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sun_condition_descriptions",
|
||||
[
|
||||
"""
|
||||
sun:
|
||||
fields:
|
||||
after:
|
||||
example: sunrise
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- sunrise
|
||||
- sunset
|
||||
after_offset:
|
||||
selector:
|
||||
time: null
|
||||
before:
|
||||
example: sunrise
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- sunrise
|
||||
- sunset
|
||||
before_offset:
|
||||
selector:
|
||||
time: null
|
||||
""",
|
||||
"""
|
||||
.sunrise_sunset_selector: &sunrise_sunset_selector
|
||||
example: sunrise
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- sunrise
|
||||
- sunset
|
||||
.offset_selector: &offset_selector
|
||||
selector:
|
||||
time: null
|
||||
sun:
|
||||
fields:
|
||||
after: *sunrise_sunset_selector
|
||||
after_offset: *offset_selector
|
||||
before: *sunrise_sunset_selector
|
||||
before_offset: *offset_selector
|
||||
""",
|
||||
],
|
||||
)
|
||||
async def test_async_get_all_descriptions(
|
||||
hass: HomeAssistant, sun_condition_descriptions: str
|
||||
) -> None:
|
||||
"""Test async_get_all_descriptions."""
|
||||
device_automation_condition_descriptions = """
|
||||
device: {}
|
||||
"""
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN_SUN, {})
|
||||
assert await async_setup_component(hass, DOMAIN_SYSTEM_HEALTH, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
def _load_yaml(fname, secrets=None):
|
||||
if fname.endswith("device_automation/conditions.yaml"):
|
||||
condition_descriptions = device_automation_condition_descriptions
|
||||
elif fname.endswith("sun/conditions.yaml"):
|
||||
condition_descriptions = sun_condition_descriptions
|
||||
with io.StringIO(condition_descriptions) as file:
|
||||
return parse_yaml(file)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.helpers.condition._load_conditions_files",
|
||||
side_effect=condition._load_conditions_files,
|
||||
) as proxy_load_conditions_files,
|
||||
patch(
|
||||
"annotatedyaml.loader.load_yaml",
|
||||
side_effect=_load_yaml,
|
||||
),
|
||||
patch.object(Integration, "has_conditions", return_value=True),
|
||||
):
|
||||
descriptions = await condition.async_get_all_descriptions(hass)
|
||||
|
||||
# Test we only load conditions.yaml for integrations with conditions,
|
||||
# system_health has no conditions
|
||||
assert proxy_load_conditions_files.mock_calls[0][1][1] == unordered(
|
||||
[
|
||||
await async_get_integration(hass, DOMAIN_SUN),
|
||||
]
|
||||
)
|
||||
|
||||
# system_health does not have conditions and should not be in descriptions
|
||||
assert descriptions == {
|
||||
DOMAIN_SUN: {
|
||||
"fields": {
|
||||
"after": {
|
||||
"example": "sunrise",
|
||||
"selector": {"select": {"options": ["sunrise", "sunset"]}},
|
||||
},
|
||||
"after_offset": {"selector": {"time": None}},
|
||||
"before": {
|
||||
"example": "sunrise",
|
||||
"selector": {"select": {"options": ["sunrise", "sunset"]}},
|
||||
},
|
||||
"before_offset": {"selector": {"time": None}},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Verify the cache returns the same object
|
||||
assert await condition.async_get_all_descriptions(hass) is descriptions
|
||||
|
||||
# Load the device_automation integration and check a new cache object is created
|
||||
assert await async_setup_component(hass, DOMAIN_DEVICE_AUTOMATION, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"annotatedyaml.loader.load_yaml",
|
||||
side_effect=_load_yaml,
|
||||
),
|
||||
patch.object(Integration, "has_conditions", return_value=True),
|
||||
):
|
||||
new_descriptions = await condition.async_get_all_descriptions(hass)
|
||||
assert new_descriptions is not descriptions
|
||||
assert new_descriptions == {
|
||||
"device": {
|
||||
"fields": {},
|
||||
},
|
||||
DOMAIN_SUN: {
|
||||
"fields": {
|
||||
"after": {
|
||||
"example": "sunrise",
|
||||
"selector": {"select": {"options": ["sunrise", "sunset"]}},
|
||||
},
|
||||
"after_offset": {"selector": {"time": None}},
|
||||
"before": {
|
||||
"example": "sunrise",
|
||||
"selector": {"select": {"options": ["sunrise", "sunset"]}},
|
||||
},
|
||||
"before_offset": {"selector": {"time": None}},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Verify the cache returns the same object
|
||||
assert await condition.async_get_all_descriptions(hass) is new_descriptions
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("yaml_error", "expected_message"),
|
||||
[
|
||||
(
|
||||
FileNotFoundError("Blah"),
|
||||
"Unable to find conditions.yaml for the sun integration",
|
||||
),
|
||||
(
|
||||
HomeAssistantError("Test error"),
|
||||
"Unable to parse conditions.yaml for the sun integration: Test error",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_async_get_all_descriptions_with_yaml_error(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
yaml_error: Exception,
|
||||
expected_message: str,
|
||||
) -> None:
|
||||
"""Test async_get_all_descriptions."""
|
||||
assert await async_setup_component(hass, DOMAIN_SUN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
def _load_yaml_dict(fname, secrets=None):
|
||||
raise yaml_error
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.helpers.condition.load_yaml_dict",
|
||||
side_effect=_load_yaml_dict,
|
||||
),
|
||||
patch.object(Integration, "has_conditions", return_value=True),
|
||||
):
|
||||
descriptions = await condition.async_get_all_descriptions(hass)
|
||||
|
||||
assert descriptions == {DOMAIN_SUN: None}
|
||||
|
||||
assert expected_message in caplog.text
|
||||
|
||||
|
||||
async def test_async_get_all_descriptions_with_bad_description(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test async_get_all_descriptions."""
|
||||
sun_service_descriptions = """
|
||||
sun:
|
||||
fields: not_a_dict
|
||||
"""
|
||||
|
||||
assert await async_setup_component(hass, DOMAIN_SUN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
def _load_yaml(fname, secrets=None):
|
||||
with io.StringIO(sun_service_descriptions) as file:
|
||||
return parse_yaml(file)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"annotatedyaml.loader.load_yaml",
|
||||
side_effect=_load_yaml,
|
||||
),
|
||||
patch.object(Integration, "has_conditions", return_value=True),
|
||||
):
|
||||
descriptions = await condition.async_get_all_descriptions(hass)
|
||||
|
||||
assert descriptions == {DOMAIN_SUN: None}
|
||||
|
||||
assert (
|
||||
"Unable to parse conditions.yaml for the sun integration: "
|
||||
"expected a dictionary for dictionary value @ data['sun']['fields']"
|
||||
) in caplog.text
|
||||
|
||||
|
||||
async def test_invalid_condition_platform(
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test invalid condition platform."""
|
||||
mock_integration(hass, MockModule("test", async_setup=AsyncMock(return_value=True)))
|
||||
mock_platform(hass, "test.condition", MockPlatform())
|
||||
|
||||
await async_setup_component(hass, "test", {})
|
||||
|
||||
assert (
|
||||
"Integration test does not provide condition support, skipping" in caplog.text
|
||||
)
|
||||
|
||||
|
||||
@patch("annotatedyaml.loader.load_yaml")
|
||||
@patch.object(Integration, "has_conditions", return_value=True)
|
||||
async def test_subscribe_conditions(
|
||||
mock_has_conditions: Mock,
|
||||
mock_load_yaml: Mock,
|
||||
hass: HomeAssistant,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test condition.async_subscribe_platform_events."""
|
||||
sun_condition_descriptions = """
|
||||
sun: {}
|
||||
"""
|
||||
|
||||
def _load_yaml(fname, secrets=None):
|
||||
if fname.endswith("sun/conditions.yaml"):
|
||||
condition_descriptions = sun_condition_descriptions
|
||||
else:
|
||||
raise FileNotFoundError
|
||||
with io.StringIO(condition_descriptions) as file:
|
||||
return parse_yaml(file)
|
||||
|
||||
mock_load_yaml.side_effect = _load_yaml
|
||||
|
||||
async def broken_subscriber(_):
|
||||
"""Simulate a broken subscriber."""
|
||||
raise Exception("Boom!") # noqa: TRY002
|
||||
|
||||
condition_events = []
|
||||
|
||||
async def good_subscriber(new_conditions: set[str]):
|
||||
"""Simulate a working subscriber."""
|
||||
condition_events.append(new_conditions)
|
||||
|
||||
condition.async_subscribe_platform_events(hass, broken_subscriber)
|
||||
condition.async_subscribe_platform_events(hass, good_subscriber)
|
||||
|
||||
assert await async_setup_component(hass, "sun", {})
|
||||
|
||||
assert condition_events == [{"sun"}]
|
||||
assert "Error while notifying condition platform listener" in caplog.text
|
||||
|
||||
@@ -2713,41 +2713,6 @@ async def test_platform_state(
|
||||
assert hass.states.get("test.test") is None
|
||||
|
||||
|
||||
async def test_platform_state_no_platform(hass: HomeAssistant) -> None:
|
||||
"""Test platform state for entities which are not added by an entity platform."""
|
||||
|
||||
class MockEntity(entity.Entity):
|
||||
entity_id = "test.test"
|
||||
|
||||
def async_set_state(self, state: str) -> None:
|
||||
self._attr_state = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
ent = MockEntity()
|
||||
ent.hass = hass
|
||||
assert hass.states.get("test.test") is None
|
||||
|
||||
# The attempt to write when in state NOT_ADDED should be allowed
|
||||
assert ent._platform_state == entity.EntityPlatformState.NOT_ADDED
|
||||
ent.async_set_state("not_added")
|
||||
assert hass.states.get("test.test").state == "not_added"
|
||||
|
||||
# The attempt to write when in state ADDING should be allowed
|
||||
ent._platform_state = entity.EntityPlatformState.ADDING
|
||||
ent.async_set_state("adding")
|
||||
assert hass.states.get("test.test").state == "adding"
|
||||
|
||||
# The attempt to write when in state ADDED should be allowed
|
||||
ent._platform_state = entity.EntityPlatformState.ADDED
|
||||
ent.async_set_state("added")
|
||||
assert hass.states.get("test.test").state == "added"
|
||||
|
||||
# The attempt to write when in state REMOVED should be ignored
|
||||
ent._platform_state = entity.EntityPlatformState.REMOVED
|
||||
ent.async_set_state("removed")
|
||||
assert hass.states.get("test.test").state == "added"
|
||||
|
||||
|
||||
async def test_platform_state_fail_to_add(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
) -> None:
|
||||
|
||||
@@ -396,13 +396,7 @@ def test_assist_pipeline_selector_schema(
|
||||
({"min": -100, "max": 100, "step": 5}, (), ()),
|
||||
({"min": -20, "max": -10, "mode": "box"}, (), ()),
|
||||
(
|
||||
{
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"unit_of_measurement": "seconds",
|
||||
"mode": "slider",
|
||||
"translation_key": "foo",
|
||||
},
|
||||
{"min": 0, "max": 100, "unit_of_measurement": "seconds", "mode": "slider"},
|
||||
(),
|
||||
(),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user