mirror of
https://github.com/home-assistant/core.git
synced 2025-04-22 16:27:56 +00:00
Merge branch 'dev' into dev
This commit is contained in:
commit
ce5dd8c83a
@ -234,6 +234,7 @@ homeassistant.components.here_travel_time.*
|
||||
homeassistant.components.history.*
|
||||
homeassistant.components.history_stats.*
|
||||
homeassistant.components.holiday.*
|
||||
homeassistant.components.home_connect.*
|
||||
homeassistant.components.homeassistant.*
|
||||
homeassistant.components.homeassistant_alerts.*
|
||||
homeassistant.components.homeassistant_green.*
|
||||
|
38
Dockerfile
generated
38
Dockerfile
generated
@ -12,8 +12,26 @@ ENV \
|
||||
|
||||
ARG QEMU_CPU
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||
ARG BUILD_ARCH
|
||||
# Get go2rtc binary
|
||||
RUN \
|
||||
case "${BUILD_ARCH}" in \
|
||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||
"armhf") go2rtc_suffix='armv6' ;; \
|
||||
"armv7") go2rtc_suffix='arm' ;; \
|
||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||
esac \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.8/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||
&& chmod +x /bin/go2rtc \
|
||||
# Verify go2rtc can be executed
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv==0.6.0
|
||||
RUN pip3 install uv==0.6.1
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
@ -42,22 +60,4 @@ RUN \
|
||||
&& python3 -m compileall \
|
||||
homeassistant/homeassistant
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||
ARG BUILD_ARCH
|
||||
# Get go2rtc binary
|
||||
RUN \
|
||||
case "${BUILD_ARCH}" in \
|
||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||
"armhf") go2rtc_suffix='armv6' ;; \
|
||||
"armv7") go2rtc_suffix='arm' ;; \
|
||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||
esac \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.8/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||
&& chmod +x /bin/go2rtc \
|
||||
# Verify go2rtc can be executed
|
||||
&& go2rtc --version
|
||||
|
||||
WORKDIR /config
|
||||
|
10
build.yaml
10
build.yaml
@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.12.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.12.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.12.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.12.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.12.0
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.02.1
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.02.1
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.02.1
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.02.1
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.02.1
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
@ -83,6 +83,7 @@ class AirQConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(device_info["id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
_LOGGER.debug("Creating an entry for %s", device_info["name"])
|
||||
return self.async_create_entry(title=device_info["name"], data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aioairq import AirQ
|
||||
from aioairq.core import AirQ, identify_warming_up_sensors
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
||||
@ -55,6 +55,9 @@ class AirQCoordinator(DataUpdateCoordinator):
|
||||
async def _async_update_data(self) -> dict:
|
||||
"""Fetch the data from the device."""
|
||||
if "name" not in self.device_info:
|
||||
_LOGGER.debug(
|
||||
"'name' not found in AirQCoordinator.device_info, fetching from the device"
|
||||
)
|
||||
info = await self.airq.fetch_device_info()
|
||||
self.device_info.update(
|
||||
DeviceInfo(
|
||||
@ -64,7 +67,16 @@ class AirQCoordinator(DataUpdateCoordinator):
|
||||
hw_version=info["hw_version"],
|
||||
)
|
||||
)
|
||||
return await self.airq.get_latest_data( # type: ignore[no-any-return]
|
||||
_LOGGER.debug(
|
||||
"Updated AirQCoordinator.device_info for 'name' %s",
|
||||
self.device_info.get("name"),
|
||||
)
|
||||
data: dict = await self.airq.get_latest_data(
|
||||
return_average=self.return_average,
|
||||
clip_negative_values=self.clip_negative,
|
||||
)
|
||||
if warming_up_sensors := identify_warming_up_sensors(data):
|
||||
_LOGGER.debug(
|
||||
"Following sensors are still warming up: %s", warming_up_sensors
|
||||
)
|
||||
return data
|
||||
|
@ -10,7 +10,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"cannot_receive_deviceinfo": "Failed to retreive MAC Address. Make sure the device is turned on"
|
||||
"cannot_receive_deviceinfo": "Failed to retrieve MAC Address. Make sure the device is turned on"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from aranet4.client import Aranet4Advertisement
|
||||
from aranet4.client import Aranet4Advertisement, Color
|
||||
from bleak.backends.device import BLEDevice
|
||||
|
||||
from homeassistant.components.bluetooth.passive_update_processor import (
|
||||
@ -74,6 +74,13 @@ SENSOR_DESCRIPTIONS = {
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"status": AranetSensorEntityDescription(
|
||||
key="threshold",
|
||||
translation_key="threshold",
|
||||
name="Threshold",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[status.name.lower() for status in Color],
|
||||
),
|
||||
"co2": AranetSensorEntityDescription(
|
||||
key="co2",
|
||||
name="Carbon Dioxide",
|
||||
@ -161,7 +168,10 @@ def sensor_update_to_bluetooth_data_update(
|
||||
val = getattr(adv.readings, key)
|
||||
if val == -1:
|
||||
continue
|
||||
val *= desc.scale
|
||||
if key == "status":
|
||||
val = val.name.lower()
|
||||
else:
|
||||
val *= desc.scale
|
||||
data[tag] = val
|
||||
names[tag] = desc.name
|
||||
descs[tag] = desc
|
||||
|
@ -21,5 +21,17 @@
|
||||
"no_devices_found": "No unconfigured Aranet devices found.",
|
||||
"outdated_version": "This device is using outdated firmware. Please update it to at least v1.2.0 and try again."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"threshold": {
|
||||
"state": {
|
||||
"error": "Error",
|
||||
"green": "Green",
|
||||
"yellow": "Yellow",
|
||||
"red": "Red"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Unable to connect, please check serial port, address, electrical connection and that inverter is on (in daylight)",
|
||||
"invalid_serial_port": "Serial port is not a valid device or could not be openned",
|
||||
"invalid_serial_port": "Serial port is not a valid device or could not be opened",
|
||||
"cannot_open_serial_port": "Cannot open serial port, please check and try again"
|
||||
},
|
||||
"abort": {
|
||||
|
@ -3,11 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.humidifier import (
|
||||
DEFAULT_MAX_HUMIDITY,
|
||||
DEFAULT_MIN_HUMIDITY,
|
||||
MODE_AUTO,
|
||||
HumidifierAction,
|
||||
HumidifierDeviceClass,
|
||||
HumidifierEntity,
|
||||
HumidifierEntityFeature,
|
||||
@ -41,6 +43,12 @@ async def async_setup_entry(
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
ECOBEE_HUMIDIFIER_ACTION_TO_HASS = {
|
||||
"humidifier": HumidifierAction.HUMIDIFYING,
|
||||
"dehumidifier": HumidifierAction.DRYING,
|
||||
}
|
||||
|
||||
|
||||
class EcobeeHumidifier(HumidifierEntity):
|
||||
"""A humidifier class for an ecobee thermostat with humidifier attached."""
|
||||
|
||||
@ -52,7 +60,7 @@ class EcobeeHumidifier(HumidifierEntity):
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, data, thermostat_index):
|
||||
def __init__(self, data, thermostat_index) -> None:
|
||||
"""Initialize ecobee humidifier platform."""
|
||||
self.data = data
|
||||
self.thermostat_index = thermostat_index
|
||||
@ -80,11 +88,11 @@ class EcobeeHumidifier(HumidifierEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
def available(self) -> bool:
|
||||
"""Return if device is available."""
|
||||
return self.thermostat["runtime"]["connected"]
|
||||
|
||||
async def async_update(self):
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest state from the thermostat."""
|
||||
if self.update_without_throttle:
|
||||
await self.data.update(no_throttle=True)
|
||||
@ -96,12 +104,20 @@ class EcobeeHumidifier(HumidifierEntity):
|
||||
self._last_humidifier_on_mode = self.mode
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def action(self) -> HumidifierAction:
|
||||
"""Return the current action."""
|
||||
for status in self.thermostat["equipmentStatus"].split(","):
|
||||
if status in ECOBEE_HUMIDIFIER_ACTION_TO_HASS:
|
||||
return ECOBEE_HUMIDIFIER_ACTION_TO_HASS[status]
|
||||
return HumidifierAction.IDLE if self.is_on else HumidifierAction.OFF
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the humidifier is on."""
|
||||
return self.mode != MODE_OFF
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
def mode(self) -> str:
|
||||
"""Return the current mode, e.g., off, auto, manual."""
|
||||
return self.thermostat["settings"]["humidifierMode"]
|
||||
|
||||
@ -118,9 +134,11 @@ class EcobeeHumidifier(HumidifierEntity):
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def set_mode(self, mode):
|
||||
def set_mode(self, mode: str) -> None:
|
||||
"""Set humidifier mode (auto, off, manual)."""
|
||||
if mode.lower() not in (self.available_modes):
|
||||
if self.available_modes is None:
|
||||
raise NotImplementedError("Humidifier does not support modes.")
|
||||
if mode.lower() not in self.available_modes:
|
||||
raise ValueError(
|
||||
f"Invalid mode value: {mode} Valid values are"
|
||||
f" {', '.join(self.available_modes)}."
|
||||
@ -134,10 +152,10 @@ class EcobeeHumidifier(HumidifierEntity):
|
||||
self.data.ecobee.set_humidity(self.thermostat_index, humidity)
|
||||
self.update_without_throttle = True
|
||||
|
||||
def turn_off(self, **kwargs):
|
||||
def turn_off(self, **kwargs: Any) -> None:
|
||||
"""Set humidifier to off mode."""
|
||||
self.set_mode(MODE_OFF)
|
||||
|
||||
def turn_on(self, **kwargs):
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Set humidifier to on mode."""
|
||||
self.set_mode(self._last_humidifier_on_mode)
|
||||
|
@ -19,7 +19,7 @@
|
||||
},
|
||||
"abort": {
|
||||
"metering_point_id_already_configured": "Metering point with ID `{metering_point_id}` is already configured.",
|
||||
"no_metering_points": "The provived API token has no metering points."
|
||||
"no_metering_points": "The provided API token has no metering points."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"api_error": "An error occured in the pyemoncms API : {details}"
|
||||
"api_error": "An error occurred in the pyemoncms API : {details}"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
@ -57,7 +57,7 @@
|
||||
"init": {
|
||||
"title": "Envoy {serial} {host} options",
|
||||
"data": {
|
||||
"diagnostics_include_fixtures": "Include test fixture data in diagnostic report. Use when requested to provide test data for troubleshooting or development activies. With this option enabled the diagnostic report may take more time to download. When report is created best disable this option again.",
|
||||
"diagnostics_include_fixtures": "Include test fixture data in diagnostic report. Use when requested to provide test data for troubleshooting or development activities. With this option enabled the diagnostic report may take more time to download. When report is created best disable this option again.",
|
||||
"disable_keep_alive": "Always use a new connection when requesting data from the Envoy. May resolve communication issues with some Envoy firmwares."
|
||||
},
|
||||
"data_description": {
|
||||
|
@ -36,7 +36,7 @@
|
||||
"issues": {
|
||||
"import_yaml_error_url_error": {
|
||||
"title": "The Feedreader YAML configuration import failed",
|
||||
"description": "Configuring the Feedreader using YAML is being removed but there was a connection error when trying to import the YAML configuration for `{url}`.\n\nPlease verify that url is reachable and accessable for Home Assistant and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually."
|
||||
"description": "Configuring the Feedreader using YAML is being removed but there was a connection error when trying to import the YAML configuration for `{url}`.\n\nPlease verify that url is reachable and accessible for Home Assistant and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -548,7 +548,7 @@
|
||||
},
|
||||
"cancel_quest": {
|
||||
"name": "Cancel a pending quest",
|
||||
"description": "Cancels a quest that has not yet startet. All accepted and pending invitations will be canceled and the quest roll returned to the owner's inventory. Only quest leader or group leader can perform this action.",
|
||||
"description": "Cancels a quest that has not yet started. All accepted and pending invitations will be canceled and the quest roll returned to the owner's inventory. Only quest leader or group leader can perform this action.",
|
||||
"fields": {
|
||||
"config_entry": {
|
||||
"name": "[%key:component::habitica::common::config_entry_name%]",
|
||||
|
@ -3,5 +3,6 @@
|
||||
ATTR_PASSWORD = "password"
|
||||
ATTR_USERNAME = "username"
|
||||
DOMAIN = "heos"
|
||||
SERVICE_GROUP_VOLUME_SET = "group_volume_set"
|
||||
SERVICE_SIGN_IN = "sign_in"
|
||||
SERVICE_SIGN_OUT = "sign_out"
|
||||
|
@ -1,5 +1,8 @@
|
||||
{
|
||||
"services": {
|
||||
"group_volume_set": {
|
||||
"service": "mdi:volume-medium"
|
||||
},
|
||||
"sign_in": {
|
||||
"service": "mdi:login"
|
||||
},
|
||||
|
@ -17,10 +17,12 @@ from pyheos import (
|
||||
RepeatType,
|
||||
const as heos_const,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
ATTR_MEDIA_VOLUME_LEVEL,
|
||||
BrowseMedia,
|
||||
MediaPlayerEnqueue,
|
||||
MediaPlayerEntity,
|
||||
@ -33,13 +35,17 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
entity_platform,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import DOMAIN as HEOS_DOMAIN
|
||||
from .const import DOMAIN as HEOS_DOMAIN, SERVICE_GROUP_VOLUME_SET
|
||||
from .coordinator import HeosConfigEntry, HeosCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
@ -93,6 +99,13 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add media players for a config entry."""
|
||||
# Register custom entity services
|
||||
platform = entity_platform.async_get_current_platform()
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
{vol.Required(ATTR_MEDIA_VOLUME_LEVEL): cv.small_float},
|
||||
"async_set_group_volume_level",
|
||||
)
|
||||
|
||||
def add_entities_callback(players: Sequence[HeosPlayer]) -> None:
|
||||
"""Add entities for each player."""
|
||||
@ -346,6 +359,19 @@ class HeosMediaPlayer(CoordinatorEntity[HeosCoordinator], MediaPlayerEntity):
|
||||
"""Set volume level, range 0..1."""
|
||||
await self._player.set_volume(int(volume * 100))
|
||||
|
||||
@catch_action_error("set group volume level")
|
||||
async def async_set_group_volume_level(self, volume_level: float) -> None:
|
||||
"""Set group volume level."""
|
||||
if self._player.group_id is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=HEOS_DOMAIN,
|
||||
translation_key="entity_not_grouped",
|
||||
translation_placeholders={"entity_id": self.entity_id},
|
||||
)
|
||||
await self.coordinator.heos.set_group_volume(
|
||||
self._player.group_id, int(volume_level * 100)
|
||||
)
|
||||
|
||||
@catch_action_error("join players")
|
||||
async def async_join_players(self, group_members: list[str]) -> None:
|
||||
"""Join `group_members` as a player group with the current player."""
|
||||
|
@ -1,3 +1,17 @@
|
||||
group_volume_set:
|
||||
target:
|
||||
entity:
|
||||
integration: heos
|
||||
domain: media_player
|
||||
fields:
|
||||
volume_level:
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
max: 1
|
||||
step: 0.01
|
||||
|
||||
sign_in:
|
||||
fields:
|
||||
username:
|
||||
|
@ -8,7 +8,7 @@
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Host name or IP address of a HEOS-capable product (preferrably one connected via wire to the network)."
|
||||
"host": "Host name or IP address of a HEOS-capable product (preferably one connected via wire to the network)."
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
@ -71,6 +71,16 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"group_volume_set": {
|
||||
"name": "Set group volume",
|
||||
"description": "Sets the group's volume while preserving member volume ratios.",
|
||||
"fields": {
|
||||
"volume_level": {
|
||||
"name": "Level",
|
||||
"description": "The volume. 0 is inaudible, 1 is the maximum volume."
|
||||
}
|
||||
}
|
||||
},
|
||||
"sign_in": {
|
||||
"name": "Sign in",
|
||||
"description": "Signs in to a HEOS account.",
|
||||
@ -94,6 +104,9 @@
|
||||
"action_error": {
|
||||
"message": "Unable to {action}: {error}"
|
||||
},
|
||||
"entity_not_grouped": {
|
||||
"message": "Entity {entity_id} is not joined to a group"
|
||||
},
|
||||
"entity_not_found": {
|
||||
"message": "Entity {entity_id} was not found"
|
||||
},
|
||||
|
@ -24,7 +24,7 @@
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"description": "Read the documention for further details on how to configure the history stats sensor using these options.",
|
||||
"description": "Read the documentation for further details on how to configure the history stats sensor using these options.",
|
||||
"data": {
|
||||
"start": "Start",
|
||||
"end": "End",
|
||||
|
@ -237,7 +237,7 @@ async def _get_client_and_ha_id(
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
|
||||
"""Set up Home Connect component."""
|
||||
|
||||
async def _async_service_program(call: ServiceCall, start: bool):
|
||||
async def _async_service_program(call: ServiceCall, start: bool) -> None:
|
||||
"""Execute calls to services taking a program."""
|
||||
program = call.data[ATTR_PROGRAM]
|
||||
client, ha_id = await _get_client_and_ha_id(hass, call.data[ATTR_DEVICE_ID])
|
||||
@ -323,7 +323,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
},
|
||||
) from err
|
||||
|
||||
async def _async_service_set_program_options(call: ServiceCall, active: bool):
|
||||
async def _async_service_set_program_options(
|
||||
call: ServiceCall, active: bool
|
||||
) -> None:
|
||||
"""Execute calls to services taking a program."""
|
||||
option_key = call.data[ATTR_KEY]
|
||||
value = call.data[ATTR_VALUE]
|
||||
@ -396,7 +398,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
},
|
||||
) from err
|
||||
|
||||
async def _async_service_command(call: ServiceCall, command_key: CommandKey):
|
||||
async def _async_service_command(
|
||||
call: ServiceCall, command_key: CommandKey
|
||||
) -> None:
|
||||
"""Execute calls to services executing a command."""
|
||||
client, ha_id = await _get_client_and_ha_id(hass, call.data[ATTR_DEVICE_ID])
|
||||
|
||||
@ -412,15 +416,15 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_service_option_active(call: ServiceCall):
|
||||
async def async_service_option_active(call: ServiceCall) -> None:
|
||||
"""Service for setting an option for an active program."""
|
||||
await _async_service_set_program_options(call, True)
|
||||
|
||||
async def async_service_option_selected(call: ServiceCall):
|
||||
async def async_service_option_selected(call: ServiceCall) -> None:
|
||||
"""Service for setting an option for a selected program."""
|
||||
await _async_service_set_program_options(call, False)
|
||||
|
||||
async def async_service_setting(call: ServiceCall):
|
||||
async def async_service_setting(call: ServiceCall) -> None:
|
||||
"""Service for changing a setting."""
|
||||
key = call.data[ATTR_KEY]
|
||||
value = call.data[ATTR_VALUE]
|
||||
@ -439,19 +443,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_service_pause_program(call: ServiceCall):
|
||||
async def async_service_pause_program(call: ServiceCall) -> None:
|
||||
"""Service for pausing a program."""
|
||||
await _async_service_command(call, CommandKey.BSH_COMMON_PAUSE_PROGRAM)
|
||||
|
||||
async def async_service_resume_program(call: ServiceCall):
|
||||
async def async_service_resume_program(call: ServiceCall) -> None:
|
||||
"""Service for resuming a paused program."""
|
||||
await _async_service_command(call, CommandKey.BSH_COMMON_RESUME_PROGRAM)
|
||||
|
||||
async def async_service_select_program(call: ServiceCall):
|
||||
async def async_service_select_program(call: ServiceCall) -> None:
|
||||
"""Service for selecting a program."""
|
||||
await _async_service_program(call, False)
|
||||
|
||||
async def async_service_set_program_and_options(call: ServiceCall):
|
||||
async def async_service_set_program_and_options(call: ServiceCall) -> None:
|
||||
"""Service for setting a program and options."""
|
||||
data = dict(call.data)
|
||||
program = data.pop(ATTR_PROGRAM, None)
|
||||
@ -521,7 +525,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_service_start_program(call: ServiceCall):
|
||||
async def async_service_start_program(call: ServiceCall) -> None:
|
||||
"""Service for starting a program."""
|
||||
await _async_service_program(call, True)
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
"""API for Home Connect bound to HASS OAuth."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from aiohomeconnect.client import AbstractAuth
|
||||
from aiohomeconnect.const import API_ENDPOINT
|
||||
|
||||
@ -25,4 +27,4 @@ class AsyncConfigEntryAuth(AbstractAuth):
|
||||
"""Return a valid access token."""
|
||||
await self.session.async_ensure_token_valid()
|
||||
|
||||
return self.session.token["access_token"]
|
||||
return cast(str, self.session.token["access_token"])
|
||||
|
@ -3,7 +3,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import cast
|
||||
|
||||
from aiohomeconnect.model import StatusKey
|
||||
from aiohomeconnect.model import EventKey, StatusKey
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@ -12,6 +12,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@ -93,12 +94,24 @@ BINARY_SENSORS = (
|
||||
key=StatusKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_LOST,
|
||||
translation_key="lost",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=StatusKey.REFRIGERATION_COMMON_DOOR_BOTTLE_COOLER,
|
||||
boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
translation_key="bottle_cooler_door",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=StatusKey.REFRIGERATION_COMMON_DOOR_CHILLER_COMMON,
|
||||
boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
translation_key="chiller_door",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=StatusKey.REFRIGERATION_COMMON_DOOR_FLEX_COMPARTMENT,
|
||||
boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
translation_key="flex_compartment_door",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=StatusKey.REFRIGERATION_COMMON_DOOR_FREEZER,
|
||||
boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
|
||||
@ -111,6 +124,17 @@ BINARY_SENSORS = (
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
translation_key="refrigerator_door",
|
||||
),
|
||||
HomeConnectBinarySensorEntityDescription(
|
||||
key=StatusKey.REFRIGERATION_COMMON_DOOR_WINE_COMPARTMENT,
|
||||
boolean_map=REFRIGERATION_DOOR_BOOLEAN_MAP,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
translation_key="wine_compartment_door",
|
||||
),
|
||||
)
|
||||
|
||||
CONNECTED_BINARY_ENTITY_DESCRIPTION = BinarySensorEntityDescription(
|
||||
key=EventKey.BSH_COMMON_APPLIANCE_CONNECTED,
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
)
|
||||
|
||||
|
||||
@ -119,7 +143,11 @@ def _get_entities_for_appliance(
|
||||
appliance: HomeConnectApplianceData,
|
||||
) -> list[HomeConnectEntity]:
|
||||
"""Get a list of entities."""
|
||||
entities: list[HomeConnectEntity] = []
|
||||
entities: list[HomeConnectEntity] = [
|
||||
HomeConnectConnectivityBinarySensor(
|
||||
entry.runtime_data, appliance, CONNECTED_BINARY_ENTITY_DESCRIPTION
|
||||
)
|
||||
]
|
||||
entities.extend(
|
||||
HomeConnectBinarySensor(entry.runtime_data, appliance, description)
|
||||
for description in BINARY_SENSORS
|
||||
@ -159,6 +187,21 @@ class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity):
|
||||
self._attr_is_on = None
|
||||
|
||||
|
||||
class HomeConnectConnectivityBinarySensor(HomeConnectEntity, BinarySensorEntity):
|
||||
"""Binary sensor for Home Connect appliance's connection status."""
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
def update_native_value(self) -> None:
|
||||
"""Set the native value of the binary sensor."""
|
||||
self._attr_is_on = self.appliance.info.connected
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return the availability."""
|
||||
return self.coordinator.last_update_success
|
||||
|
||||
|
||||
class HomeConnectDoorBinarySensor(HomeConnectBinarySensor):
|
||||
"""Binary sensor for Home Connect Generic Door."""
|
||||
|
||||
|
@ -254,7 +254,7 @@ class HomeConnectCoordinator(
|
||||
await self.async_refresh()
|
||||
|
||||
@callback
|
||||
def _call_event_listener(self, event_message: EventMessage):
|
||||
def _call_event_listener(self, event_message: EventMessage) -> None:
|
||||
"""Call listener for event."""
|
||||
for event in event_message.data.items:
|
||||
for listener in self.context_listeners.get(
|
||||
@ -263,7 +263,7 @@ class HomeConnectCoordinator(
|
||||
listener()
|
||||
|
||||
@callback
|
||||
def _call_all_event_listeners_for_appliance(self, ha_id: str):
|
||||
def _call_all_event_listeners_for_appliance(self, ha_id: str) -> None:
|
||||
for listener, context in self._listeners.values():
|
||||
if isinstance(context, tuple) and context[0] == ha_id:
|
||||
listener()
|
||||
|
@ -76,6 +76,16 @@ NUMBERS = (
|
||||
device_class=NumberDeviceClass.TEMPERATURE,
|
||||
translation_key="wine_compartment_3_setpoint_temperature",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key=SettingKey.LAUNDRY_CARE_WASHER_I_DOS_1_BASE_LEVEL,
|
||||
device_class=NumberDeviceClass.VOLUME,
|
||||
translation_key="washer_i_dos_1_base_level",
|
||||
),
|
||||
NumberEntityDescription(
|
||||
key=SettingKey.LAUNDRY_CARE_WASHER_I_DOS_2_BASE_LEVEL,
|
||||
device_class=NumberDeviceClass.VOLUME,
|
||||
translation_key="washer_i_dos_2_base_level",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@ -12,7 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTime, UnitOfVolume
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
@ -99,16 +99,19 @@ SENSORS = (
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_COFFEE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="coffee_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_POWDER_COFFEE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="powder_coffee_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_WATER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfVolume.MILLILITERS,
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
@ -116,31 +119,37 @@ SENSORS = (
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_WATER_CUPS,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="hot_water_cups_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_HOT_MILK,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="hot_milk_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_FROTHY_MILK,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="frothy_milk_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_MILK,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="milk_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_COFFEE_AND_MILK,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="coffee_and_milk_counter",
|
||||
),
|
||||
HomeConnectSensorEntityDescription(
|
||||
key=StatusKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEVERAGE_COUNTER_RISTRETTO_ESPRESSO,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
translation_key="ristretto_espresso_counter",
|
||||
),
|
||||
|
@ -793,14 +793,23 @@
|
||||
"lost": {
|
||||
"name": "Lost"
|
||||
},
|
||||
"bottle_cooler_door": {
|
||||
"name": "Bottle cooler door"
|
||||
},
|
||||
"chiller_door": {
|
||||
"name": "Chiller door"
|
||||
},
|
||||
"flex_compartment_door": {
|
||||
"name": "Flex compartment door"
|
||||
},
|
||||
"freezer_door": {
|
||||
"name": "Freezer door"
|
||||
},
|
||||
"refrigerator_door": {
|
||||
"name": "Refrigerator door"
|
||||
},
|
||||
"wine_compartment_door": {
|
||||
"name": "Wine compartment door"
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
@ -844,6 +853,12 @@
|
||||
},
|
||||
"wine_compartment_3_setpoint_temperature": {
|
||||
"name": "Wine compartment 3 temperature"
|
||||
},
|
||||
"washer_i_dos_1_base_level": {
|
||||
"name": "i-Dos 1 base level"
|
||||
},
|
||||
"washer_i_dos_2_base_level": {
|
||||
"name": "i-Dos 2 base level"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
|
@ -44,7 +44,7 @@
|
||||
},
|
||||
"no_platform_setup": {
|
||||
"title": "Unused YAML configuration for the {platform} integration",
|
||||
"description": "It's not possible to configure {platform} {domain} by adding `{platform_key}` to the {domain} configuration. Please check the documentation for more information on how to set up this integration.\n\nTo resolve this:\n1. Remove `{platform_key}` occurences from the `{domain}:` configuration in your YAML configuration file.\n2. Restart Home Assistant.\n\nExample that should be removed:\n{yaml_example}"
|
||||
"description": "It's not possible to configure {platform} {domain} by adding `{platform_key}` to the {domain} configuration. Please check the documentation for more information on how to set up this integration.\n\nTo resolve this:\n1. Remove `{platform_key}` occurrences from the `{domain}:` configuration in your YAML configuration file.\n2. Restart Home Assistant.\n\nExample that should be removed:\n{yaml_example}"
|
||||
},
|
||||
"storage_corruption": {
|
||||
"title": "Storage corruption detected for {storage_key}",
|
||||
|
@ -14,7 +14,7 @@
|
||||
},
|
||||
"step": {
|
||||
"import_finish": {
|
||||
"description": "The existing YAML configuration has succesfully been imported.\n\nYou can now remove the `homeworks` configuration from your configuration.yaml file."
|
||||
"description": "The existing YAML configuration has successfully been imported.\n\nYou can now remove the `homeworks` configuration from your configuration.yaml file."
|
||||
},
|
||||
"import_controller_name": {
|
||||
"description": "Lutron Homeworks is no longer configured through configuration.yaml.\n\nPlease fill in the form to import the existing configuration to the UI.",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up madVR Envy",
|
||||
"description": "Your device needs to be on in order to add the integation.",
|
||||
"description": "Your device needs to be on in order to add the integration.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
@ -15,7 +15,7 @@
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Reconfigure madVR Envy",
|
||||
"description": "Your device needs to be on in order to reconfigure the integation.",
|
||||
"description": "Your device needs to be on in order to reconfigure the integration.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
|
@ -22,7 +22,7 @@
|
||||
"error": {
|
||||
"unauthorized_error": "The credentials are incorrect.",
|
||||
"network_error": "The Mastodon instance was not found.",
|
||||
"unknown": "Unknown error occured when connecting to the Mastodon instance."
|
||||
"unknown": "Unknown error occurred when connecting to the Mastodon instance."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
@ -7,7 +7,7 @@
|
||||
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]"
|
||||
},
|
||||
"data_description": {
|
||||
"llm_hass_api": "The method for controling Home Assistant to expose with the Model Context Protocol."
|
||||
"llm_hass_api": "The method for controlling Home Assistant to expose with the Model Context Protocol."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -21,7 +21,7 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"connection_error_during_import": "Connection error occured during yaml configuration import",
|
||||
"connection_error_during_import": "Connection error occurred during yaml configuration import",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
@ -70,7 +70,7 @@
|
||||
"name": "Cache memory"
|
||||
},
|
||||
"nextcloud_cache_num_entries": {
|
||||
"name": "Cache number of entires"
|
||||
"name": "Cache number of entries"
|
||||
},
|
||||
"nextcloud_cache_num_hits": {
|
||||
"name": "Cache number of hits"
|
||||
|
@ -61,7 +61,7 @@
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::proximity::issues::tracked_entity_removed::title%]",
|
||||
"description": "The entity `{entity_id}` has been removed from HA, but is used in proximity {name}. Please remove `{entity_id}` from the list of tracked entities. Related proximity sensor entites were set to unavailable and can be removed."
|
||||
"description": "The entity `{entity_id}` has been removed from HA, but is used in proximity {name}. Please remove `{entity_id}` from the list of tracked entities. Related proximity sensor entities were set to unavailable and can be removed."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,10 +33,10 @@
|
||||
"name": "Upload speed limit"
|
||||
},
|
||||
"alltime_download": {
|
||||
"name": "Alltime download"
|
||||
"name": "All-time download"
|
||||
},
|
||||
"alltime_upload": {
|
||||
"name": "Alltime upload"
|
||||
"name": "All-time upload"
|
||||
},
|
||||
"global_ratio": {
|
||||
"name": "Global ratio"
|
||||
@ -115,7 +115,7 @@
|
||||
"message": "No entry with ID {device_id} was found"
|
||||
},
|
||||
"login_error": {
|
||||
"message": "A login error occured. Please check your username and password."
|
||||
"message": "A login error occurred. Please check your username and password."
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "Can't connect to qBittorrent, please check your configuration."
|
||||
|
@ -10,7 +10,7 @@
|
||||
"abort": {
|
||||
"already_configured": "Controller already configured",
|
||||
"discovery_in_progress": "Discovery in progress",
|
||||
"not_supported": "Configuration for QBUS happens through MQTT discovery. If your controller is not automatically discovered, check the prerequisites and troubleshooting section of the documention."
|
||||
"not_supported": "Configuration for QBUS happens through MQTT discovery. If your controller is not automatically discovered, check the prerequisites and troubleshooting section of the documentation."
|
||||
},
|
||||
"error": {
|
||||
"no_controller": "No controllers were found"
|
||||
|
@ -126,7 +126,7 @@
|
||||
},
|
||||
"hub_switch_deprecated": {
|
||||
"title": "Reolink Home Hub switches deprecated",
|
||||
"description": "The redundant 'Record', 'Email on event', 'FTP upload', 'Push notifications', and 'Buzzer on event' switches on the Reolink Home Hub are depricated since the new firmware no longer supports these. Please use the equally named switches under each of the camera devices connected to the Home Hub instead. To remove this issue, please adjust automations accordingly and disable the switch entities mentioned."
|
||||
"description": "The redundant 'Record', 'Email on event', 'FTP upload', 'Push notifications', and 'Buzzer on event' switches on the Reolink Home Hub are deprecated since the new firmware no longer supports these. Please use the equally named switches under each of the camera devices connected to the Home Hub instead. To remove this issue, please adjust automations accordingly and disable the switch entities mentioned."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
@ -164,7 +164,9 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(self.info[CONF_MAC])
|
||||
await self.async_set_unique_id(
|
||||
self.info[CONF_MAC], raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: host})
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
@ -13,6 +13,9 @@
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
"name": "shelly*"
|
||||
},
|
||||
{
|
||||
"type": "_shelly._tcp.local."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -36,3 +36,5 @@ CONF_BROWSE_LIMIT = "browse_limit"
|
||||
CONF_VOLUME_STEP = "volume_step"
|
||||
DEFAULT_BROWSE_LIMIT = 1000
|
||||
DEFAULT_VOLUME_STEP = 5
|
||||
ATTR_ANNOUNCE_VOLUME = "announce_volume"
|
||||
ATTR_ANNOUNCE_TIMEOUT = "announce_timeout"
|
||||
|
@ -14,6 +14,7 @@ import voluptuous as vol
|
||||
from homeassistant.components import media_source
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
ATTR_MEDIA_EXTRA,
|
||||
BrowseError,
|
||||
BrowseMedia,
|
||||
MediaPlayerEnqueue,
|
||||
@ -52,6 +53,8 @@ from .browse_media import (
|
||||
media_source_content_filter,
|
||||
)
|
||||
from .const import (
|
||||
ATTR_ANNOUNCE_TIMEOUT,
|
||||
ATTR_ANNOUNCE_VOLUME,
|
||||
CONF_BROWSE_LIMIT,
|
||||
CONF_VOLUME_STEP,
|
||||
DEFAULT_BROWSE_LIMIT,
|
||||
@ -157,6 +160,26 @@ async def async_setup_entry(
|
||||
entry.async_on_unload(async_at_start(hass, start_server_discovery))
|
||||
|
||||
|
||||
def get_announce_volume(extra: dict) -> float | None:
|
||||
"""Get announce volume from extra service data."""
|
||||
if ATTR_ANNOUNCE_VOLUME not in extra:
|
||||
return None
|
||||
announce_volume = float(extra[ATTR_ANNOUNCE_VOLUME])
|
||||
if not (0 < announce_volume <= 1):
|
||||
raise ValueError
|
||||
return announce_volume * 100
|
||||
|
||||
|
||||
def get_announce_timeout(extra: dict) -> int | None:
|
||||
"""Get announce volume from extra service data."""
|
||||
if ATTR_ANNOUNCE_TIMEOUT not in extra:
|
||||
return None
|
||||
announce_timeout = int(extra[ATTR_ANNOUNCE_TIMEOUT])
|
||||
if announce_timeout < 1:
|
||||
raise ValueError
|
||||
return announce_timeout
|
||||
|
||||
|
||||
class SqueezeBoxMediaPlayerEntity(
|
||||
CoordinatorEntity[SqueezeBoxPlayerUpdateCoordinator], MediaPlayerEntity
|
||||
):
|
||||
@ -184,6 +207,7 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
| MediaPlayerEntityFeature.STOP
|
||||
| MediaPlayerEntityFeature.GROUPING
|
||||
| MediaPlayerEntityFeature.MEDIA_ENQUEUE
|
||||
| MediaPlayerEntityFeature.MEDIA_ANNOUNCE
|
||||
)
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
@ -437,7 +461,11 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
self,
|
||||
media_type: MediaType | str,
|
||||
media_id: str,
|
||||
announce: bool | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Send the play_media command to the media player."""
|
||||
index = None
|
||||
@ -460,6 +488,32 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
)
|
||||
media_id = play_item.url
|
||||
|
||||
if announce:
|
||||
if media_type not in MediaType.MUSIC:
|
||||
raise ServiceValidationError(
|
||||
"Announcements must have media type of 'music'. Playlists are not supported"
|
||||
)
|
||||
|
||||
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
|
||||
cmd = "announce"
|
||||
try:
|
||||
announce_volume = get_announce_volume(extra)
|
||||
except ValueError:
|
||||
raise ServiceValidationError(
|
||||
f"{ATTR_ANNOUNCE_VOLUME} must be a number greater than 0 and less than or equal to 1"
|
||||
) from None
|
||||
else:
|
||||
self._player.set_announce_volume(announce_volume)
|
||||
|
||||
try:
|
||||
announce_timeout = get_announce_timeout(extra)
|
||||
except ValueError:
|
||||
raise ServiceValidationError(
|
||||
f"{ATTR_ANNOUNCE_TIMEOUT} must be a whole number greater than 0"
|
||||
) from None
|
||||
else:
|
||||
self._player.set_announce_timeout(announce_timeout)
|
||||
|
||||
if media_type in MediaType.MUSIC:
|
||||
if not media_id.startswith(SQUEEZEBOX_SOURCE_STRINGS):
|
||||
# do not process special squeezebox "source" media ids
|
||||
|
@ -21,7 +21,7 @@
|
||||
}
|
||||
},
|
||||
"state_characteristic": {
|
||||
"description": "Read the documention for further details on available options and how to use them.",
|
||||
"description": "Read the documentation for further details on available options and how to use them.",
|
||||
"data": {
|
||||
"state_characteristic": "Statistic characteristic"
|
||||
},
|
||||
@ -30,7 +30,7 @@
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"description": "Read the documention for further details on how to configure the statistics sensor using these options.",
|
||||
"description": "Read the documentation for further details on how to configure the statistics sensor using these options.",
|
||||
"data": {
|
||||
"sampling_size": "Sampling size",
|
||||
"max_age": "Max age",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Select the location you want to recieve the Stookwijzer information for.",
|
||||
"description": "Select the location you want to receive the Stookwijzer information for.",
|
||||
"data": {
|
||||
"location": "[%key:common::config_flow::data::location%]"
|
||||
},
|
||||
|
@ -506,7 +506,7 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"unknown": {
|
||||
"message": "An unknown issue occured changing {name}."
|
||||
"message": "An unknown issue occurred changing {name}."
|
||||
},
|
||||
"not_supported": {
|
||||
"message": "{name} is not supported."
|
||||
|
@ -57,7 +57,7 @@
|
||||
"name": "[%key:component::sensor::entity_component::pm25::name%]"
|
||||
},
|
||||
"neph": {
|
||||
"name": "Visbility using nephelometry"
|
||||
"name": "Visibility using nephelometry"
|
||||
},
|
||||
"dominant_pollutant": {
|
||||
"name": "Dominant pollutant",
|
||||
|
@ -37,7 +37,7 @@
|
||||
"name": "Indoor unit water pump"
|
||||
},
|
||||
"indoor_unit_auxiliary_pump_state": {
|
||||
"name": "Indoor unit auxilary water pump"
|
||||
"name": "Indoor unit auxiliary water pump"
|
||||
},
|
||||
"indoor_unit_dhw_valve_or_pump_state": {
|
||||
"name": "Indoor unit DHW valve or water pump"
|
||||
|
@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from yolink.const import ATTR_DEVICE_SMART_REMOTER
|
||||
from yolink.const import ATTR_DEVICE_SMART_REMOTER, ATTR_DEVICE_SWITCH
|
||||
from yolink.device import YoLinkDevice
|
||||
from yolink.exception import YoLinkAuthFailError, YoLinkClientError
|
||||
from yolink.home_manager import YoLinkHome
|
||||
@ -75,7 +75,8 @@ class YoLinkHomeMessageListener(MessageListener):
|
||||
device_coordinator.async_set_updated_data(msg_data)
|
||||
# handling events
|
||||
if (
|
||||
device_coordinator.device.device_type == ATTR_DEVICE_SMART_REMOTER
|
||||
device_coordinator.device.device_type
|
||||
in [ATTR_DEVICE_SMART_REMOTER, ATTR_DEVICE_SWITCH]
|
||||
and msg_data.get("event") is not None
|
||||
):
|
||||
device_registry = dr.async_get(self._hass)
|
||||
|
@ -33,3 +33,7 @@ DEV_MODEL_PLUG_YS6602_UC = "YS6602-UC"
|
||||
DEV_MODEL_PLUG_YS6602_EC = "YS6602-EC"
|
||||
DEV_MODEL_PLUG_YS6803_UC = "YS6803-UC"
|
||||
DEV_MODEL_PLUG_YS6803_EC = "YS6803-EC"
|
||||
DEV_MODEL_SWITCH_YS5708_UC = "YS5708-UC"
|
||||
DEV_MODEL_SWITCH_YS5708_EC = "YS5708-EC"
|
||||
DEV_MODEL_SWITCH_YS5709_UC = "YS5709-UC"
|
||||
DEV_MODEL_SWITCH_YS5709_EC = "YS5709-EC"
|
||||
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from yolink.const import ATTR_DEVICE_SMART_REMOTER
|
||||
from yolink.const import ATTR_DEVICE_SMART_REMOTER, ATTR_DEVICE_SWITCH
|
||||
|
||||
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
||||
from homeassistant.components.homeassistant.triggers import event as event_trigger
|
||||
@ -21,6 +21,10 @@ from .const import (
|
||||
DEV_MODEL_FLEX_FOB_YS3604_UC,
|
||||
DEV_MODEL_FLEX_FOB_YS3614_EC,
|
||||
DEV_MODEL_FLEX_FOB_YS3614_UC,
|
||||
DEV_MODEL_SWITCH_YS5708_EC,
|
||||
DEV_MODEL_SWITCH_YS5708_UC,
|
||||
DEV_MODEL_SWITCH_YS5709_EC,
|
||||
DEV_MODEL_SWITCH_YS5709_UC,
|
||||
)
|
||||
|
||||
CONF_BUTTON_1 = "button_1"
|
||||
@ -30,7 +34,7 @@ CONF_BUTTON_4 = "button_4"
|
||||
CONF_SHORT_PRESS = "short_press"
|
||||
CONF_LONG_PRESS = "long_press"
|
||||
|
||||
FLEX_FOB_4_BUTTONS = {
|
||||
FLEX_BUTTONS_4 = {
|
||||
f"{CONF_BUTTON_1}_{CONF_SHORT_PRESS}",
|
||||
f"{CONF_BUTTON_1}_{CONF_LONG_PRESS}",
|
||||
f"{CONF_BUTTON_2}_{CONF_SHORT_PRESS}",
|
||||
@ -41,7 +45,7 @@ FLEX_FOB_4_BUTTONS = {
|
||||
f"{CONF_BUTTON_4}_{CONF_LONG_PRESS}",
|
||||
}
|
||||
|
||||
FLEX_FOB_2_BUTTONS = {
|
||||
FLEX_BUTTONS_2 = {
|
||||
f"{CONF_BUTTON_1}_{CONF_SHORT_PRESS}",
|
||||
f"{CONF_BUTTON_1}_{CONF_LONG_PRESS}",
|
||||
f"{CONF_BUTTON_2}_{CONF_SHORT_PRESS}",
|
||||
@ -49,16 +53,19 @@ FLEX_FOB_2_BUTTONS = {
|
||||
}
|
||||
|
||||
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
{vol.Required(CONF_TYPE): vol.In(FLEX_FOB_4_BUTTONS)}
|
||||
{vol.Required(CONF_TYPE): vol.In(FLEX_BUTTONS_4)}
|
||||
)
|
||||
|
||||
|
||||
# YoLink Remotes YS3604/YS3614
|
||||
FLEX_FOB_TRIGGER_TYPES: dict[str, set[str]] = {
|
||||
DEV_MODEL_FLEX_FOB_YS3604_EC: FLEX_FOB_4_BUTTONS,
|
||||
DEV_MODEL_FLEX_FOB_YS3604_UC: FLEX_FOB_4_BUTTONS,
|
||||
DEV_MODEL_FLEX_FOB_YS3614_UC: FLEX_FOB_2_BUTTONS,
|
||||
DEV_MODEL_FLEX_FOB_YS3614_EC: FLEX_FOB_2_BUTTONS,
|
||||
# YoLink Remotes YS3604/YS3614, Switch YS5708/YS5709
|
||||
TRIGGER_MAPPINGS: dict[str, set[str]] = {
|
||||
DEV_MODEL_FLEX_FOB_YS3604_EC: FLEX_BUTTONS_4,
|
||||
DEV_MODEL_FLEX_FOB_YS3604_UC: FLEX_BUTTONS_4,
|
||||
DEV_MODEL_FLEX_FOB_YS3614_UC: FLEX_BUTTONS_2,
|
||||
DEV_MODEL_FLEX_FOB_YS3614_EC: FLEX_BUTTONS_2,
|
||||
DEV_MODEL_SWITCH_YS5708_EC: FLEX_BUTTONS_2,
|
||||
DEV_MODEL_SWITCH_YS5708_UC: FLEX_BUTTONS_2,
|
||||
DEV_MODEL_SWITCH_YS5709_EC: FLEX_BUTTONS_2,
|
||||
DEV_MODEL_SWITCH_YS5709_UC: FLEX_BUTTONS_2,
|
||||
}
|
||||
|
||||
|
||||
@ -68,9 +75,12 @@ async def async_get_triggers(
|
||||
"""List device triggers for YoLink devices."""
|
||||
device_registry = dr.async_get(hass)
|
||||
registry_device = device_registry.async_get(device_id)
|
||||
if not registry_device or registry_device.model != ATTR_DEVICE_SMART_REMOTER:
|
||||
if not registry_device or registry_device.model not in [
|
||||
ATTR_DEVICE_SMART_REMOTER,
|
||||
ATTR_DEVICE_SWITCH,
|
||||
]:
|
||||
return []
|
||||
if registry_device.model_id not in list(FLEX_FOB_TRIGGER_TYPES.keys()):
|
||||
if registry_device.model_id not in list(TRIGGER_MAPPINGS.keys()):
|
||||
return []
|
||||
return [
|
||||
{
|
||||
@ -79,7 +89,7 @@ async def async_get_triggers(
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_TYPE: trigger,
|
||||
}
|
||||
for trigger in FLEX_FOB_TRIGGER_TYPES[registry_device.model_id]
|
||||
for trigger in TRIGGER_MAPPINGS[registry_device.model_id]
|
||||
]
|
||||
|
||||
|
||||
|
@ -162,11 +162,12 @@ class YoLinkSwitchEntity(YoLinkEntity, SwitchEntity):
|
||||
@callback
|
||||
def update_entity_state(self, state: dict[str, str | list[str]]) -> None:
|
||||
"""Update HA Entity State."""
|
||||
self._attr_is_on = self._get_state(
|
||||
state.get("state"),
|
||||
self.entity_description.plug_index_fn(self.coordinator.device),
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
if (state_value := state.get("state")) is not None:
|
||||
self._attr_is_on = self._get_state(
|
||||
state_value,
|
||||
self.entity_description.plug_index_fn(self.coordinator.device),
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def call_state_change(self, state: str) -> None:
|
||||
"""Call setState api to change switch state."""
|
||||
|
@ -28,8 +28,8 @@ MINOR_VERSION: Final = 3
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0)
|
||||
REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 0)
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
||||
REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
||||
# Truthy date string triggers showing related deprecation warning messages.
|
||||
REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = ""
|
||||
|
||||
|
5
homeassistant/generated/zeroconf.py
generated
5
homeassistant/generated/zeroconf.py
generated
@ -803,6 +803,11 @@ ZEROCONF = {
|
||||
"domain": "russound_rio",
|
||||
},
|
||||
],
|
||||
"_shelly._tcp.local.": [
|
||||
{
|
||||
"domain": "shelly",
|
||||
},
|
||||
],
|
||||
"_sideplay._tcp.local.": [
|
||||
{
|
||||
"domain": "ecobee",
|
||||
|
@ -67,7 +67,7 @@ standard-telnetlib==3.13.0
|
||||
typing-extensions>=4.12.2,<5.0
|
||||
ulid-transform==1.2.0
|
||||
urllib3>=1.26.5,<2
|
||||
uv==0.6.0
|
||||
uv==0.6.1
|
||||
voluptuous-openapi==0.0.6
|
||||
voluptuous-serialize==2.6.0
|
||||
voluptuous==0.15.2
|
||||
|
10
mypy.ini
generated
10
mypy.ini
generated
@ -2096,6 +2096,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.home_connect.*]
|
||||
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.homeassistant.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
@ -21,7 +21,7 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Topic :: Home Automation",
|
||||
]
|
||||
requires-python = ">=3.13.0"
|
||||
requires-python = ">=3.13.2"
|
||||
dependencies = [
|
||||
"aiodns==3.2.0",
|
||||
# Integrations may depend on hassio integration without listing it to
|
||||
@ -76,7 +76,7 @@ dependencies = [
|
||||
# Temporary setting an upper bound, to prevent compat issues with urllib3>=2
|
||||
# https://github.com/home-assistant/core/issues/97248
|
||||
"urllib3>=1.26.5,<2",
|
||||
"uv==0.6.0",
|
||||
"uv==0.6.1",
|
||||
"voluptuous==0.15.2",
|
||||
"voluptuous-serialize==2.6.0",
|
||||
"voluptuous-openapi==0.0.6",
|
||||
|
2
requirements.txt
generated
2
requirements.txt
generated
@ -45,7 +45,7 @@ standard-telnetlib==3.13.0
|
||||
typing-extensions>=4.12.2,<5.0
|
||||
ulid-transform==1.2.0
|
||||
urllib3>=1.26.5,<2
|
||||
uv==0.6.0
|
||||
uv==0.6.1
|
||||
voluptuous==0.15.2
|
||||
voluptuous-serialize==2.6.0
|
||||
voluptuous-openapi==0.0.6
|
||||
|
@ -26,6 +26,24 @@ ENV \
|
||||
|
||||
ARG QEMU_CPU
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||
ARG BUILD_ARCH
|
||||
# Get go2rtc binary
|
||||
RUN \
|
||||
case "${{BUILD_ARCH}}" in \
|
||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||
"armhf") go2rtc_suffix='armv6' ;; \
|
||||
"armv7") go2rtc_suffix='arm' ;; \
|
||||
*) go2rtc_suffix=${{BUILD_ARCH}} ;; \
|
||||
esac \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v{go2rtc}/go2rtc_linux_${{go2rtc_suffix}} --output /bin/go2rtc \
|
||||
&& chmod +x /bin/go2rtc \
|
||||
# Verify go2rtc can be executed
|
||||
&& go2rtc --version
|
||||
|
||||
# Install uv
|
||||
RUN pip3 install uv=={uv}
|
||||
|
||||
@ -56,24 +74,6 @@ RUN \
|
||||
&& python3 -m compileall \
|
||||
homeassistant/homeassistant
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||
ARG BUILD_ARCH
|
||||
# Get go2rtc binary
|
||||
RUN \
|
||||
case "${{BUILD_ARCH}}" in \
|
||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||
"armhf") go2rtc_suffix='armv6' ;; \
|
||||
"armv7") go2rtc_suffix='arm' ;; \
|
||||
*) go2rtc_suffix=${{BUILD_ARCH}} ;; \
|
||||
esac \
|
||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v{go2rtc}/go2rtc_linux_${{go2rtc_suffix}} --output /bin/go2rtc \
|
||||
&& chmod +x /bin/go2rtc \
|
||||
# Verify go2rtc can be executed
|
||||
&& go2rtc --version
|
||||
|
||||
WORKDIR /config
|
||||
"""
|
||||
|
||||
|
2
script/hassfest/docker/Dockerfile
generated
2
script/hassfest/docker/Dockerfile
generated
@ -14,7 +14,7 @@ WORKDIR "/github/workspace"
|
||||
COPY . /usr/src/homeassistant
|
||||
|
||||
# Uv is only needed during build
|
||||
RUN --mount=from=ghcr.io/astral-sh/uv:0.6.0,source=/uv,target=/bin/uv \
|
||||
RUN --mount=from=ghcr.io/astral-sh/uv:0.6.1,source=/uv,target=/bin/uv \
|
||||
# Uv creates a lock file in /tmp
|
||||
--mount=type=tmpfs,target=/tmp \
|
||||
# Required for PyTurboJPEG
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Test the air-Q config flow."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
|
||||
from aioairq import DeviceInfo, InvalidAuth
|
||||
@ -37,8 +38,9 @@ DEFAULT_OPTIONS = {
|
||||
}
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
async def test_form(hass: HomeAssistant, caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Test we get the form."""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
@ -54,6 +56,7 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
TEST_USER_DATA,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert f"Creating an entry for {TEST_DEVICE_INFO['name']}" in caplog.text
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == TEST_DEVICE_INFO["name"]
|
||||
|
129
tests/components/airq/test_coordinator.py
Normal file
129
tests/components/airq/test_coordinator.py
Normal file
@ -0,0 +1,129 @@
|
||||
"""Test the air-Q coordinator."""
|
||||
|
||||
import logging
|
||||
from unittest.mock import patch
|
||||
|
||||
from aioairq import DeviceInfo as AirQDeviceInfo
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.airq import AirQCoordinator
|
||||
from homeassistant.components.airq.const import DOMAIN
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("mock_setup_entry")
|
||||
MOCKED_ENTRY = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_IP_ADDRESS: "192.168.0.0",
|
||||
CONF_PASSWORD: "password",
|
||||
},
|
||||
unique_id="123-456",
|
||||
)
|
||||
|
||||
TEST_DEVICE_INFO = AirQDeviceInfo(
|
||||
id="id",
|
||||
name="name",
|
||||
model="model",
|
||||
sw_version="sw",
|
||||
hw_version="hw",
|
||||
)
|
||||
TEST_DEVICE_DATA = {"co2": 500.0, "Status": "OK"}
|
||||
STATUS_WARMUP = {
|
||||
"co": "co sensor still in warm up phase; waiting time = 18 s",
|
||||
"tvoc": "tvoc sensor still in warm up phase; waiting time = 18 s",
|
||||
"so2": "so2 sensor still in warm up phase; waiting time = 17 s",
|
||||
}
|
||||
|
||||
|
||||
async def test_logging_in_coordinator_first_update_data(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test that the first AirQCoordinator._async_update_data call logs necessary setup.
|
||||
|
||||
The fields of AirQCoordinator.device_info that are specific to the device are only
|
||||
populated upon the first call to AirQCoordinator._async_update_data. The one field
|
||||
which is actually necessary is 'name', and its absence is checked and logged,
|
||||
as well as its being set.
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
coordinator = AirQCoordinator(hass, MOCKED_ENTRY)
|
||||
|
||||
# check that the name _is_ missing
|
||||
assert "name" not in coordinator.device_info
|
||||
|
||||
# First call: fetch missing device info
|
||||
with (
|
||||
patch("aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO),
|
||||
patch("aioairq.AirQ.get_latest_data", return_value=TEST_DEVICE_DATA),
|
||||
):
|
||||
await coordinator._async_update_data()
|
||||
|
||||
# check that the missing name is logged...
|
||||
assert (
|
||||
"'name' not found in AirQCoordinator.device_info, fetching from the device"
|
||||
in caplog.text
|
||||
)
|
||||
# ...and fixed
|
||||
assert coordinator.device_info.get("name") == TEST_DEVICE_INFO["name"]
|
||||
assert (
|
||||
f"Updated AirQCoordinator.device_info for 'name' {TEST_DEVICE_INFO['name']}"
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
# Also that no warming up sensors is found as none are mocked
|
||||
assert "Following sensors are still warming up" not in caplog.text
|
||||
|
||||
|
||||
async def test_logging_in_coordinator_subsequent_update_data(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test that the second AirQCoordinator._async_update_data call has nothing to log.
|
||||
|
||||
The second call is emulated by setting up AirQCoordinator.device_info correctly,
|
||||
instead of actually calling the _async_update_data, which would populate the log
|
||||
with the messages we want to see not being repeated.
|
||||
"""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
coordinator = AirQCoordinator(hass, MOCKED_ENTRY)
|
||||
coordinator.device_info.update(DeviceInfo(**TEST_DEVICE_INFO))
|
||||
|
||||
with (
|
||||
patch("aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO),
|
||||
patch("aioairq.AirQ.get_latest_data", return_value=TEST_DEVICE_DATA),
|
||||
):
|
||||
await coordinator._async_update_data()
|
||||
# check that the name _is not_ missing
|
||||
assert "name" in coordinator.device_info
|
||||
# and that nothing of the kind is logged
|
||||
assert (
|
||||
"'name' not found in AirQCoordinator.device_info, fetching from the device"
|
||||
not in caplog.text
|
||||
)
|
||||
assert (
|
||||
f"Updated AirQCoordinator.device_info for 'name' {TEST_DEVICE_INFO['name']}"
|
||||
not in caplog.text
|
||||
)
|
||||
|
||||
|
||||
async def test_logging_when_warming_up_sensor_present(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test that warming up sensors are logged."""
|
||||
caplog.set_level(logging.DEBUG)
|
||||
coordinator = AirQCoordinator(hass, MOCKED_ENTRY)
|
||||
with (
|
||||
patch("aioairq.AirQ.fetch_device_info", return_value=TEST_DEVICE_INFO),
|
||||
patch(
|
||||
"aioairq.AirQ.get_latest_data",
|
||||
return_value=TEST_DEVICE_DATA | {"Status": STATUS_WARMUP},
|
||||
),
|
||||
):
|
||||
await coordinator._async_update_data()
|
||||
assert (
|
||||
f"Following sensors are still warming up: {set(STATUS_WARMUP.keys())}"
|
||||
in caplog.text
|
||||
)
|
@ -3,7 +3,7 @@
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.aranet.const import DOMAIN
|
||||
from homeassistant.components.sensor import ATTR_STATE_CLASS
|
||||
from homeassistant.components.sensor import ATTR_OPTIONS, ATTR_STATE_CLASS
|
||||
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
@ -170,7 +170,7 @@ async def test_sensors_aranet4(
|
||||
assert len(hass.states.async_all("sensor")) == 0
|
||||
inject_bluetooth_service_info(hass, VALID_DATA_SERVICE_INFO)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_all("sensor")) == 6
|
||||
assert len(hass.states.async_all("sensor")) == 7
|
||||
|
||||
batt_sensor = hass.states.get("sensor.aranet4_12345_battery")
|
||||
batt_sensor_attrs = batt_sensor.attributes
|
||||
@ -214,6 +214,12 @@ async def test_sensors_aranet4(
|
||||
assert interval_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "s"
|
||||
assert interval_sensor_attrs[ATTR_STATE_CLASS] == "measurement"
|
||||
|
||||
status_sensor = hass.states.get("sensor.aranet4_12345_threshold")
|
||||
status_sensor_attrs = status_sensor.attributes
|
||||
assert status_sensor.state == "green"
|
||||
assert status_sensor_attrs[ATTR_FRIENDLY_NAME] == "Aranet4 12345 Threshold"
|
||||
assert status_sensor_attrs[ATTR_OPTIONS] == ["error", "green", "yellow", "red"]
|
||||
|
||||
# Check device context for the battery sensor
|
||||
entity = entity_registry.async_get("sensor.aranet4_12345_battery")
|
||||
device = device_registry.async_get(entity.device_id)
|
||||
@ -245,7 +251,7 @@ async def test_sensors_aranetrn(
|
||||
assert len(hass.states.async_all("sensor")) == 0
|
||||
inject_bluetooth_service_info(hass, VALID_ARANET_RADON_DATA_SERVICE_INFO)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.states.async_all("sensor")) == 6
|
||||
assert len(hass.states.async_all("sensor")) == 7
|
||||
|
||||
batt_sensor = hass.states.get("sensor.aranetrn_12345_battery")
|
||||
batt_sensor_attrs = batt_sensor.attributes
|
||||
@ -291,6 +297,12 @@ async def test_sensors_aranetrn(
|
||||
assert interval_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "s"
|
||||
assert interval_sensor_attrs[ATTR_STATE_CLASS] == "measurement"
|
||||
|
||||
status_sensor = hass.states.get("sensor.aranetrn_12345_threshold")
|
||||
status_sensor_attrs = status_sensor.attributes
|
||||
assert status_sensor.state == "green"
|
||||
assert status_sensor_attrs[ATTR_FRIENDLY_NAME] == "AranetRn+ 12345 Threshold"
|
||||
assert status_sensor_attrs[ATTR_OPTIONS] == ["error", "green", "yellow", "red"]
|
||||
|
||||
# Check device context for the battery sensor
|
||||
entity = entity_registry.async_get("sensor.aranetrn_12345_battery")
|
||||
device = device_registry.async_get(entity.device_id)
|
||||
|
@ -67,7 +67,7 @@
|
||||
"hasHeatPump": false,
|
||||
"humidity": "30"
|
||||
},
|
||||
"equipmentStatus": "fan",
|
||||
"equipmentStatus": "fan,humidifier",
|
||||
"events": [
|
||||
{
|
||||
"name": "Event1",
|
||||
|
@ -6,6 +6,7 @@ import pytest
|
||||
|
||||
from homeassistant.components.ecobee.humidifier import MODE_MANUAL, MODE_OFF
|
||||
from homeassistant.components.humidifier import (
|
||||
ATTR_ACTION,
|
||||
ATTR_AVAILABLE_MODES,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
ATTR_HUMIDITY,
|
||||
@ -17,6 +18,7 @@ from homeassistant.components.humidifier import (
|
||||
MODE_AUTO,
|
||||
SERVICE_SET_HUMIDITY,
|
||||
SERVICE_SET_MODE,
|
||||
HumidifierAction,
|
||||
HumidifierDeviceClass,
|
||||
HumidifierEntityFeature,
|
||||
)
|
||||
@ -44,6 +46,7 @@ async def test_attributes(hass: HomeAssistant) -> None:
|
||||
|
||||
state = hass.states.get(DEVICE_ID)
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes[ATTR_ACTION] == HumidifierAction.HUMIDIFYING
|
||||
assert state.attributes[ATTR_CURRENT_HUMIDITY] == 15
|
||||
assert state.attributes[ATTR_MIN_HUMIDITY] == DEFAULT_MIN_HUMIDITY
|
||||
assert state.attributes[ATTR_MAX_HUMIDITY] == DEFAULT_MAX_HUMIDITY
|
||||
|
@ -34,6 +34,7 @@ class MockHeos(Heos):
|
||||
self.player_set_play_state: AsyncMock = AsyncMock()
|
||||
self.player_set_volume: AsyncMock = AsyncMock()
|
||||
self.set_group: AsyncMock = AsyncMock()
|
||||
self.set_group_volume: AsyncMock = AsyncMock()
|
||||
self.sign_in: AsyncMock = AsyncMock()
|
||||
self.sign_out: AsyncMock = AsyncMock()
|
||||
|
||||
|
@ -22,7 +22,7 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from syrupy.filters import props
|
||||
|
||||
from homeassistant.components.heos.const import DOMAIN
|
||||
from homeassistant.components.heos.const import DOMAIN, SERVICE_GROUP_VOLUME_SET
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_GROUP_MEMBERS,
|
||||
ATTR_INPUT_SOURCE,
|
||||
@ -724,6 +724,62 @@ async def test_volume_set_error(
|
||||
controller.player_set_volume.assert_called_once_with(1, 100)
|
||||
|
||||
|
||||
async def test_group_volume_set(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos
|
||||
) -> None:
|
||||
"""Test the group volume set service."""
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
{ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_VOLUME_LEVEL: 1},
|
||||
blocking=True,
|
||||
)
|
||||
controller.set_group_volume.assert_called_once_with(999, 100)
|
||||
|
||||
|
||||
async def test_group_volume_set_error(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos
|
||||
) -> None:
|
||||
"""Test the group volume set service errors."""
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
controller.set_group_volume.side_effect = CommandFailedError("", "Failure", 1)
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match=re.escape("Unable to set group volume level: Failure (1)"),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
{ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_VOLUME_LEVEL: 1},
|
||||
blocking=True,
|
||||
)
|
||||
controller.set_group_volume.assert_called_once_with(999, 100)
|
||||
|
||||
|
||||
async def test_group_volume_set_not_grouped_error(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry, controller: MockHeos
|
||||
) -> None:
|
||||
"""Test the group volume set service when not grouped raises error."""
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
player = controller.players[1]
|
||||
player.group_id = None
|
||||
with pytest.raises(
|
||||
ServiceValidationError,
|
||||
match=re.escape("Entity media_player.test_player is not joined to a group"),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_GROUP_VOLUME_SET,
|
||||
{ATTR_ENTITY_ID: "media_player.test_player", ATTR_MEDIA_VOLUME_LEVEL: 1},
|
||||
blocking=True,
|
||||
)
|
||||
controller.set_group_volume.assert_not_called()
|
||||
|
||||
|
||||
async def test_select_favorite(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
|
@ -346,6 +346,49 @@ async def test_binary_sensors_functionality(
|
||||
assert hass.states.is_state(entity_id, expected)
|
||||
|
||||
|
||||
async def test_connected_sensor_functionality(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
integration_setup: Callable[[MagicMock], Awaitable[bool]],
|
||||
setup_credentials: None,
|
||||
client: MagicMock,
|
||||
appliance_ha_id: str,
|
||||
) -> None:
|
||||
"""Test if the connected binary sensor reports the right values."""
|
||||
entity_id = "binary_sensor.washer_connectivity"
|
||||
assert config_entry.state == ConfigEntryState.NOT_LOADED
|
||||
assert await integration_setup(client)
|
||||
assert config_entry.state == ConfigEntryState.LOADED
|
||||
|
||||
assert hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
EventType.DISCONNECTED,
|
||||
ArrayOfEvents([]),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.is_state(entity_id, STATE_OFF)
|
||||
|
||||
await client.add_events(
|
||||
[
|
||||
EventMessage(
|
||||
appliance_ha_id,
|
||||
EventType.CONNECTED,
|
||||
ArrayOfEvents([]),
|
||||
)
|
||||
]
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.is_state(entity_id, STATE_ON)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_create_issue(
|
||||
hass: HomeAssistant,
|
||||
|
@ -1101,7 +1101,7 @@
|
||||
'state': '0.175296',
|
||||
})
|
||||
# ---
|
||||
# name: test_async_setup_entry[sensor.my_nc_url_local_cache_number_of_entires-entry]
|
||||
# name: test_async_setup_entry[sensor.my_nc_url_local_cache_number_of_entries-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@ -1116,7 +1116,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.my_nc_url_local_cache_number_of_entires',
|
||||
'entity_id': 'sensor.my_nc_url_local_cache_number_of_entries',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@ -1128,7 +1128,7 @@
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Cache number of entires',
|
||||
'original_name': 'Cache number of entries',
|
||||
'platform': 'nextcloud',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
@ -1137,14 +1137,14 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_async_setup_entry[sensor.my_nc_url_local_cache_number_of_entires-state]
|
||||
# name: test_async_setup_entry[sensor.my_nc_url_local_cache_number_of_entries-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'my.nc_url.local Cache number of entires',
|
||||
'friendly_name': 'my.nc_url.local Cache number of entries',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.my_nc_url_local_cache_number_of_entires',
|
||||
'entity_id': 'sensor.my_nc_url_local_cache_number_of_entries',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
@ -117,6 +117,73 @@ async def test_form(
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_user_flow_overrides_existing_discovery(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
) -> None:
|
||||
"""Test setting up from the user flow when the devices is already discovered."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.shelly.config_flow.get_info",
|
||||
return_value={
|
||||
"mac": "AABBCCDDEEFF",
|
||||
"model": MODEL_PLUS_2PM,
|
||||
"auth": False,
|
||||
"gen": 2,
|
||||
"port": 80,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.shelly.async_setup", return_value=True
|
||||
) as mock_setup,
|
||||
patch(
|
||||
"homeassistant.components.shelly.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
discovery_result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
data=ZeroconfServiceInfo(
|
||||
ip_address=ip_address("1.1.1.1"),
|
||||
ip_addresses=[ip_address("1.1.1.1")],
|
||||
hostname="mock_hostname",
|
||||
name="shelly2pm-aabbccddeeff",
|
||||
port=None,
|
||||
properties={ATTR_PROPERTIES_ID: "shelly2pm-aabbccddeeff"},
|
||||
type="mock_type",
|
||||
),
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
)
|
||||
assert discovery_result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"host": "1.1.1.1", "port": 80},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Test name"
|
||||
assert result2["data"] == {
|
||||
"host": "1.1.1.1",
|
||||
"port": 80,
|
||||
"model": MODEL_PLUS_2PM,
|
||||
"sleep_period": 0,
|
||||
"gen": 2,
|
||||
}
|
||||
assert result2["context"]["unique_id"] == "AABBCCDDEEFF"
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
# discovery flow should have been aborted
|
||||
assert not hass.config_entries.flow.async_progress(DOMAIN)
|
||||
|
||||
|
||||
async def test_form_gen1_custom_port(
|
||||
hass: HomeAssistant,
|
||||
mock_block_device: Mock,
|
||||
|
@ -120,6 +120,11 @@ def config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
return config_entry
|
||||
|
||||
|
||||
async def mock_async_play_announcement(media_id: str) -> bool:
|
||||
"""Mock the announcement."""
|
||||
return True
|
||||
|
||||
|
||||
async def mock_async_browse(
|
||||
media_type: MediaType, limit: int, browse_id: tuple | None = None
|
||||
) -> dict | None:
|
||||
@ -222,6 +227,11 @@ def mock_pysqueezebox_player(uuid: str) -> MagicMock:
|
||||
mock_player.generate_image_url_from_track_id = MagicMock(
|
||||
return_value="http://lms.internal:9000/html/images/favorites.png"
|
||||
)
|
||||
mock_player.set_announce_volume = MagicMock(return_value=True)
|
||||
mock_player.set_announce_timeout = MagicMock(return_value=True)
|
||||
mock_player.async_play_announcement = AsyncMock(
|
||||
side_effect=mock_async_play_announcement
|
||||
)
|
||||
mock_player.name = TEST_PLAYER_NAME
|
||||
mock_player.player_id = uuid
|
||||
mock_player.mode = "stop"
|
||||
|
@ -65,7 +65,7 @@
|
||||
'original_name': None,
|
||||
'platform': 'squeezebox',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <MediaPlayerEntityFeature: 3078079>,
|
||||
'supported_features': <MediaPlayerEntityFeature: 4126655>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff',
|
||||
'unit_of_measurement': None,
|
||||
@ -88,7 +88,7 @@
|
||||
}),
|
||||
'repeat': <RepeatMode.OFF: 'off'>,
|
||||
'shuffle': False,
|
||||
'supported_features': <MediaPlayerEntityFeature: 3078079>,
|
||||
'supported_features': <MediaPlayerEntityFeature: 4126655>,
|
||||
'volume_level': 0.01,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
|
@ -10,9 +10,11 @@ from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_GROUP_MEMBERS,
|
||||
ATTR_MEDIA_ANNOUNCE,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_CONTENT_TYPE,
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
ATTR_MEDIA_EXTRA,
|
||||
ATTR_MEDIA_POSITION,
|
||||
ATTR_MEDIA_POSITION_UPDATED_AT,
|
||||
ATTR_MEDIA_REPEAT,
|
||||
@ -31,6 +33,8 @@ from homeassistant.components.media_player import (
|
||||
RepeatMode,
|
||||
)
|
||||
from homeassistant.components.squeezebox.const import (
|
||||
ATTR_ANNOUNCE_TIMEOUT,
|
||||
ATTR_ANNOUNCE_VOLUME,
|
||||
DISCOVERY_INTERVAL,
|
||||
DOMAIN,
|
||||
PLAYER_UPDATE_INTERVAL,
|
||||
@ -436,6 +440,115 @@ async def test_squeezebox_play(
|
||||
configured_player.async_play.assert_called_once()
|
||||
|
||||
|
||||
async def test_squeezebox_play_media_with_announce(
|
||||
hass: HomeAssistant, configured_player: MagicMock
|
||||
) -> None:
|
||||
"""Test play service call with announce."""
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||||
ATTR_MEDIA_CONTENT_ID: FAKE_VALID_ITEM_ID,
|
||||
ATTR_MEDIA_ANNOUNCE: True,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.async_load_url.assert_called_once_with(
|
||||
FAKE_VALID_ITEM_ID, "announce"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"announce_volume",
|
||||
["0.2", 0.2],
|
||||
)
|
||||
async def test_squeezebox_play_media_with_announce_volume(
|
||||
hass: HomeAssistant, configured_player: MagicMock, announce_volume: str | int
|
||||
) -> None:
|
||||
"""Test play service call with announce."""
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||||
ATTR_MEDIA_CONTENT_ID: FAKE_VALID_ITEM_ID,
|
||||
ATTR_MEDIA_ANNOUNCE: True,
|
||||
ATTR_MEDIA_EXTRA: {ATTR_ANNOUNCE_VOLUME: announce_volume},
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.set_announce_volume.assert_called_once_with(20)
|
||||
configured_player.async_load_url.assert_called_once_with(
|
||||
FAKE_VALID_ITEM_ID, "announce"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("announce_volume", ["1.1", 1.1, "text", "-1", -1, 0, "0"])
|
||||
async def test_squeezebox_play_media_with_announce_volume_invalid(
|
||||
hass: HomeAssistant, configured_player: MagicMock, announce_volume: str | int
|
||||
) -> None:
|
||||
"""Test play service call with announce and volume zero."""
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||||
ATTR_MEDIA_CONTENT_ID: FAKE_VALID_ITEM_ID,
|
||||
ATTR_MEDIA_ANNOUNCE: True,
|
||||
ATTR_MEDIA_EXTRA: {ATTR_ANNOUNCE_VOLUME: announce_volume},
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("announce_timeout", ["-1", "text", -1, 0, "0"])
|
||||
async def test_squeezebox_play_media_with_announce_timeout_invalid(
|
||||
hass: HomeAssistant, configured_player: MagicMock, announce_timeout: str | int
|
||||
) -> None:
|
||||
"""Test play service call with announce and invalid timeout."""
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||||
ATTR_MEDIA_CONTENT_ID: FAKE_VALID_ITEM_ID,
|
||||
ATTR_MEDIA_ANNOUNCE: True,
|
||||
ATTR_MEDIA_EXTRA: {ATTR_ANNOUNCE_TIMEOUT: announce_timeout},
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("announce_timeout", ["100", 100])
|
||||
async def test_squeezebox_play_media_with_announce_timeout(
|
||||
hass: HomeAssistant, configured_player: MagicMock, announce_timeout: str | int
|
||||
) -> None:
|
||||
"""Test play service call with announce."""
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_PLAY_MEDIA,
|
||||
{
|
||||
ATTR_ENTITY_ID: "media_player.test_player",
|
||||
ATTR_MEDIA_CONTENT_TYPE: MediaType.MUSIC,
|
||||
ATTR_MEDIA_CONTENT_ID: FAKE_VALID_ITEM_ID,
|
||||
ATTR_MEDIA_ANNOUNCE: True,
|
||||
ATTR_MEDIA_EXTRA: {ATTR_ANNOUNCE_TIMEOUT: announce_timeout},
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
configured_player.set_announce_timeout.assert_called_once_with(100)
|
||||
configured_player.async_load_url.assert_called_once_with(
|
||||
FAKE_VALID_ITEM_ID, "announce"
|
||||
)
|
||||
|
||||
|
||||
async def test_squeezebox_play_pause(
|
||||
hass: HomeAssistant, configured_player: MagicMock
|
||||
) -> None:
|
||||
|
@ -36,11 +36,11 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'RIVM - Rijksinstituut voor Volksgezondheid en Milieum, Landelijk Meetnet Luchtkwaliteit and World Air Quality Index Project',
|
||||
'friendly_name': 'de Jongweg, Utrecht Visbility using nephelometry',
|
||||
'friendly_name': 'de Jongweg, Utrecht Visibility using nephelometry',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.de_jongweg_utrecht_visbility_using_nephelometry',
|
||||
'entity_id': 'sensor.de_jongweg_utrecht_visibility_using_nephelometry',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
@ -1,5 +1,5 @@
|
||||
# serializer version: 1
|
||||
# name: test_binary_entities[binary_sensor.test_model_indoor_unit_auxilary_water_pump-entry]
|
||||
# name: test_binary_entities[binary_sensor.test_model_indoor_unit_auxiliary_water_pump-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@ -12,7 +12,7 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.test_model_indoor_unit_auxilary_water_pump',
|
||||
'entity_id': 'binary_sensor.test_model_indoor_unit_auxiliary_water_pump',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
@ -24,7 +24,7 @@
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.RUNNING: 'running'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Indoor unit auxilary water pump',
|
||||
'original_name': 'Indoor unit auxiliary water pump',
|
||||
'platform': 'weheat',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
@ -33,14 +33,14 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_binary_entities[binary_sensor.test_model_indoor_unit_auxilary_water_pump-state]
|
||||
# name: test_binary_entities[binary_sensor.test_model_indoor_unit_auxiliary_water_pump-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'running',
|
||||
'friendly_name': 'Test Model Indoor unit auxilary water pump',
|
||||
'friendly_name': 'Test Model Indoor unit auxiliary water pump',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.test_model_indoor_unit_auxilary_water_pump',
|
||||
'entity_id': 'binary_sensor.test_model_indoor_unit_auxiliary_water_pump',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
Loading…
x
Reference in New Issue
Block a user