Compare commits

...

20 Commits

Author SHA1 Message Date
Abílio Costa
5ae3e101d5 Merge branch 'dev' into ubisys_virtual 2026-03-01 19:48:26 +00:00
Joost Lekkerkerker
0aa66ed6cb Add select for SmartThings driving mode (#164522) 2026-03-01 19:11:58 +01:00
HadiAyache
6903463f14 Fix AccuWeather daily forecast crash when humidity average is missing (#163968)
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-01 17:19:15 +01:00
Brett Adams
a473010fee Update Tessie quality scale to silver (#164104)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-01 16:53:39 +01:00
Robin Lintermann
ddf7a783a8 Bump smarla quality scale to silver (#164325) 2026-03-01 11:52:11 +01:00
Joost Lekkerkerker
513e4d52fe Add button to reset HEPA filter to SmartThings (#164464) 2026-03-01 07:33:10 +01:00
Klaas Schoute
17bb14e260 Update error handling messages for Powerfox Local integration (#164465) 2026-03-01 07:32:36 +01:00
Brett Adams
cd1258464b Fix OAuth token type narrowing in Teslemetry (#164505) 2026-03-01 07:31:34 +01:00
Allen Porter
d3f5e0e6d7 Update nest access token error handling to use specific OAuth2 token request exceptions (#164506) 2026-03-01 07:26:07 +01:00
Joost Lekkerkerker
e124829364 Rename Overseerr integration to Seerr (#164060) 2026-02-28 23:07:31 +01:00
Jan Bouwhuis
87b83dcc1b Remove the MQTT object_id option after 6 months of deprecation (#164460)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 20:12:23 +01:00
Erik Montnemery
be9b47539d Revert "Remove unnecessary volume_up/volume_down overrides from frontier_silicon media player" (#164463) 2026-02-28 20:11:52 +01:00
Joost Lekkerkerker
be6ddc314c Add sound detection switch to SmartThings (#164470) 2026-02-28 20:11:13 +01:00
David Bonnes
c6f8a7b7e4 Harden test of an invalid service call for Evohome (#164458) 2026-02-28 20:10:11 +01:00
Joost Lekkerkerker
53da5612e9 Add fan speed to SmartThings vacuum (#164452)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 20:09:43 +01:00
Michael Davie
6cc56b76f9 Bump env-canada to 0.13.2 (#164480)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 20:08:17 +01:00
Tom Matheussen
03cb65d555 Require user code to be set when toggling Satel Integra switches (#164483) 2026-02-28 20:06:56 +01:00
Abílio Costa
73dd024933 Add merged PR count sensor to Github integration (#164405) 2026-02-28 15:13:17 +01:00
Barry vd. Heuvel
1c8c92bf8f Bump weheat to 2026.2.28 (#164456) 2026-02-28 14:40:58 +01:00
abmantis
9823e31206 Add Ubisys virtual integration 2026-02-27 10:58:27 +00:00
50 changed files with 545 additions and 409 deletions

View File

@@ -0,0 +1,5 @@
{
"domain": "ubisys",
"name": "Ubisys",
"iot_standards": ["zigbee"]
}

View File

@@ -191,7 +191,7 @@ class AccuWeatherEntity(
{
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCoverDay"],
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"]["Average"],
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"].get("Average"),
ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"][ATTR_VALUE],
ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"][ATTR_VALUE],
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperatureMax"][

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.12.4"]
"requirements": ["env-canada==0.13.2"]
}

View File

@@ -151,8 +151,6 @@ class AFSAPIDevice(MediaPlayerEntity):
# If call to get_volume fails set to 0 and try again next time.
if not self._max_volume:
self._max_volume = int(await afsapi.get_volume_steps() or 1) - 1
if self._max_volume:
self._attr_volume_step = 1 / self._max_volume
if self._attr_state != MediaPlayerState.OFF:
info_name = await afsapi.get_play_name()
@@ -241,6 +239,18 @@ class AFSAPIDevice(MediaPlayerEntity):
await self.fs_device.set_mute(mute)
# volume
async def async_volume_up(self) -> None:
"""Send volume up command."""
volume = await self.fs_device.get_volume()
volume = int(volume or 0) + 1
await self.fs_device.set_volume(min(volume, self._max_volume or 1))
async def async_volume_down(self) -> None:
"""Send volume down command."""
volume = await self.fs_device.get_volume()
volume = int(volume or 0) - 1
await self.fs_device.set_volume(max(volume, 0))
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume command."""
if self._max_volume: # Can't do anything sensible if not set

View File

@@ -78,6 +78,12 @@ query ($owner: String!, $repository: String!) {
number
}
}
merged_pull_request: pullRequests(
first:1
states: MERGED
) {
total: totalCount
}
release: latestRelease {
name
url

View File

@@ -28,6 +28,9 @@
"latest_tag": {
"default": "mdi:tag"
},
"merged_pulls_count": {
"default": "mdi:source-merge"
},
"pulls_count": {
"default": "mdi:source-pull"
},

View File

@@ -75,6 +75,13 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data["pull_request"]["total"],
),
GitHubSensorEntityDescription(
key="merged_pulls_count",
translation_key="merged_pulls_count",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL,
value_fn=lambda data: data["merged_pull_request"]["total"],
),
GitHubSensorEntityDescription(
key="latest_commit",
translation_key="latest_commit",

View File

@@ -48,6 +48,10 @@
"latest_tag": {
"name": "Latest tag"
},
"merged_pulls_count": {
"name": "Merged pull requests",
"unit_of_measurement": "pull requests"
},
"pulls_count": {
"name": "Pull requests",
"unit_of_measurement": "pull requests"

View File

@@ -107,7 +107,6 @@ ABBREVIATIONS = {
"modes": "modes",
"name": "name",
"o": "origin",
"obj_id": "object_id",
"off_dly": "off_delay",
"on_cmd_type": "on_command_type",
"ops": "options",

View File

@@ -268,7 +268,6 @@ CONF_VIA_DEVICE = "via_device"
CONF_DEPRECATED_VIA_HUB = "via_hub"
CONF_SUGGESTED_AREA = "suggested_area"
CONF_CONFIGURATION_URL = "configuration_url"
CONF_OBJECT_ID = "object_id"
CONF_SUPPORT_URL = "support_url"
DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE = "{{action}}"

View File

@@ -29,7 +29,6 @@ from homeassistant.const import (
CONF_MODEL_ID,
CONF_NAME,
CONF_UNIQUE_ID,
CONF_URL,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import Event, HassJobType, HomeAssistant, callback
@@ -84,8 +83,6 @@ from .const import (
CONF_JSON_ATTRS_TEMPLATE,
CONF_JSON_ATTRS_TOPIC,
CONF_MANUFACTURER,
CONF_OBJECT_ID,
CONF_ORIGIN,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS,
@@ -1412,58 +1409,12 @@ class MqttEntity(
"""Set entity_id from default_entity_id if defined in config."""
object_id: str
default_entity_id: str | None
# Setting the default entity_id through the CONF_OBJECT_ID is deprecated
# Support will be removed with HA Core 2026.4
if (
CONF_DEFAULT_ENTITY_ID not in self._config
and CONF_OBJECT_ID not in self._config
):
return
if (default_entity_id := self._config.get(CONF_DEFAULT_ENTITY_ID)) is None:
object_id = self._config[CONF_OBJECT_ID]
else:
_, _, object_id = default_entity_id.partition(".")
return
_, _, object_id = default_entity_id.partition(".")
self.entity_id = async_generate_entity_id(
self._entity_id_format, object_id, None, self.hass
)
if CONF_OBJECT_ID in self._config:
domain = self.entity_id.split(".")[0]
if not self._discovery:
async_create_issue(
self.hass,
DOMAIN,
self.entity_id,
issue_domain=DOMAIN,
is_fixable=False,
breaks_in_ha_version="2026.4",
severity=IssueSeverity.WARNING,
learn_more_url=f"{learn_more_url(domain)}#default_enity_id",
translation_placeholders={
"entity_id": self.entity_id,
"object_id": self._config[CONF_OBJECT_ID],
"domain": domain,
},
translation_key="deprecated_object_id",
)
elif CONF_DEFAULT_ENTITY_ID not in self._config:
if CONF_ORIGIN in self._config:
origin_name = self._config[CONF_ORIGIN][CONF_NAME]
url = self._config[CONF_ORIGIN].get(CONF_URL)
origin = f"[{origin_name}]({url})" if url else origin_name
else:
origin = "the integration"
_LOGGER.warning(
"The configuration for entity %s uses the deprecated option "
"`object_id` to set the default entity id. Replace the "
'`"object_id": "%s"` option with `"default_entity_id": '
'"%s"` in your published discovery configuration to fix this '
"issue, or contact the maintainer of %s that published this config "
"to fix this. This will stop working in Home Assistant Core 2026.4",
self.entity_id,
self._config[CONF_OBJECT_ID],
f"{domain}.{self._config[CONF_OBJECT_ID]}",
origin,
)
if self.unique_id is None:
return
@@ -1475,7 +1426,8 @@ class MqttEntity(
(entity_platform, DOMAIN, self.unique_id)
)
) and deleted_entry.entity_id != self.entity_id:
# Plan to update the entity_id basis on `object_id` if a deleted entity was found
# Plan to update the entity_id based on `default_entity_id`
# if a deleted entity was found
self._update_registry_entity_id = self.entity_id
@final

View File

@@ -42,7 +42,6 @@ from .const import (
CONF_JSON_ATTRS_TEMPLATE,
CONF_JSON_ATTRS_TOPIC,
CONF_MANUFACTURER,
CONF_OBJECT_ID,
CONF_ORIGIN,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
@@ -173,7 +172,6 @@ MQTT_ENTITY_COMMON_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template,
vol.Optional(CONF_DEFAULT_ENTITY_ID): cv.string,
vol.Optional(CONF_OBJECT_ID): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)

View File

@@ -1116,10 +1116,6 @@
}
},
"issues": {
"deprecated_object_id": {
"description": "Entity {entity_id} uses the `object_id` option which is deprecated. To fix the issue, replace the `object_id: {object_id}` option with `default_entity_id: {domain}.{object_id}` in your \"configuration.yaml\", and restart Home Assistant.",
"title": "Deprecated option object_id used"
},
"invalid_platform_config": {
"description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/config/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue.",
"title": "Invalid config found for MQTT {domain} item"

View File

@@ -7,7 +7,7 @@ import asyncio
from http import HTTPStatus
import logging
from aiohttp import ClientError, ClientResponseError, web
from aiohttp import ClientError, web
from google_nest_sdm.camera_traits import CameraClipPreviewTrait
from google_nest_sdm.device import Device
from google_nest_sdm.device_manager import DeviceManager
@@ -43,6 +43,8 @@ from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
Unauthorized,
)
from homeassistant.helpers import (
@@ -253,11 +255,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: NestConfigEntry) -> bool
auth = await api.new_auth(hass, entry)
try:
await auth.async_get_access_token()
except ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="reauth_required"
) from err
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="reauth_required"
) from err
except OAuth2TokenRequestError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN, translation_key="auth_server_error"
) from err

View File

@@ -69,7 +69,7 @@ class OverseerrConfigFlow(ConfigFlow, domain=DOMAIN):
else:
if self.source == SOURCE_USER:
return self.async_create_entry(
title="Overseerr",
title="Seerr",
data={
CONF_HOST: host,
CONF_PORT: port,

View File

@@ -1,6 +1,6 @@
{
"domain": "overseerr",
"name": "Overseerr",
"name": "Seerr",
"after_dependencies": ["cloud"],
"codeowners": ["@joostlek", "@AmGarera"],
"config_flow": true,

View File

@@ -25,8 +25,8 @@
"url": "[%key:common::config_flow::data::url%]"
},
"data_description": {
"api_key": "The API key of the Overseerr instance.",
"url": "The URL of the Overseerr instance."
"api_key": "The API key of the Seerr instance.",
"url": "The URL of the Seerr instance."
}
}
}
@@ -137,11 +137,11 @@
},
"services": {
"get_requests": {
"description": "Retrieves a list of media requests from Overseerr.",
"description": "Retrieves a list of media requests from Seerr.",
"fields": {
"config_entry_id": {
"description": "The Overseerr instance to get requests from.",
"name": "Overseerr instance"
"description": "The Seerr instance to get requests from.",
"name": "Seerr instance"
},
"requested_by": {
"description": "Filter the requests by the user ID that requested them.",

View File

@@ -49,12 +49,12 @@ class PowerfoxLocalDataUpdateCoordinator(DataUpdateCoordinator[LocalResponse]):
except PowerfoxAuthenticationError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": str(err)},
translation_key="auth_failed",
translation_placeholders={"host": self.config_entry.data[CONF_HOST]},
) from err
except PowerfoxConnectionError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
translation_key="connection_error",
translation_placeholders={"host": self.config_entry.data[CONF_HOST]},
) from err

View File

@@ -56,11 +56,11 @@
}
},
"exceptions": {
"invalid_auth": {
"message": "Error while authenticating with the device: {error}"
"auth_failed": {
"message": "Authentication with the Poweropti device at {host} failed. Please check your API key."
},
"update_failed": {
"message": "Error while updating the device: {error}"
"connection_error": {
"message": "Could not connect to the Poweropti device at {host}. Please check if the device is online and reachable."
}
}
}

View File

@@ -162,6 +162,11 @@
}
}
},
"exceptions": {
"missing_output_access_code": {
"message": "Cannot control switchable outputs because no user code is configured for this Satel Integra entry. Configure a code in the integration options to enable output control."
}
},
"issues": {
"deprecated_yaml_import_issue_cannot_connect": {
"description": "Configuring {integration_title} using YAML is being removed but there was an connection error importing your existing configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the `{domain}` YAML configuration from your configuration.yaml file and add the {integration_title} integration manually.",

View File

@@ -8,9 +8,14 @@ from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import CONF_CODE
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CONF_SWITCHABLE_OUTPUT_NUMBER, SUBENTRY_TYPE_SWITCHABLE_OUTPUT
from .const import (
CONF_SWITCHABLE_OUTPUT_NUMBER,
DOMAIN,
SUBENTRY_TYPE_SWITCHABLE_OUTPUT,
)
from .coordinator import SatelConfigEntry, SatelIntegraOutputsCoordinator
from .entity import SatelIntegraEntity
@@ -83,12 +88,24 @@ class SatelIntegraSwitch(
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the device on."""
if self._code is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="missing_output_access_code",
)
await self._controller.set_output(self._code, self._device_number, True)
self._attr_is_on = True
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the device off."""
if self._code is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="missing_output_access_code",
)
await self._controller.set_output(self._code, self._device_number, False)
self._attr_is_on = False
self.async_write_ha_state()

View File

@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "cloud_push",
"loggers": ["pysmarlaapi", "pysignalr"],
"quality_scale": "bronze",
"quality_scale": "silver",
"requirements": ["pysmarlaapi==1.0.1"]
}

View File

@@ -22,6 +22,7 @@ class SmartThingsButtonDescription(ButtonEntityDescription):
key: Capability
command: Command
component: str = MAIN
CAPABILITIES_TO_BUTTONS: dict[Capability | str, SmartThingsButtonDescription] = {
@@ -42,6 +43,13 @@ CAPABILITIES_TO_BUTTONS: dict[Capability | str, SmartThingsButtonDescription] =
command=Command.RESET_HOOD_FILTER,
entity_category=EntityCategory.DIAGNOSTIC,
),
Capability.CUSTOM_HEPA_FILTER: SmartThingsButtonDescription(
key=Capability.CUSTOM_HEPA_FILTER,
translation_key="reset_hepa_filter",
command=Command.RESET_HEPA_FILTER,
entity_category=EntityCategory.DIAGNOSTIC,
component="station",
),
}
@@ -53,12 +61,11 @@ async def async_setup_entry(
"""Add button entities for a config entry."""
entry_data = entry.runtime_data
async_add_entities(
SmartThingsButtonEntity(
entry_data.client, device, CAPABILITIES_TO_BUTTONS[capability]
)
SmartThingsButtonEntity(entry_data.client, device, description)
for capability, description in CAPABILITIES_TO_BUTTONS.items()
for device in entry_data.devices.values()
for capability in device.status[MAIN]
if capability in CAPABILITIES_TO_BUTTONS
if description.component in device.status
and capability in device.status[description.component]
)
@@ -74,9 +81,9 @@ class SmartThingsButtonEntity(SmartThingsEntity, ButtonEntity):
entity_description: SmartThingsButtonDescription,
) -> None:
"""Initialize the instance."""
super().__init__(client, device, set())
super().__init__(client, device, set(), component=entity_description.component)
self.entity_description = entity_description
self._attr_unique_id = f"{device.device.device_id}_{MAIN}_{entity_description.key}_{entity_description.command}"
self._attr_unique_id = f"{device.device.device_id}_{entity_description.component}_{entity_description.key}_{entity_description.command}"
async def async_press(self) -> None:
"""Press the button."""

View File

@@ -24,6 +24,9 @@
}
},
"button": {
"reset_hepa_filter": {
"default": "mdi:air-filter"
},
"reset_water_filter": {
"default": "mdi:reload"
},
@@ -93,6 +96,9 @@
"stop": "mdi:stop"
}
},
"robot_cleaner_driving_mode": {
"default": "mdi:car-cog"
},
"selected_zone": {
"state": {
"all": "mdi:card",
@@ -219,6 +225,9 @@
"sanitizing_wash": {
"default": "mdi:lotion"
},
"sound_detection": {
"default": "mdi:home-sound-in"
},
"sound_effect": {
"default": "mdi:volume-high",
"state": {

View File

@@ -26,6 +26,12 @@ LAMP_TO_HA = {
"off": "off",
}
DRIVING_MODE_TO_HA = {
"areaThenWalls": "area_then_walls",
"wallFirst": "walls_first",
"quickCleaningZigzagPattern": "quick_clean_zigzag_pattern",
}
WASHER_SOIL_LEVEL_TO_HA = {
"none": "none",
"heavy": "heavy",
@@ -187,6 +193,15 @@ CAPABILITIES_TO_SELECT: dict[Capability | str, SmartThingsSelectDescription] = {
options_map=WASHER_WATER_TEMPERATURE_TO_HA,
entity_category=EntityCategory.CONFIG,
),
Capability.SAMSUNG_CE_ROBOT_CLEANER_DRIVING_MODE: SmartThingsSelectDescription(
key=Capability.SAMSUNG_CE_ROBOT_CLEANER_DRIVING_MODE,
translation_key="robot_cleaner_driving_mode",
options_attribute=Attribute.SUPPORTED_DRIVING_MODES,
status_attribute=Attribute.DRIVING_MODE,
command=Command.SET_DRIVING_MODE,
options_map=DRIVING_MODE_TO_HA,
entity_category=EntityCategory.CONFIG,
),
Capability.SAMSUNG_CE_DUST_FILTER_ALARM: SmartThingsSelectDescription(
key=Capability.SAMSUNG_CE_DUST_FILTER_ALARM,
translation_key="dust_filter_alarm",

View File

@@ -84,6 +84,9 @@
}
},
"button": {
"reset_hepa_filter": {
"name": "Reset HEPA filter"
},
"reset_hood_filter": {
"name": "Reset filter"
},
@@ -223,6 +226,14 @@
"stop": "[%key:common::state::stopped%]"
}
},
"robot_cleaner_driving_mode": {
"name": "Driving mode",
"state": {
"area_then_walls": "Area then walls",
"quick_clean_zigzag_pattern": "Quick clean in a zigzag pattern",
"walls_first": "Walls first"
}
},
"selected_zone": {
"name": "Selected zone",
"state": {
@@ -904,6 +915,9 @@
"sanitizing_wash": {
"name": "Sanitizing wash"
},
"sound_detection": {
"name": "Sound detection"
},
"sound_effect": {
"name": "Sound effect"
},
@@ -919,6 +933,20 @@
"wrinkle_prevent": {
"name": "Wrinkle prevent"
}
},
"vacuum": {
"vacuum": {
"state_attributes": {
"fan_speed": {
"state": {
"maximum": "Maximum",
"normal": "Normal",
"quiet": "Quiet",
"smart": "Smart"
}
}
}
}
}
},
"exceptions": {

View File

@@ -170,6 +170,15 @@ CAPABILITY_TO_SWITCHES: dict[Capability | str, SmartThingsSwitchEntityDescriptio
on_command=Command.DO_NOT_DISTURB_ON,
off_command=Command.DO_NOT_DISTURB_OFF,
),
Capability.SOUND_DETECTION: SmartThingsSwitchEntityDescription(
key=Capability.SOUND_DETECTION,
translation_key="sound_detection",
status_attribute=Attribute.SOUND_DETECTION_STATE,
entity_category=EntityCategory.CONFIG,
on_key="enabled",
on_command=Command.ENABLE_SOUND_DETECTION,
off_command=Command.DISABLE_SOUND_DETECTION,
),
}
DISHWASHER_WASHING_OPTIONS_TO_SWITCHES: dict[
Attribute | str, SmartThingsDishwasherWashingOptionSwitchEntityDescription

View File

@@ -22,6 +22,15 @@ from .entity import SmartThingsEntity
_LOGGER = logging.getLogger(__name__)
TURBO_MODE_TO_FAN_SPEED = {
"silence": "normal",
"on": "maximum",
"off": "smart",
"extraSilence": "quiet",
}
FAN_SPEED_TO_TURBO_MODE = {v: k for k, v in TURBO_MODE_TO_FAN_SPEED.items()}
async def async_setup_entry(
hass: HomeAssistant,
@@ -41,20 +50,26 @@ class SamsungJetBotVacuum(SmartThingsEntity, StateVacuumEntity):
"""Representation of a Vacuum."""
_attr_name = None
_attr_supported_features = (
VacuumEntityFeature.START
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.STATE
)
_attr_translation_key = "vacuum"
def __init__(self, client: SmartThings, device: FullDevice) -> None:
"""Initialize the Samsung robot cleaner vacuum entity."""
super().__init__(
client,
device,
{Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE},
{
Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE,
Capability.ROBOT_CLEANER_TURBO_MODE,
},
)
self._attr_supported_features = (
VacuumEntityFeature.START
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.PAUSE
| VacuumEntityFeature.STATE
)
if self.supports_capability(Capability.ROBOT_CLEANER_TURBO_MODE):
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
@property
def activity(self) -> VacuumActivity | None:
@@ -74,6 +89,23 @@ class SamsungJetBotVacuum(SmartThingsEntity, StateVacuumEntity):
"charging": VacuumActivity.DOCKED,
}.get(status)
@property
def fan_speed_list(self) -> list[str]:
"""Return the list of available fan speeds."""
if not self.supports_capability(Capability.ROBOT_CLEANER_TURBO_MODE):
return []
return list(TURBO_MODE_TO_FAN_SPEED.values())
@property
def fan_speed(self) -> str | None:
"""Return the current fan speed."""
if not self.supports_capability(Capability.ROBOT_CLEANER_TURBO_MODE):
return None
turbo_mode = self.get_attribute_value(
Capability.ROBOT_CLEANER_TURBO_MODE, Attribute.ROBOT_CLEANER_TURBO_MODE
)
return TURBO_MODE_TO_FAN_SPEED.get(turbo_mode)
async def async_start(self) -> None:
"""Start the vacuum's operation."""
await self.execute_device_command(
@@ -93,3 +125,12 @@ class SamsungJetBotVacuum(SmartThingsEntity, StateVacuumEntity):
Capability.SAMSUNG_CE_ROBOT_CLEANER_OPERATING_STATE,
Command.RETURN_TO_HOME,
)
async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
"""Set the fan speed."""
turbo_mode = FAN_SPEED_TO_TURBO_MODE[fan_speed]
await self.execute_device_command(
Capability.ROBOT_CLEANER_TURBO_MODE,
Command.SET_ROBOT_CLEANER_TURBO_MODE,
turbo_mode,
)

View File

@@ -3,7 +3,7 @@
import asyncio
from collections.abc import Callable
from functools import partial
from typing import Any, Final
from typing import Any, Final, cast
from aiohttp import ClientError, ClientResponseError
from tesla_fleet_api.const import Scope
@@ -106,7 +106,7 @@ async def _get_access_token(oauth_session: OAuth2Session) -> str:
translation_domain=DOMAIN,
translation_key="not_ready_connection_error",
) from err
return str(oauth_session.token[CONF_ACCESS_TOKEN])
return cast(str, oauth_session.token[CONF_ACCESS_TOKEN])
async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -> bool:

View File

@@ -7,5 +7,6 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["tessie", "tesla-fleet-api"],
"quality_scale": "silver",
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.4.3"]
}

View File

@@ -34,7 +34,10 @@ rules:
comment: |
No custom actions are defined. Only entity-based actions exist.
config-entry-unloading: done
docs-configuration-parameters: todo
docs-configuration-parameters:
status: exempt
comment: |
No options flow and no configurable options after initial setup.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/weheat",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["weheat==2026.1.25"]
"requirements": ["weheat==2026.2.28"]
}

View File

@@ -5036,7 +5036,7 @@
"iot_class": "local_polling"
},
"overseerr": {
"name": "Overseerr",
"name": "Seerr",
"integration_type": "service",
"config_flow": true,
"iot_class": "local_push"
@@ -7338,6 +7338,12 @@
}
}
},
"ubisys": {
"name": "Ubisys",
"iot_standards": [
"zigbee"
]
},
"ubiwizz": {
"name": "Ubiwizz",
"integration_type": "virtual",

4
requirements_all.txt generated
View File

@@ -906,7 +906,7 @@ enocean==0.50
enturclient==0.2.4
# homeassistant.components.environment_canada
env-canada==0.12.4
env-canada==0.13.2
# homeassistant.components.season
ephem==4.1.6
@@ -3256,7 +3256,7 @@ webio-api==0.1.12
webmin-xmlrpc==0.0.2
# homeassistant.components.weheat
weheat==2026.1.25
weheat==2026.2.28
# homeassistant.components.whirlpool
whirlpool-sixth-sense==1.0.3

View File

@@ -797,7 +797,7 @@ energyzero==4.0.1
enocean==0.50
# homeassistant.components.environment_canada
env-canada==0.12.4
env-canada==0.13.2
# homeassistant.components.season
ephem==4.1.6
@@ -2741,7 +2741,7 @@ webio-api==0.1.12
webmin-xmlrpc==0.0.2
# homeassistant.components.weheat
weheat==2026.1.25
weheat==2026.2.28
# homeassistant.components.whirlpool
whirlpool-sixth-sense==1.0.3

View File

@@ -1945,7 +1945,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
"template",
"tesla_fleet",
"tesla_wall_connector",
"tessie",
"tfiac",
"thermobeacon",
"thermopro",

View File

@@ -136,6 +136,31 @@ async def test_forecast_service(
assert response == snapshot
async def test_forecast_daily_missing_average_humidity(
hass: HomeAssistant,
mock_accuweather_client: AsyncMock,
) -> None:
"""Test daily forecast does not crash when average humidity is missing."""
mock_accuweather_client.async_get_daily_forecast.return_value[0][
"RelativeHumidityDay"
] = {}
await init_integration(hass)
response = await hass.services.async_call(
WEATHER_DOMAIN,
SERVICE_GET_FORECASTS,
{
"entity_id": "weather.home",
"type": "daily",
},
blocking=True,
return_response=True,
)
assert response["weather.home"]["forecast"][0].get("humidity") is None
async def test_forecast_subscription(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,

View File

@@ -194,7 +194,7 @@ async def test_zone_services_with_ctl_id(
) -> None:
"""Test calling zone-only services with a non-zone entity_id fail."""
with pytest.raises(ServiceValidationError) as excinfo:
with pytest.raises(ServiceValidationError) as exc_info:
await hass.services.async_call(
DOMAIN,
service,
@@ -203,4 +203,5 @@ async def test_zone_services_with_ctl_id(
blocking=True,
)
assert excinfo.value.translation_key == "zone_only_service"
assert exc_info.value.translation_key == "zone_only_service"
assert exc_info.value.translation_placeholders == {"service": service}

View File

@@ -49,6 +49,9 @@
}
]
},
"merged_pull_request": {
"total": 42
},
"release": {
"name": "v1.0.0",
"url": "https://github.com/octocat/Hello-World/releases/v1.0.0",

View File

@@ -1330,257 +1330,6 @@ async def test_discover_alarm_control_panel(
].discovery_already_discovered
@pytest.mark.parametrize(
("topic", "config", "entity_id", "name", "domain", "deprecation_warning"),
[
(
"homeassistant/alarm_control_panel/object/bla/config",
'{ "name": "Hello World 1", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"alarm_control_panel.hello_id",
"Hello World 1",
"alarm_control_panel",
True,
),
(
"homeassistant/binary_sensor/object/bla/config",
'{ "name": "Hello World 2", "obj_id": "hello_id", "state_topic": "test-topic" }',
"binary_sensor.hello_id",
"Hello World 2",
"binary_sensor",
True,
),
(
"homeassistant/button/object/bla/config",
'{ "name": "Hello World button", "obj_id": "hello_id", "command_topic": "test-topic" }',
"button.hello_id",
"Hello World button",
"button",
True,
),
(
"homeassistant/camera/object/bla/config",
'{ "name": "Hello World 3", "obj_id": "hello_id", "state_topic": "test-topic", "topic": "test-topic" }',
"camera.hello_id",
"Hello World 3",
"camera",
True,
),
(
"homeassistant/climate/object/bla/config",
'{ "name": "Hello World 4", "obj_id": "hello_id", "state_topic": "test-topic" }',
"climate.hello_id",
"Hello World 4",
"climate",
True,
),
(
"homeassistant/cover/object/bla/config",
'{ "name": "Hello World 5", "obj_id": "hello_id", "state_topic": "test-topic" }',
"cover.hello_id",
"Hello World 5",
"cover",
True,
),
(
"homeassistant/fan/object/bla/config",
'{ "name": "Hello World 6", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"fan.hello_id",
"Hello World 6",
"fan",
True,
),
(
"homeassistant/humidifier/object/bla/config",
'{ "name": "Hello World 7", "obj_id": "hello_id", "state_topic": "test-topic", "target_humidity_command_topic": "test-topic", "command_topic": "test-topic" }',
"humidifier.hello_id",
"Hello World 7",
"humidifier",
True,
),
(
"homeassistant/number/object/bla/config",
'{ "name": "Hello World 8", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"number.hello_id",
"Hello World 8",
"number",
True,
),
(
"homeassistant/scene/object/bla/config",
'{ "name": "Hello World 9", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"scene.hello_id",
"Hello World 9",
"scene",
True,
),
(
"homeassistant/select/object/bla/config",
'{ "name": "Hello World 10", "obj_id": "hello_id", "state_topic": "test-topic", "options": [ "opt1", "opt2" ], "command_topic": "test-topic" }',
"select.hello_id",
"Hello World 10",
"select",
True,
),
(
"homeassistant/sensor/object/bla/config",
'{ "name": "Hello World 11", "obj_id": "hello_id", "state_topic": "test-topic" }',
"sensor.hello_id",
"Hello World 11",
"sensor",
True,
),
(
"homeassistant/switch/object/bla/config",
'{ "name": "Hello World 12", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"switch.hello_id",
"Hello World 12",
"switch",
True,
),
(
"homeassistant/light/object/bla/config",
'{ "name": "Hello World 13", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"light.hello_id",
"Hello World 13",
"light",
True,
),
(
"homeassistant/light/object/bla/config",
'{ "name": "Hello World 14", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic", "schema": "json" }',
"light.hello_id",
"Hello World 14",
"light",
True,
),
(
"homeassistant/light/object/bla/config",
'{ "name": "Hello World 15", "obj_id": "hello_id", "state_topic": "test-topic", "command_off_template": "template", "command_on_template": "template", "command_topic": "test-topic", "schema": "template" }',
"light.hello_id",
"Hello World 15",
"light",
True,
),
(
"homeassistant/vacuum/object/bla/config",
'{ "name": "Hello World 16", "obj_id": "hello_id", "state_topic": "test-topic", "schema": "state" }',
"vacuum.hello_id",
"Hello World 16",
"vacuum",
True,
),
(
"homeassistant/valve/object/bla/config",
'{ "name": "Hello World 17", "obj_id": "hello_id", "state_topic": "test-topic" }',
"valve.hello_id",
"Hello World 17",
"valve",
True,
),
(
"homeassistant/lock/object/bla/config",
'{ "name": "Hello World 18", "obj_id": "hello_id", "state_topic": "test-topic", "command_topic": "test-topic" }',
"lock.hello_id",
"Hello World 18",
"lock",
True,
),
(
"homeassistant/device_tracker/object/bla/config",
'{ "name": "Hello World 19", "obj_id": "hello_id", "state_topic": "test-topic" }',
"device_tracker.hello_id",
"Hello World 19",
"device_tracker",
True,
),
(
"homeassistant/binary_sensor/object/bla/config",
'{ "name": "Hello World 2", "obj_id": "hello_id", '
'"o": {"name": "X2mqtt"}, "state_topic": "test-topic" }',
"binary_sensor.hello_id",
"Hello World 2",
"binary_sensor",
True,
),
(
"homeassistant/button/object/bla/config",
'{ "name": "Hello World button", "obj_id": "hello_id", '
'"o": {"name": "X2mqtt", "url": "https://example.com/x2mqtt"}, '
'"command_topic": "test-topic" }',
"button.hello_id",
"Hello World button",
"button",
True,
),
(
"homeassistant/alarm_control_panel/object/bla/config",
'{ "name": "Hello World 1", "def_ent_id": "alarm_control_panel.hello_id", '
'"state_topic": "test-topic", "command_topic": "test-topic" }',
"alarm_control_panel.hello_id",
"Hello World 1",
"alarm_control_panel",
False,
),
(
"homeassistant/binary_sensor/object/bla/config",
'{ "name": "Hello World 2", "def_ent_id": "binary_sensor.hello_id", '
'"o": {"name": "X2mqtt"}, "state_topic": "test-topic" }',
"binary_sensor.hello_id",
"Hello World 2",
"binary_sensor",
False,
),
(
"homeassistant/button/object/bla/config",
'{ "name": "Hello World button", "def_ent_id": "button.hello_id", '
'"o": {"name": "X2mqtt", "url": "https://example.com/x2mqtt"}, '
'"command_topic": "test-topic" }',
"button.hello_id",
"Hello World button",
"button",
False,
),
(
"homeassistant/button/object/bla/config",
'{ "name": "Hello World button", "def_ent_id": "button.hello_id", '
'"obj_id": "hello_id_old", '
'"o": {"name": "X2mqtt", "url": "https://example.com/x2mqtt"}, '
'"command_topic": "test-topic" }',
"button.hello_id",
"Hello World button",
"button",
False,
),
],
)
async def test_discovery_with_object_id(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,
caplog: pytest.LogCaptureFixture,
topic: str,
config: str,
entity_id: str,
name: str,
domain: str,
deprecation_warning: bool,
) -> None:
"""Test discovering an MQTT entity with object_id."""
await mqtt_mock_entry()
async_fire_mqtt_message(hass, topic, config)
await hass.async_block_till_done()
state = hass.states.get(entity_id)
assert state is not None
assert state.name == name
assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered
assert (
f"The configuration for entity {domain}.hello_id uses the deprecated option `object_id`"
in caplog.text
) is deprecation_warning
async def test_discovery_with_default_entity_id_for_previous_deleted_entity(
hass: HomeAssistant,
mqtt_mock_entry: MqttMockHAClientGenerator,

View File

@@ -468,40 +468,6 @@ async def test_value_template_fails(
)
@pytest.mark.parametrize(
"hass_config",
[
{
mqtt.DOMAIN: {
sensor.DOMAIN: {
"name": "test",
"state_topic": "test-topic",
"object_id": "test",
}
}
},
],
)
async def test_deprecated_option_object_id_is_used_in_yaml(
hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator
) -> None:
"""Test issue registry in case the deprecated option object_id was used in YAML."""
await mqtt_mock_entry()
await hass.async_block_till_done()
state = hass.states.get("sensor.test")
assert state is not None
issue_registry = ir.async_get(hass)
issue = issue_registry.async_get_issue(mqtt.DOMAIN, "sensor.test")
assert issue is not None
assert issue.translation_placeholders == {
"entity_id": "sensor.test",
"object_id": "test",
"domain": "sensor",
}
@pytest.mark.parametrize(
"mqtt_config_subentries_data",
[

View File

@@ -54,7 +54,7 @@ async def test_full_flow(
{CONF_URL: "http://overseerr.test", CONF_API_KEY: "test-key"},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "Overseerr"
assert result["title"] == "Seerr"
assert result["data"] == {
CONF_HOST: "overseerr.test",
CONF_PORT: 80,

View File

@@ -15,12 +15,14 @@ from homeassistant.components.switch import (
)
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_CODE,
STATE_OFF,
STATE_ON,
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.device_registry import DeviceRegistry
from homeassistant.helpers.entity_registry import EntityRegistry
@@ -176,3 +178,35 @@ async def test_switch_last_reported(
assert first_reported != hass.states.get("switch.switchable_output").last_reported
assert len(events) == 1 # last_reported shall not fire state_changed
async def test_switch_actions_require_code(
hass: HomeAssistant,
mock_satel: AsyncMock,
mock_config_entry_with_subentries: MockConfigEntry,
) -> None:
"""Test switch actions fail when access code is missing."""
await setup_integration(hass, mock_config_entry_with_subentries)
hass.config_entries.async_update_entry(
mock_config_entry_with_subentries, options={CONF_CODE: None}
)
await hass.async_block_till_done()
# Turning the device on or off should raise ServiceValidationError.
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "switch.switchable_output"},
blocking=True,
)
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "switch.switchable_output"},
blocking=True,
)

View File

@@ -100,7 +100,7 @@
},
"robotCleanerTurboMode": {
"robotCleanerTurboMode": {
"value": "off",
"value": "on",
"timestamp": "2026-02-27T10:49:04.309Z"
}
},

View File

@@ -440,3 +440,52 @@
'state': 'unknown',
})
# ---
# name: test_all_entities[da_rvc_map_01011][button.robot_vacuum_reset_hepa_filter-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'button.robot_vacuum_reset_hepa_filter',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Reset HEPA filter',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Reset HEPA filter',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'reset_hepa_filter',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_station_custom.hepaFilter_resetHepaFilter',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][button.robot_vacuum_reset_hepa_filter-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot Vacuum Reset HEPA filter',
}),
'context': <ANY>,
'entity_id': 'button.robot_vacuum_reset_hepa_filter',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---

View File

@@ -415,6 +415,66 @@
'state': 'high',
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_driving_mode-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'area_then_walls',
'walls_first',
'quick_clean_zigzag_pattern',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'select',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'select.robot_vacuum_driving_mode',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Driving mode',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Driving mode',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'robot_cleaner_driving_mode',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_samsungce.robotCleanerDrivingMode_drivingMode_drivingMode',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_driving_mode-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot Vacuum Driving mode',
'options': list([
'area_then_walls',
'walls_first',
'quick_clean_zigzag_pattern',
]),
}),
'context': <ANY>,
'entity_id': 'select.robot_vacuum_driving_mode',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'area_then_walls',
})
# ---
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_lamp-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -11016,7 +11016,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
'state': 'on',
})
# ---
# name: test_all_entities[da_rvc_normal_000001][sensor.robot_vacuum_battery-entry]

View File

@@ -1028,6 +1028,55 @@
'state': 'on',
})
# ---
# name: test_all_entities[da_rvc_map_01011][switch.robot_vacuum_sound_detection-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.robot_vacuum_sound_detection',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Sound detection',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Sound detection',
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'sound_detection',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main_soundDetection_soundDetectionState_soundDetectionState',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[da_rvc_map_01011][switch.robot_vacuum_sound_detection-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Robot Vacuum Sound detection',
}),
'context': <ANY>,
'entity_id': 'switch.robot_vacuum_sound_detection',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[da_rvc_normal_000001][switch.robot_vacuum-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -4,7 +4,14 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'fan_speed_list': list([
'normal',
'maximum',
'smart',
'quiet',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -29,8 +36,8 @@
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <VacuumEntityFeature: 12308>,
'translation_key': None,
'supported_features': <VacuumEntityFeature: 12340>,
'translation_key': 'vacuum',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main',
'unit_of_measurement': None,
})
@@ -38,8 +45,15 @@
# name: test_all_entities[da_rvc_map_01011][vacuum.robot_vacuum-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'fan_speed': 'maximum',
'fan_speed_list': list([
'normal',
'maximum',
'smart',
'quiet',
]),
'friendly_name': 'Robot Vacuum',
'supported_features': <VacuumEntityFeature: 12308>,
'supported_features': <VacuumEntityFeature: 12340>,
}),
'context': <ANY>,
'entity_id': 'vacuum.robot_vacuum',
@@ -54,7 +68,14 @@
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'capabilities': dict({
'fan_speed_list': list([
'normal',
'maximum',
'smart',
'quiet',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
@@ -79,8 +100,8 @@
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <VacuumEntityFeature: 12308>,
'translation_key': None,
'supported_features': <VacuumEntityFeature: 12340>,
'translation_key': 'vacuum',
'unique_id': '3442dfc6-17c0-a65f-dae0-4c6e01786f44_main',
'unit_of_measurement': None,
})
@@ -88,8 +109,15 @@
# name: test_all_entities[da_rvc_normal_000001][vacuum.robot_vacuum-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'fan_speed': 'smart',
'fan_speed_list': list([
'normal',
'maximum',
'smart',
'quiet',
]),
'friendly_name': 'Robot vacuum',
'supported_features': <VacuumEntityFeature: 12308>,
'supported_features': <VacuumEntityFeature: 12340>,
}),
'context': <ANY>,
'entity_id': 'vacuum.robot_vacuum',

View File

@@ -9,9 +9,11 @@ from syrupy.assertion import SnapshotAssertion
from homeassistant.components.smartthings import MAIN
from homeassistant.components.vacuum import (
ATTR_FAN_SPEED,
DOMAIN as VACUUM_DOMAIN,
SERVICE_PAUSE,
SERVICE_RETURN_TO_BASE,
SERVICE_SET_FAN_SPEED,
SERVICE_START,
VacuumActivity,
)
@@ -131,3 +133,52 @@ async def test_availability_at_start(
"""Test unavailable at boot."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("vacuum.robot_vacuum").state == STATE_UNAVAILABLE
@pytest.mark.parametrize("device_fixture", ["da_rvc_map_01011"])
async def test_fan_speed_update(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test fan speed state update."""
await setup_integration(hass, mock_config_entry)
assert (
hass.states.get("vacuum.robot_vacuum").attributes[ATTR_FAN_SPEED] == "maximum"
)
await trigger_update(
hass,
devices,
"01b28624-5907-c8bc-0325-8ad23f03a637",
Capability.ROBOT_CLEANER_TURBO_MODE,
Attribute.ROBOT_CLEANER_TURBO_MODE,
"extraSilence",
)
assert hass.states.get("vacuum.robot_vacuum").attributes[ATTR_FAN_SPEED] == "quiet"
@pytest.mark.parametrize("device_fixture", ["da_rvc_map_01011"])
async def test_vacuum_set_fan_speed(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test setting fan speed."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
VACUUM_DOMAIN,
SERVICE_SET_FAN_SPEED,
{ATTR_ENTITY_ID: "vacuum.robot_vacuum", ATTR_FAN_SPEED: "normal"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"01b28624-5907-c8bc-0325-8ad23f03a637",
Capability.ROBOT_CLEANER_TURBO_MODE,
Command.SET_ROBOT_CLEANER_TURBO_MODE,
MAIN,
"silence",
)