Compare commits

...

10 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
abmantis
9823e31206 Add Ubisys virtual integration 2026-02-27 10:58:27 +00:00
18 changed files with 216 additions and 27 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,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

@@ -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

@@ -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",

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": {

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

@@ -7338,6 +7338,12 @@
}
}
},
"ubisys": {
"name": "Ubisys",
"iot_standards": [
"zigbee"
]
},
"ubiwizz": {
"name": "Ubiwizz",
"integration_type": "virtual",

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

@@ -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({