Compare commits

..

2 Commits

Author SHA1 Message Date
Robert Resch
aae63cb397 Adjust docker file to be multistage 2026-03-27 17:32:58 +01:00
Robert Resch
78aa5b9913 Improve build pipeline by triggering translation download async 2026-03-27 17:01:58 +01:00
192 changed files with 1861 additions and 6216 deletions

View File

@@ -14,7 +14,7 @@ env:
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.02.0"
BASE_IMAGE_VERSION: "2026.01.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}
@@ -35,6 +35,7 @@ jobs:
channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }}
architectures: ${{ env.ARCHITECTURES }}
translation_download_process: ${{ steps.translations.outputs.translation_download_process }}
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
steps:
- name: Checkout the repository
@@ -47,6 +48,26 @@ jobs:
with:
python-version-file: ".python-version"
- name: Fail if translations files are checked in
run: |
if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then
echo "Translations files are checked in, please remove the following files:"
find homeassistant/components/*/translations -type f
exit 1
fi
- name: Start translation download
id: translations
shell: bash
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
run: |
pip install -r script/translations/requirements.txt
PROCESS_OUTPUT=$(python3 -m script.translations download --async-start)
echo "$PROCESS_OUTPUT"
TRANSLATION_DOWNLOAD_PROCESS=$(echo "$PROCESS_OUTPUT" | sed -n 's/.*Process ID: //p')
echo "translation_download_process=$TRANSLATION_DOWNLOAD_PROCESS" >> "$GITHUB_OUTPUT"
- name: Get information
id: info
uses: home-assistant/actions/helpers/info@master # zizmor: ignore[unpinned-uses]
@@ -62,30 +83,6 @@ jobs:
with:
ignore-dev: true
- name: Fail if translations files are checked in
run: |
if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then
echo "Translations files are checked in, please remove the following files:"
find homeassistant/components/*/translations -type f
exit 1
fi
- name: Download Translations
run: python3 -m script.translations download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
- name: Archive translations
shell: bash
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: translations
path: translations.tar.gz
if-no-files-found: error
build_base:
name: Build ${{ matrix.arch }} base core image
if: github.repository_owner == 'home-assistant'
@@ -133,7 +130,6 @@ jobs:
name: package
- name: Set up Python
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version-file: ".python-version"
@@ -181,22 +177,35 @@ jobs:
sed -i "s|home-assistant-intents==.*||" requirements_all.txt requirements.txt
fi
- name: Download translations
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: translations
- name: Extract translations
run: |
tar xvf translations.tar.gz
rm translations.tar.gz
- name: Write meta info file
shell: bash
run: |
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
- name: Build base image
- name: Build base image (deps stage only)
uses: edenhaus/builder/actions/build-image@b3ea9d4b1d98f979d671aa8442ad8373e38aea11
with:
arch: ${{ matrix.arch }}
build-args: |
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
cosign-base-identity: "https://github.com/home-assistant/docker/.*"
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
push: false
load: true
target: deps
- name: Download translations
shell: bash
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
TRANSLATION_PROCESS_ID: ${{ needs.init.outputs.translation_download_process }}
run: |
pip install -r script/translations/requirements.txt
python3 -m script.translations download --process-id "$TRANSLATION_PROCESS_ID"
- name: Build base image (fully)
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
with:
arch: ${{ matrix.arch }}
@@ -485,14 +494,13 @@ jobs:
python-version-file: ".python-version"
- name: Download translations
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: translations
- name: Extract translations
shell: bash
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
TRANSLATION_PROCESS_ID: ${{ needs.init.outputs.translation_download_process }}
run: |
tar xvf translations.tar.gz
rm translations.tar.gz
pip install -r script/translations/requirements.txt
python3 -m script.translations download --process-id "$TRANSLATION_PROCESS_ID"
- name: Build package
shell: bash

View File

@@ -1 +1 @@
3.14.3
3.14.2

4
CODEOWNERS generated
View File

@@ -1228,8 +1228,8 @@ build.json @home-assistant/supervisor
/tests/components/onewire/ @garbled1 @epenet
/homeassistant/components/onkyo/ @arturpragacz @eclair4151
/tests/components/onkyo/ @arturpragacz @eclair4151
/homeassistant/components/onvif/ @jterrace
/tests/components/onvif/ @jterrace
/homeassistant/components/onvif/ @hunterjm @jterrace
/tests/components/onvif/ @hunterjm @jterrace
/homeassistant/components/open_meteo/ @frenck
/tests/components/open_meteo/ @frenck
/homeassistant/components/open_router/ @joostlek

4
Dockerfile generated
View File

@@ -2,7 +2,7 @@
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM
FROM ${BUILD_FROM}
FROM ${BUILD_FROM} as deps
LABEL \
io.hass.type="core" \
@@ -50,6 +50,8 @@ RUN \
--no-build \
-r homeassistant/requirements_all.txt
FROM deps
## Setup Home Assistant Core
COPY . homeassistant/
RUN \

View File

@@ -45,21 +45,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) ->
try:
await client.models.list(timeout=10.0)
except anthropic.AuthenticationError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="api_authentication_error",
translation_placeholders={"message": err.message},
) from err
raise ConfigEntryAuthFailed(err) from err
except anthropic.AnthropicError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={
"message": err.message
if isinstance(err, anthropic.APIError)
else str(err)
},
) from err
raise ConfigEntryNotReady(err) from err
entry.runtime_data = client

View File

@@ -12,7 +12,6 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.json import json_loads
from .const import DOMAIN
from .entity import AnthropicBaseLLMEntity
if TYPE_CHECKING:
@@ -61,7 +60,7 @@ class AnthropicTaskEntity(
if not isinstance(chat_log.content[-1], conversation.AssistantContent):
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="response_not_found"
"Last content in chat log is not an AssistantContent"
)
text = chat_log.content[-1].content or ""
@@ -79,9 +78,7 @@ class AnthropicTaskEntity(
err,
text,
)
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="json_parse_error"
) from err
raise HomeAssistantError("Error with Claude structured response") from err
return ai_task.GenDataTaskResult(
conversation_id=chat_log.conversation_id,

View File

@@ -401,11 +401,7 @@ def _convert_content(
messages[-1]["content"] = messages[-1]["content"][0]["text"]
else:
# Note: We don't pass SystemContent here as it's passed to the API as the prompt
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="unexpected_chat_log_content",
translation_placeholders={"type": type(content).__name__},
)
raise HomeAssistantError("Unexpected content type in chat log")
return messages, container_id
@@ -447,9 +443,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
Each message could contain multiple blocks of the same type.
"""
if stream is None or not hasattr(stream, "__aiter__"):
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="unexpected_stream_object"
)
raise HomeAssistantError("Expected a stream of messages")
current_tool_block: ToolUseBlockParam | ServerToolUseBlockParam | None = None
current_tool_args: str
@@ -611,9 +605,7 @@ async def _transform_stream( # noqa: C901 - This is complex, but better to have
chat_log.async_trace(_create_token_stats(input_usage, usage))
content_details.container = response.delta.container
if response.delta.stop_reason == "refusal":
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="api_refusal"
)
raise HomeAssistantError("Potential policy violation detected")
elif isinstance(response, RawMessageStopEvent):
if content_details:
content_details.delete_empty()
@@ -672,9 +664,7 @@ class AnthropicBaseLLMEntity(Entity):
system = chat_log.content[0]
if not isinstance(system, conversation.SystemContent):
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="system_message_not_found"
)
raise HomeAssistantError("First message must be a system message")
# System prompt with caching enabled
system_prompt: list[TextBlockParam] = [
@@ -764,7 +754,7 @@ class AnthropicBaseLLMEntity(Entity):
last_message = messages[-1]
if last_message["role"] != "user":
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="user_message_not_found"
"Last message must be a user message to add attachments"
)
if isinstance(last_message["content"], str):
last_message["content"] = [
@@ -869,19 +859,11 @@ class AnthropicBaseLLMEntity(Entity):
except anthropic.AuthenticationError as err:
self.entry.async_start_reauth(self.hass)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_authentication_error",
translation_placeholders={"message": err.message},
"Authentication error with Anthropic API, reauthentication required"
) from err
except anthropic.AnthropicError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_error",
translation_placeholders={
"message": err.message
if isinstance(err, anthropic.APIError)
else str(err)
},
f"Sorry, I had a problem talking to Anthropic: {err}"
) from err
if not chat_log.unresponded_tool_results:
@@ -901,23 +883,15 @@ async def async_prepare_files_for_prompt(
for file_path, mime_type in files:
if not file_path.exists():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="wrong_file_path",
translation_placeholders={"file_path": file_path.as_posix()},
)
raise HomeAssistantError(f"`{file_path}` does not exist")
if mime_type is None:
mime_type = guess_file_type(file_path)[0]
if not mime_type or not mime_type.startswith(("image/", "application/pdf")):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="wrong_file_type",
translation_placeholders={
"file_path": file_path.as_posix(),
"mime_type": mime_type or "unknown",
},
"Only images and PDF are supported by the Anthropic API,"
f"`{file_path}` is not an image file or PDF"
)
if mime_type == "image/jpg":
mime_type = "image/jpeg"

View File

@@ -59,7 +59,10 @@ rules:
status: exempt
comment: |
No data updates.
docs-examples: done
docs-examples:
status: todo
comment: |
To give examples of how people use the integration
docs-known-limitations: done
docs-supported-devices:
status: todo
@@ -85,7 +88,7 @@ rules:
comment: |
No entities disabled by default.
entity-translations: todo
exception-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: done
repair-issues: done

View File

@@ -161,9 +161,7 @@ class ModelDeprecatedRepairFlow(RepairsFlow):
is None
or (subentry := entry.subentries.get(self._current_subentry_id)) is None
):
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="subentry_not_found"
)
raise HomeAssistantError("Subentry not found")
updated_data = {
**subentry.data,
@@ -192,6 +190,4 @@ async def async_create_fix_flow(
"""Create flow."""
if issue_id == "model_deprecated":
return ModelDeprecatedRepairFlow()
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="unknown_issue_id"
)
raise HomeAssistantError("Unknown issue ID")

View File

@@ -149,47 +149,6 @@
}
}
},
"exceptions": {
"api_authentication_error": {
"message": "Authentication error with Anthropic API: {message}. Reauthentication required."
},
"api_error": {
"message": "Anthropic API error: {message}."
},
"api_refusal": {
"message": "Potential policy violation detected."
},
"json_parse_error": {
"message": "Error with Claude structured response."
},
"response_not_found": {
"message": "Last content in chat log is not an AssistantContent."
},
"subentry_not_found": {
"message": "Subentry not found."
},
"system_message_not_found": {
"message": "First message must be a system message."
},
"unexpected_chat_log_content": {
"message": "Unexpected content type in chat log: {type}."
},
"unexpected_stream_object": {
"message": "Expected a stream of messages."
},
"unknown_issue_id": {
"message": "Unknown issue ID."
},
"user_message_not_found": {
"message": "Last message must be a user message to add attachments."
},
"wrong_file_path": {
"message": "`{file_path}` does not exist."
},
"wrong_file_type": {
"message": "Only images and PDF are supported by the Anthropic API, `{file_path}` ({mime_type}) is not an image file or PDF."
}
},
"issues": {
"model_deprecated": {
"fix_flow": {

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/bluesound",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["pyblu==2.0.6"],
"requirements": ["pyblu==2.0.5"],
"zeroconf": [
{
"type": "_musc._tcp.local."

View File

@@ -11,12 +11,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.LIGHT,
Platform.SELECT,
]
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:

View File

@@ -12,7 +12,5 @@ SORTED_BRIGHTNESS_LEVELS = sorted(BRIGHTNESS_LEVELS)
DEFAULT_DIMMING_TIME_MINUTES: int = DIMMING_TIME_MINUTES[0]
DIMMING_TIME_OPTIONS: tuple[str, ...] = tuple(str(m) for m in DIMMING_TIME_MINUTES)
# Interval between periodic state polls to catch externally-triggered changes.
STATE_POLL_INTERVAL = timedelta(seconds=30)

View File

@@ -19,7 +19,7 @@ from homeassistant.components.bluetooth.active_update_coordinator import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from .const import SORTED_BRIGHTNESS_LEVELS, STATE_POLL_INTERVAL
from .const import STATE_POLL_INTERVAL
_LOGGER = logging.getLogger(__name__)
@@ -51,15 +51,6 @@ class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
)
self.title = title
# The device API couples brightness and dimming time into a
# single command (set_brightness_and_dimming_time), so both
# values must be tracked here for cross-entity use.
self.last_brightness_pct: int = (
device.state.brightness_level
if device.state.brightness_level is not None
else SORTED_BRIGHTNESS_LEVELS[0]
)
@callback
def _needs_poll(
self,

View File

@@ -12,11 +12,6 @@
"resume": {
"default": "mdi:play"
}
},
"select": {
"dimming_time": {
"default": "mdi:timer-outline"
}
}
}
}

View File

@@ -71,7 +71,6 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
self._attr_color_mode = ColorMode.BRIGHTNESS
if state.brightness_level is not None:
self._attr_brightness = _device_pct_to_ha_brightness(state.brightness_level)
self.coordinator.last_brightness_pct = state.brightness_level
@callback
def _async_handle_state_update(self, state: GlowState) -> None:
@@ -98,7 +97,6 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
)
)
self._attr_brightness = _device_pct_to_ha_brightness(brightness_pct)
self.coordinator.last_brightness_pct = brightness_pct
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the light off."""

View File

@@ -52,10 +52,8 @@ rules:
docs-troubleshooting: done
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class:
status: exempt
comment: No applicable device classes for binary_sensor, button, light, or select entities.
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: done
exception-translations: done

View File

@@ -1,92 +0,0 @@
"""Casper Glow integration select platform for dimming time."""
from __future__ import annotations
from pycasperglow import GlowState
from homeassistant.components.select import SelectEntity
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from .const import DIMMING_TIME_OPTIONS
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
from .entity import CasperGlowEntity
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
entry: CasperGlowConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the select platform for Casper Glow."""
async_add_entities([CasperGlowDimmingTimeSelect(entry.runtime_data)])
class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity):
"""Select entity for Casper Glow dimming time."""
_attr_translation_key = "dimming_time"
_attr_entity_category = EntityCategory.CONFIG
_attr_options = list(DIMMING_TIME_OPTIONS)
_attr_unit_of_measurement = UnitOfTime.MINUTES
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
"""Initialize the dimming time select entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_dimming_time"
@property
def current_option(self) -> str | None:
"""Return the currently selected dimming time from the coordinator."""
if self.coordinator.last_dimming_time_minutes is None:
return None
return str(self.coordinator.last_dimming_time_minutes)
async def async_added_to_hass(self) -> None:
"""Restore last known dimming time and register state update callback."""
await super().async_added_to_hass()
if self.coordinator.last_dimming_time_minutes is None and (
last_state := await self.async_get_last_state()
):
if last_state.state in DIMMING_TIME_OPTIONS:
self.coordinator.last_dimming_time_minutes = int(last_state.state)
self.async_on_remove(
self._device.register_callback(self._async_handle_state_update)
)
@callback
def _async_handle_state_update(self, state: GlowState) -> None:
"""Handle a state update from the device."""
if state.brightness_level is not None:
self.coordinator.last_brightness_pct = state.brightness_level
if (
state.configured_dimming_time_minutes is not None
and self.coordinator.last_dimming_time_minutes is None
):
self.coordinator.last_dimming_time_minutes = (
state.configured_dimming_time_minutes
)
# Dimming time is not part of the device state
# that is provided via BLE update. Therefore
# we need to trigger a state update for the select entity
# to update the current state.
self.async_write_ha_state()
async def async_select_option(self, option: str) -> None:
"""Set the dimming time."""
await self._async_command(
self._device.set_brightness_and_dimming_time(
self.coordinator.last_brightness_pct, int(option)
)
)
self.coordinator.last_dimming_time_minutes = int(option)
# Dimming time is not part of the device state
# that is provided via BLE update. Therefore
# we need to trigger a state update for the select entity
# to update the current state.
self.async_write_ha_state()

View File

@@ -39,11 +39,6 @@
"resume": {
"name": "Resume dimming"
}
},
"select": {
"dimming_time": {
"name": "Dimming time"
}
}
},
"exceptions": {

View File

@@ -9,6 +9,7 @@ from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import CLIMATE
from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
@@ -91,7 +92,7 @@ async def async_setup_entry(
entities: list[ClimateEntity] = []
for device in coordinator.data[CLIMATE].values():
values = load_api_data(device, "climate")
values = load_api_data(device, CLIMATE_DOMAIN)
if values[0] == 0 and values[4] == 0:
# No climate data, device is only a humidifier/dehumidifier
@@ -139,7 +140,7 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
def _update_attributes(self) -> None:
"""Update class attributes."""
device = self.coordinator.data[CLIMATE][self._device.index]
values = load_api_data(device, "climate")
values = load_api_data(device, CLIMATE_DOMAIN)
_active = values[1]
_mode = values[2] # Values from API: "O", "L", "U"

View File

@@ -9,6 +9,7 @@ from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import CLIMATE
from homeassistant.components.humidifier import (
DOMAIN as HUMIDIFIER_DOMAIN,
MODE_AUTO,
MODE_NORMAL,
HumidifierAction,
@@ -67,7 +68,7 @@ async def async_setup_entry(
entities: list[ComelitHumidifierEntity] = []
for device in coordinator.data[CLIMATE].values():
values = load_api_data(device, "humidifier")
values = load_api_data(device, HUMIDIFIER_DOMAIN)
if values[0] == 0 and values[4] == 0:
# No humidity data, device is only a climate
@@ -141,7 +142,7 @@ class ComelitHumidifierEntity(ComelitBridgeBaseEntity, HumidifierEntity):
def _update_attributes(self) -> None:
"""Update class attributes."""
device = self.coordinator.data[CLIMATE][self._device.index]
values = load_api_data(device, "humidifier")
values = load_api_data(device, HUMIDIFIER_DOMAIN)
_active = values[1]
_mode = values[2] # Values from API: "O", "L", "U"

View File

@@ -113,6 +113,9 @@
"humidity_while_off": {
"message": "Cannot change humidity while off"
},
"invalid_clima_data": {
"message": "Invalid 'clima' data"
},
"update_failed": {
"message": "Failed to update data: {error}"
}

View File

@@ -2,12 +2,13 @@
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import TYPE_CHECKING, Any, Concatenate, Literal
from typing import TYPE_CHECKING, Any, Concatenate
from aiocomelit.api import ComelitSerialBridgeObject
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiohttp import ClientSession, CookieJar
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
@@ -29,19 +30,17 @@ async def async_client_session(hass: HomeAssistant) -> ClientSession:
)
def load_api_data(
device: ComelitSerialBridgeObject,
domain: Literal["climate", "humidifier"],
) -> list[Any]:
def load_api_data(device: ComelitSerialBridgeObject, domain: str) -> list[Any]:
"""Load data from the API."""
# This function is called when the data is loaded from the API.
# For climate and humidifier device.val is always a list.
if TYPE_CHECKING:
assert isinstance(device.val, list)
# This function is called when the data is loaded from the API
if not isinstance(device.val, list):
raise HomeAssistantError(
translation_domain=domain, translation_key="invalid_clima_data"
)
# CLIMATE has a 2 item tuple:
# - first for Clima
# - second for Humidifier
return device.val[0] if domain == "climate" else device.val[1]
return device.val[0] if domain == CLIMATE_DOMAIN else device.val[1]
async def cleanup_stale_entity(

View File

@@ -51,6 +51,7 @@ def _entity_entry_filter(a: attr.Attribute, _: Any) -> bool:
return a.name not in (
"_cache",
"compat_aliases",
"compat_name",
"original_name_unprefixed",
)

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260325.2"]
"requirements": ["home-assistant-frontend==20260325.1"]
}

View File

@@ -15,5 +15,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
"requirements": ["gardena-bluetooth==2.3.0"]
"requirements": ["gardena-bluetooth==2.1.0"]
}

View File

@@ -7,7 +7,7 @@ from homelink.mqtt_provider import MQTTProvider
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from . import oauth2
@@ -29,17 +29,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeLinkConfigEntry) ->
hass, DOMAIN, auth_implementation
)
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
authenticated_session = oauth2.AsyncConfigEntryAuth(

View File

@@ -49,10 +49,5 @@
}
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -2,14 +2,11 @@
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
from .const import DOMAIN
from .coordinator import GeocachingConfigEntry, GeocachingDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
@@ -17,13 +14,7 @@ PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: GeocachingConfigEntry) -> bool:
"""Set up Geocaching from a config entry."""
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
implementation = await async_get_config_entry_implementation(hass, entry)
oauth_session = OAuth2Session(hass, entry, implementation)
coordinator = GeocachingDataUpdateCoordinator(

View File

@@ -65,10 +65,5 @@
"unit_of_measurement": "souvenirs"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["goodwe"],
"requirements": ["goodwe==0.4.10"]
"requirements": ["goodwe==0.4.8"]
}

View File

@@ -24,9 +24,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from homeassistant.helpers.entity import generate_entity_id
from .api import ApiAuthImpl, get_feature_access
@@ -91,17 +88,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleConfigEntry) -> bo
_LOGGER.error("Configuration error in %s: %s", YAML_DEVICES, str(err))
return False
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
# Force a token refresh to fix a bug where tokens were persisted with
# expires_in (relative time delta) and expires_at (absolute time) swapped.

View File

@@ -57,11 +57,6 @@
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"options": {
"step": {
"init": {

View File

@@ -17,7 +17,6 @@ from homeassistant.exceptions import (
)
from homeassistant.helpers import config_validation as cv, discovery, intent
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
@@ -53,13 +52,7 @@ async def async_setup_entry(
hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry
) -> bool:
"""Set up Google Assistant SDK from a config entry."""
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()

View File

@@ -48,9 +48,6 @@
"grpc_error": {
"message": "Failed to communicate with Google Assistant"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
},
"reauth_required": {
"message": "Credentials are invalid, re-authentication required"
}

View File

@@ -5,10 +5,8 @@ from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
@@ -36,13 +34,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -> bool:
"""Set up Google Mail from a config entry."""
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
auth = AsyncConfigEntryAuth(hass, session)
await auth.check_and_refresh_token()

View File

@@ -51,9 +51,6 @@
"exceptions": {
"missing_from_for_alias": {
"message": "Missing 'from' email when setting an alias to show. You have to provide a 'from' email"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"services": {

View File

@@ -15,7 +15,6 @@ from homeassistant.exceptions import (
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
@@ -41,13 +40,7 @@ async def async_setup_entry(
hass: HomeAssistant, entry: GoogleSheetsConfigEntry
) -> bool:
"""Set up Google Sheets from a config entry."""
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()

View File

@@ -42,11 +42,6 @@
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"services": {
"append_sheet": {
"description": "Appends data to a worksheet in Google Sheets.",

View File

@@ -25,17 +25,11 @@ PLATFORMS: list[Platform] = [Platform.TODO]
async def async_setup_entry(hass: HomeAssistant, entry: GoogleTasksConfigEntry) -> bool:
"""Set up Google Tasks from a config entry."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
auth = api.AsyncConfigEntryAuth(hass, session)
try:

View File

@@ -42,10 +42,5 @@
"title": "[%key:common::config_flow::title::reauth%]"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["habiticalib"],
"quality_scale": "platinum",
"requirements": ["habiticalib==0.4.7"]
"requirements": ["habiticalib==0.4.6"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["python_qube_heatpump"],
"quality_scale": "bronze",
"requirements": ["python-qube-heatpump==1.8.0"]
"requirements": ["python-qube-heatpump==1.7.0"]
}

View File

@@ -9,7 +9,7 @@ from .const import DOMAIN
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [Platform.EVENT, Platform.NOTIFY]
PLATFORMS = [Platform.NOTIFY]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -7,9 +7,3 @@ SERVICE_DISMISS = "dismiss"
ATTR_VAPID_PUB_KEY = "vapid_pub_key"
ATTR_VAPID_PRV_KEY = "vapid_prv_key"
ATTR_VAPID_EMAIL = "vapid_email"
REGISTRATIONS_FILE = "html5_push_registrations.conf"
ATTR_ACTION = "action"
ATTR_DATA = "data"
ATTR_TAG = "tag"

View File

@@ -1,73 +0,0 @@
"""Base entities for HTML5 integration."""
from __future__ import annotations
from typing import NotRequired, TypedDict
from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
class Keys(TypedDict):
"""Types for keys."""
p256dh: str
auth: str
class Subscription(TypedDict):
"""Types for subscription."""
endpoint: str
expirationTime: int | None
keys: Keys
class Registration(TypedDict):
"""Types for registration."""
subscription: Subscription
browser: str
name: NotRequired[str]
class HTML5Entity(Entity):
"""Base entity for HTML5 integration."""
_attr_has_entity_name = True
_attr_name = None
_key: str
def __init__(
self,
config_entry: ConfigEntry,
target: str,
registrations: dict[str, Registration],
session: ClientSession,
json_path: str,
) -> None:
"""Initialize the entity."""
self.config_entry = config_entry
self.target = target
self.registrations = registrations
self.registration = registrations[target]
self.session = session
self.json_path = json_path
self._attr_unique_id = f"{config_entry.entry_id}_{target}_{self._key}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
name=target,
model=self.registration["browser"].capitalize(),
identifiers={(DOMAIN, f"{config_entry.entry_id}_{target}")},
)
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self.target in self.registrations

View File

@@ -1,67 +0,0 @@
"""Event platform for HTML5 integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.event import EventEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import ATTR_ACTION, ATTR_DATA, ATTR_TAG, DOMAIN, REGISTRATIONS_FILE
from .entity import HTML5Entity
from .notify import _load_config
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the event entity platform."""
json_path = hass.config.path(REGISTRATIONS_FILE)
registrations = await hass.async_add_executor_job(_load_config, json_path)
session = async_get_clientsession(hass)
async_add_entities(
HTML5EventEntity(config_entry, target, registrations, session, json_path)
for target in registrations
)
class HTML5EventEntity(HTML5Entity, EventEntity):
"""Representation of an event entity."""
_key = "event"
_attr_event_types = ["clicked", "received", "closed"]
_attr_translation_key = "event"
@callback
def _async_handle_event(
self, target: str, event_type: str, event_data: dict[str, Any]
) -> None:
"""Handle the event."""
if target == self.target:
self._trigger_event(
event_type,
{
**event_data.get(ATTR_DATA, {}),
ATTR_ACTION: event_data.get(ATTR_ACTION),
ATTR_TAG: event_data.get(ATTR_TAG),
},
)
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register event callback."""
self.async_on_remove(
async_dispatcher_connect(self.hass, DOMAIN, self._async_handle_event)
)

View File

@@ -1,11 +1,4 @@
{
"entity": {
"event": {
"event": {
"default": "mdi:gesture-tap-button"
}
}
},
"services": {
"dismiss": {
"service": "mdi:bell-off"

View File

@@ -8,7 +8,7 @@ from http import HTTPStatus
import json
import logging
import time
from typing import TYPE_CHECKING, Any, cast
from typing import TYPE_CHECKING, Any, NotRequired, TypedDict, cast
from urllib.parse import urlparse
import uuid
@@ -38,7 +38,7 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.json import save_json
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -46,19 +46,17 @@ from homeassistant.util import ensure_unique_string
from homeassistant.util.json import load_json_object
from .const import (
ATTR_ACTION,
ATTR_TAG,
ATTR_VAPID_EMAIL,
ATTR_VAPID_PRV_KEY,
ATTR_VAPID_PUB_KEY,
DOMAIN,
REGISTRATIONS_FILE,
SERVICE_DISMISS,
)
from .entity import HTML5Entity, Registration
_LOGGER = logging.getLogger(__name__)
REGISTRATIONS_FILE = "html5_push_registrations.conf"
ATTR_SUBSCRIPTION = "subscription"
ATTR_BROWSER = "browser"
@@ -69,6 +67,8 @@ ATTR_AUTH = "auth"
ATTR_P256DH = "p256dh"
ATTR_EXPIRATIONTIME = "expirationTime"
ATTR_TAG = "tag"
ATTR_ACTION = "action"
ATTR_ACTIONS = "actions"
ATTR_TYPE = "type"
ATTR_URL = "url"
@@ -156,6 +156,29 @@ HTML5_SHOWNOTIFICATION_PARAMETERS = (
)
class Keys(TypedDict):
"""Types for keys."""
p256dh: str
auth: str
class Subscription(TypedDict):
"""Types for subscription."""
endpoint: str
expirationTime: int | None
keys: Keys
class Registration(TypedDict):
"""Types for registration."""
subscription: Subscription
browser: str
name: NotRequired[str]
async def async_get_service(
hass: HomeAssistant,
config: ConfigType,
@@ -396,15 +419,7 @@ class HTML5PushCallbackView(HomeAssistantView):
)
event_name = f"{NOTIFY_CALLBACK_EVENT}.{event_payload[ATTR_TYPE]}"
hass = request.app[KEY_HASS]
hass.bus.fire(event_name, event_payload)
async_dispatcher_send(
hass,
DOMAIN,
event_payload[ATTR_TARGET],
event_payload[ATTR_TYPE],
event_payload,
)
request.app[KEY_HASS].bus.fire(event_name, event_payload)
return self.json({"status": "ok", "event": event_payload[ATTR_TYPE]})
@@ -598,11 +613,37 @@ async def async_setup_entry(
)
class HTML5NotifyEntity(HTML5Entity, NotifyEntity):
class HTML5NotifyEntity(NotifyEntity):
"""Representation of a notification entity."""
_attr_has_entity_name = True
_attr_name = None
_attr_supported_features = NotifyEntityFeature.TITLE
_key = "device"
def __init__(
self,
config_entry: ConfigEntry,
target: str,
registrations: dict[str, Registration],
session: ClientSession,
json_path: str,
) -> None:
"""Initialize the entity."""
self.config_entry = config_entry
self.target = target
self.registrations = registrations
self.registration = registrations[target]
self.session = session
self.json_path = json_path
self._attr_unique_id = f"{config_entry.entry_id}_{target}_device"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
name=target,
model=self.registration["browser"].capitalize(),
identifiers={(DOMAIN, f"{config_entry.entry_id}_{target}")},
)
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message to a device."""
@@ -673,3 +714,8 @@ class HTML5NotifyEntity(HTML5Entity, NotifyEntity):
translation_key="connection_error",
translation_placeholders={"target": self.target},
) from e
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available and self.target in self.registrations

View File

@@ -20,23 +20,6 @@
}
}
},
"entity": {
"event": {
"event": {
"state_attributes": {
"action": { "name": "Action" },
"event_type": {
"state": {
"clicked": "Clicked",
"closed": "Closed",
"received": "Received"
}
},
"tag": { "name": "Tag" }
}
}
}
},
"exceptions": {
"channel_expired": {
"message": "Notification channel for {target} has expired"
@@ -48,6 +31,12 @@
"message": "Sending notification to {target} failed due to a request error"
}
},
"issues": {
"deprecated_yaml_import_issue": {
"description": "Configuring HTML5 push notification using YAML has been deprecated. An automatic import of your existing configuration was attempted, but it failed.\n\nPlease remove the HTML5 push notification YAML configuration from your configuration.yaml file and reconfigure HTML5 push notification again manually.",
"title": "HTML5 YAML configuration import failed"
}
},
"services": {
"dismiss": {
"description": "Dismisses an HTML5 notification.",

View File

@@ -11,9 +11,6 @@ from homeassistant.helpers import (
config_entry_oauth2_flow,
config_validation as cv,
)
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
)
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
@@ -45,17 +42,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: AutomowerConfigEntry) -> bool:
"""Set up this integration using UI."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
api_api = api.AsyncConfigEntryAuth(
aiohttp_client.async_get_clientsession(hass),

View File

@@ -491,9 +491,6 @@
"command_send_failed": {
"message": "Failed to send command: {exception}"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
},
"work_area_not_existing": {
"message": "The selected work area does not exist."
},

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["automower-ble==0.2.8", "gardena-bluetooth==2.3.0"]
"requirements": ["automower-ble==0.2.8", "gardena-bluetooth==2.1.0"]
}

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/huum",
"integration_type": "device",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"quality_scale": "bronze",
"requirements": ["huum==0.8.2"]
}

View File

@@ -39,7 +39,7 @@ rules:
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage: done
test-coverage: todo
# Gold
devices: done

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2026.3.0"]
"requirements": ["pydrawise==2025.9.0"]
}

View File

@@ -9,5 +9,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "platinum",
"requirements": ["aioimmich==0.12.1"]
"requirements": ["aioimmich==0.12.0"]
}

View File

@@ -6,14 +6,11 @@ import logging
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
from .const import DOMAIN
from .coordinator import (
IottyConfigEntry,
IottyConfigEntryData,
@@ -29,13 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: IottyConfigEntry) -> boo
"""Set up iotty from a config entry."""
_LOGGER.debug("async_setup_entry entry_id=%s", entry.entry_id)
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)
data_update_coordinator = IottyDataUpdateCoordinator(hass, entry, session)

View File

@@ -25,10 +25,5 @@
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/kaleidescape",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["pykaleidescape==1.1.4"],
"requirements": ["pykaleidescape==1.1.3"],
"ssdp": [
{
"deviceType": "schemas-upnp-org:device:Basic:1",

View File

@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.15.0",
"xknxproject==3.8.2",
"knx-frontend==2026.3.28.223133"
"knx-frontend==2026.3.2.183756"
],
"single_config_entry": true
}

View File

@@ -6,7 +6,6 @@ from aiolyric import Lyric
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
@@ -28,17 +27,11 @@ PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: LyricConfigEntry) -> bool:
"""Set up Honeywell Lyric from a config entry."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
if not isinstance(implementation, LyricLocalOAuth2Implementation):
raise TypeError("Unexpected auth implementation; can't find oauth client id")

View File

@@ -64,11 +64,6 @@
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"services": {
"set_hold_time": {
"description": "Sets the time period to keep the temperature and override the schedule.",

View File

@@ -16,10 +16,6 @@ from homeassistant.components.climate import (
ATTR_TARGET_TEMP_LOW,
DEFAULT_MAX_TEMP,
DEFAULT_MIN_TEMP,
PRESET_AWAY,
PRESET_HOME,
PRESET_NONE,
PRESET_SLEEP,
ClimateEntity,
ClimateEntityDescription,
ClimateEntityFeature,
@@ -46,18 +42,6 @@ HVAC_SYSTEM_MODE_MAP = {
HVACMode.FAN_ONLY: 7,
}
# Map of Matter PresetScenarioEnum to HA standard preset constants or custom names
# This ensures presets are translated correctly using HA's translation system.
# kUserDefined scenarios always use device-provided names.
PRESET_SCENARIO_TO_HA_PRESET: dict[int, str] = {
clusters.Thermostat.Enums.PresetScenarioEnum.kOccupied: PRESET_HOME,
clusters.Thermostat.Enums.PresetScenarioEnum.kUnoccupied: PRESET_AWAY,
clusters.Thermostat.Enums.PresetScenarioEnum.kSleep: PRESET_SLEEP,
clusters.Thermostat.Enums.PresetScenarioEnum.kWake: "wake",
clusters.Thermostat.Enums.PresetScenarioEnum.kVacation: "vacation",
clusters.Thermostat.Enums.PresetScenarioEnum.kGoingToSleep: "going_to_sleep",
}
SINGLE_SETPOINT_DEVICES: set[tuple[int, int]] = {
# Some devices only have a single setpoint while the matter spec
# assumes that you need separate setpoints for heating and cooling.
@@ -175,6 +159,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
}
SystemModeEnum = clusters.Thermostat.Enums.SystemModeEnum
ControlSequenceEnum = clusters.Thermostat.Enums.ControlSequenceOfOperationEnum
ThermostatFeature = clusters.Thermostat.Bitmaps.Feature
@@ -210,22 +195,10 @@ class MatterClimate(MatterEntity, ClimateEntity):
_attr_temperature_unit: str = UnitOfTemperature.CELSIUS
_attr_hvac_mode: HVACMode = HVACMode.OFF
_matter_presets: list[clusters.Thermostat.Structs.PresetStruct]
_attr_preset_mode: str | None = None
_attr_preset_modes: list[str] | None = None
_feature_map: int | None = None
_platform_translation_key = "thermostat"
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the climate entity."""
# Initialize preset handle mapping as instance attribute before calling super().__init__()
# because MatterEntity.__init__() calls _update_from_device() which needs this attribute
self._matter_presets = []
self._preset_handle_by_name: dict[str, bytes | None] = {}
self._preset_name_by_handle: dict[bytes | None, str] = {}
super().__init__(*args, **kwargs)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
target_hvac_mode: HVACMode | None = kwargs.get(ATTR_HVAC_MODE)
@@ -270,34 +243,6 @@ class MatterClimate(MatterEntity, ClimateEntity):
matter_attribute=clusters.Thermostat.Attributes.OccupiedCoolingSetpoint,
)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
preset_handle = self._preset_handle_by_name[preset_mode]
command = clusters.Thermostat.Commands.SetActivePresetRequest(
presetHandle=preset_handle
)
await self.send_device_command(command)
# Optimistic update is required because Matter devices usually confirm
# preset changes asynchronously via a later attribute subscription.
# Additionally, some devices based on connectedhomeip do not send a
# subscription report for ActivePresetHandle after SetActivePresetRequest
# because thermostat-server-presets.cpp/SetActivePreset() updates the
# value without notifying the reporting engine. Keep this optimistic
# update as a workaround for that SDK bug and for normal report delays.
# Reference: project-chip/connectedhomeip,
# src/app/clusters/thermostat-server/thermostat-server-presets.cpp.
self._attr_preset_mode = preset_mode
self.async_write_ha_state()
# Keep the local ActivePresetHandle in sync until subscription update.
active_preset_path = create_attribute_path_from_attribute(
endpoint_id=self._endpoint.endpoint_id,
attribute=clusters.Thermostat.Attributes.ActivePresetHandle,
)
self._endpoint.set_attribute_value(active_preset_path, preset_handle)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
@@ -322,10 +267,10 @@ class MatterClimate(MatterEntity, ClimateEntity):
def _update_from_device(self) -> None:
"""Update from device."""
self._calculate_features()
self._attr_current_temperature = self._get_temperature_in_degrees(
clusters.Thermostat.Attributes.LocalTemperature
)
self._attr_current_humidity = (
int(raw_measured_humidity) / HUMIDITY_SCALING_FACTOR
if (
@@ -337,81 +282,6 @@ class MatterClimate(MatterEntity, ClimateEntity):
else None
)
self._update_presets()
self._update_hvac_mode_and_action()
self._update_target_temperatures()
self._update_temperature_limits()
@callback
def _update_presets(self) -> None:
"""Update preset modes and active preset."""
# Check if the device supports presets feature before attempting to load.
# Use the already computed supported features instead of re-reading
# the FeatureMap attribute to keep a single source of truth and avoid
# casting None when the attribute is temporarily unavailable.
supported_features = self._attr_supported_features or 0
if not (supported_features & ClimateEntityFeature.PRESET_MODE):
# Device does not support presets, skip preset update
self._preset_handle_by_name.clear()
self._preset_name_by_handle.clear()
self._attr_preset_modes = []
self._attr_preset_mode = None
return
self._matter_presets = (
self.get_matter_attribute_value(clusters.Thermostat.Attributes.Presets)
or []
)
# Build preset mapping: use device-provided name if available, else generate unique name
self._preset_handle_by_name.clear()
self._preset_name_by_handle.clear()
if self._matter_presets:
used_names = set()
for i, preset in enumerate(self._matter_presets, start=1):
preset_translation = PRESET_SCENARIO_TO_HA_PRESET.get(
preset.presetScenario
)
if preset_translation:
preset_name = preset_translation.lower()
else:
name = str(preset.name) if preset.name is not None else ""
name = name.strip()
if name:
preset_name = name
else:
# Ensure fallback name is unique
j = i
preset_name = f"Preset{j}"
while preset_name in used_names:
j += 1
preset_name = f"Preset{j}"
used_names.add(preset_name)
preset_handle = (
preset.presetHandle
if isinstance(preset.presetHandle, (bytes, type(None)))
else None
)
self._preset_handle_by_name[preset_name] = preset_handle
self._preset_name_by_handle[preset_handle] = preset_name
# Always include PRESET_NONE to allow users to clear the preset
self._preset_handle_by_name[PRESET_NONE] = None
self._preset_name_by_handle[None] = PRESET_NONE
self._attr_preset_modes = list(self._preset_handle_by_name)
# Update active preset mode
active_preset_handle = self.get_matter_attribute_value(
clusters.Thermostat.Attributes.ActivePresetHandle
)
self._attr_preset_mode = self._preset_name_by_handle.get(
active_preset_handle, PRESET_NONE
)
@callback
def _update_hvac_mode_and_action(self) -> None:
"""Update HVAC mode and action from device."""
if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False:
# special case: the appliance has a dedicated Power switch on the OnOff cluster
# if the mains power is off - treat it as if the HVAC mode is off
@@ -463,10 +333,7 @@ class MatterClimate(MatterEntity, ClimateEntity):
self._attr_hvac_action = HVACAction.FAN
else:
self._attr_hvac_action = HVACAction.OFF
@callback
def _update_target_temperatures(self) -> None:
"""Update target temperature or temperature range."""
# update target temperature high/low
supports_range = (
self._attr_supported_features
& ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
@@ -492,9 +359,6 @@ class MatterClimate(MatterEntity, ClimateEntity):
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
)
@callback
def _update_temperature_limits(self) -> None:
"""Update min and max temperature limits."""
# update min_temp
if self._attr_hvac_mode == HVACMode.COOL:
attribute = clusters.Thermostat.Attributes.AbsMinCoolSetpointLimit
@@ -534,9 +398,6 @@ class MatterClimate(MatterEntity, ClimateEntity):
self._attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_OFF
)
if feature_map & ThermostatFeature.kPresets:
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
# determine supported hvac modes
if feature_map & ThermostatFeature.kHeating:
self._attr_hvac_modes.append(HVACMode.HEAT)
if feature_map & ThermostatFeature.kCooling:
@@ -579,13 +440,9 @@ DISCOVERY_SCHEMAS = [
optional_attributes=(
clusters.Thermostat.Attributes.FeatureMap,
clusters.Thermostat.Attributes.ControlSequenceOfOperation,
clusters.Thermostat.Attributes.NumberOfPresets,
clusters.Thermostat.Attributes.Occupancy,
clusters.Thermostat.Attributes.OccupiedCoolingSetpoint,
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint,
clusters.Thermostat.Attributes.Presets,
clusters.Thermostat.Attributes.PresetTypes,
clusters.Thermostat.Attributes.ActivePresetHandle,
clusters.Thermostat.Attributes.SystemMode,
clusters.Thermostat.Attributes.ThermostatRunningMode,
clusters.Thermostat.Attributes.ThermostatRunningState,
@@ -597,6 +454,5 @@ DISCOVERY_SCHEMAS = [
),
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),
allow_multi=True, # also used for sensor entity
allow_none_value=True,
),
]

View File

@@ -145,16 +145,7 @@
},
"climate": {
"thermostat": {
"name": "Thermostat",
"state_attributes": {
"preset_mode": {
"state": {
"going_to_sleep": "Going to sleep",
"vacation": "Vacation",
"wake": "Wake"
}
}
}
"name": "Thermostat"
}
},
"cover": {

View File

@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN, PLATFORMS
from .const import PLATFORMS
from .coordinator import MicroBeesUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -50,17 +50,11 @@ async def async_migrate_entry(hass: HomeAssistant, entry: MicroBeesConfigEntry)
async def async_setup_entry(hass: HomeAssistant, entry: MicroBeesConfigEntry) -> bool:
"""Set up microBees from a config entry."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
except config_entry_oauth2_flow.ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
try:

View File

@@ -35,10 +35,5 @@
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -440,7 +440,6 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True):
no_program = 0, -1
cottons = 1, 10001
normal = 2
minimum_iron = 3
delicates = 4, 10022
woollens = 8, 10040

View File

@@ -6,16 +6,13 @@ import logging
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
from .api import AuthenticatedMonzoAPI
from .const import DOMAIN
from .coordinator import MonzoConfigEntry, MonzoCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -42,13 +39,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> b
async def async_setup_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> bool:
"""Set up Monzo from a config entry."""
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
implementation = await async_get_config_entry_implementation(hass, entry)
session = OAuth2Session(hass, entry, implementation)

View File

@@ -50,10 +50,5 @@
"name": "Total balance"
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
}
}

View File

@@ -18,6 +18,8 @@ ABBREVIATIONS = {
"bri_stat_t": "brightness_state_topic",
"bri_tpl": "brightness_template",
"bri_val_tpl": "brightness_value_template",
"cln_segmnts_cmd_t": "clean_segments_command_topic",
"cln_segmnts_cmd_tpl": "clean_segments_command_template",
"clr_temp_cmd_tpl": "color_temp_command_template",
"clrm_stat_t": "color_mode_state_topic",
"clrm_val_tpl": "color_mode_value_template",
@@ -185,6 +187,7 @@ ABBREVIATIONS = {
"rgbww_cmd_t": "rgbww_command_topic",
"rgbww_stat_t": "rgbww_state_topic",
"rgbww_val_tpl": "rgbww_value_template",
"segmnts": "segments",
"send_cmd_t": "send_command_topic",
"send_if_off": "send_if_off",
"set_fan_spd_t": "set_fan_speed_topic",

View File

@@ -1484,6 +1484,7 @@ class MqttEntity(
self._config = config
self._setup_from_config(self._config)
self._setup_common_attributes_from_config(self._config)
self._process_entity_update()
# Prepare MQTT subscriptions
self.attributes_prepare_discovery_update(config)
@@ -1586,6 +1587,10 @@ class MqttEntity(
def _setup_from_config(self, config: ConfigType) -> None:
"""(Re)Setup the entity."""
@callback
def _process_entity_update(self) -> None:
"""Process an entity discovery update."""
@abstractmethod
@callback
def _prepare_subscribe_topics(self) -> None:

View File

@@ -10,12 +10,13 @@ import voluptuous as vol
from homeassistant.components import vacuum
from homeassistant.components.vacuum import (
ENTITY_ID_FORMAT,
Segment,
StateVacuumEntity,
VacuumActivity,
VacuumEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -27,7 +28,7 @@ from . import subscription
from .config import MQTT_BASE_SCHEMA
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC
from .entity import MqttEntity, async_setup_entity_entry_helper
from .models import ReceiveMessage
from .models import MqttCommandTemplate, ReceiveMessage
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
from .util import valid_publish_topic
@@ -52,6 +53,9 @@ POSSIBLE_STATES: dict[str, VacuumActivity] = {
STATE_CLEANING: VacuumActivity.CLEANING,
}
CONF_SEGMENTS = "segments"
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC = "clean_segments_command_topic"
CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE = "clean_segments_command_template"
CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES
CONF_PAYLOAD_TURN_ON = "payload_turn_on"
CONF_PAYLOAD_TURN_OFF = "payload_turn_off"
@@ -137,8 +141,39 @@ MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset(
MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/"
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
def validate_clean_area_config(config: ConfigType) -> ConfigType:
"""Check for a valid configuration and check segments."""
if (config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC not in config) or (
not config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config
):
raise vol.Invalid(
f"Options `{CONF_SEGMENTS}` and "
f"`{CONF_CLEAN_SEGMENTS_COMMAND_TOPIC}` must be defined together"
)
segments: list[str]
if segments := config[CONF_SEGMENTS]:
if not config.get(CONF_UNIQUE_ID):
raise vol.Invalid(
f"Option `{CONF_SEGMENTS}` requires `{CONF_UNIQUE_ID}` to be configured"
)
unique_segments: set[str] = set()
for segment in segments:
segment_id, _, _ = segment.partition(".")
if not segment_id or segment_id in unique_segments:
raise vol.Invalid(
f"The `{CONF_SEGMENTS}` option contains an invalid or non-"
f"unique segment ID '{segment_id}'. Got {segments}"
)
unique_segments.add(segment_id)
return config
_BASE_SCHEMA = MQTT_BASE_SCHEMA.extend(
{
vol.Optional(CONF_SEGMENTS, default=[]): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TOPIC): valid_publish_topic,
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE): cv.template,
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(
cv.ensure_list, [cv.string]
),
@@ -164,7 +199,10 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
}
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.ALLOW_EXTRA)
PLATFORM_SCHEMA_MODERN = vol.All(_BASE_SCHEMA, validate_clean_area_config)
DISCOVERY_SCHEMA = vol.All(
_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_clean_area_config
)
async def async_setup_entry(
@@ -191,9 +229,11 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
_entity_id_format = ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED
_segments: list[Segment]
_command_topic: str | None
_set_fan_speed_topic: str | None
_send_command_topic: str | None
_clean_segments_command_topic: str
_payloads: dict[str, str | None]
def __init__(
@@ -229,6 +269,23 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
self._attr_supported_features = _strings_to_services(
supported_feature_strings, STRING_TO_SERVICE
)
if config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config:
self._attr_supported_features |= VacuumEntityFeature.CLEAN_AREA
segments: list[str] = config[CONF_SEGMENTS]
self._segments = [
Segment(id=segment_id, name=name or segment_id)
for segment_id, _, name in [
segment.partition(".") for segment in segments
]
]
self._clean_segments_command_topic = config[
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC
]
self._clean_segments_command_template = MqttCommandTemplate(
config.get(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE),
entity=self,
).async_render
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
self._command_topic = config.get(CONF_COMMAND_TOPIC)
self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC)
@@ -246,6 +303,20 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
)
}
@callback
def _process_entity_update(self) -> None:
"""Check vacuum segments with registry entry."""
if (
self._attr_supported_features & VacuumEntityFeature.CLEAN_AREA
and (last_seen := self.last_seen_segments) is not None
and {s.id: s for s in last_seen} != {s.id: s for s in self._segments}
):
self.async_create_segments_issue()
async def mqtt_async_added_to_hass(self) -> None:
"""Check vacuum segments with registry entry."""
self._process_entity_update()
def _update_state_attributes(self, payload: dict[str, Any]) -> None:
"""Update the entity state attributes."""
self._state_attrs.update(payload)
@@ -277,6 +348,19 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
"""(Re)Subscribe to topics."""
subscription.async_subscribe_topics_internal(self.hass, self._sub_state)
async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
"""Perform an area clean."""
await self.async_publish_with_config(
self._clean_segments_command_topic,
self._clean_segments_command_template(
json_dumps(segment_ids), {"value": segment_ids}
),
)
async def async_get_segments(self) -> list[Segment]:
"""Return the available segments."""
return self._segments
async def _async_publish_command(self, feature: VacuumEntityFeature) -> None:
"""Publish a command."""
if self._command_topic is None:

View File

@@ -2,19 +2,14 @@
import logging
from aiohttp import ClientError
import aiohttp
from pybotvac import Account
from pybotvac.exceptions import NeatoException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -63,9 +58,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
session = OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()
except OAuth2TokenRequestReauthError as ex:
raise ConfigEntryAuthFailed from ex
except (OAuth2TokenRequestError, ClientError) as ex:
except aiohttp.ClientResponseError as ex:
_LOGGER.debug("API error: %s (%s)", ex.code, ex.message)
if ex.code in (401, 403):
raise ConfigEntryAuthFailed("Token not valid, trigger renewal") from ex
raise ConfigEntryNotReady from ex
neato_session = api.ConfigEntryAuth(hass, entry, implementation)

View File

@@ -8,8 +8,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
ATTR_AFFECTED_AREAS,
@@ -25,13 +28,13 @@ from .const import (
ATTR_WEB,
CONF_MESSAGE_SLOTS,
CONF_REGIONS,
DOMAIN,
)
from .coordinator import NinaConfigEntry, NINADataUpdateCoordinator
from .entity import NinaEntity
async def async_setup_entry(
_: HomeAssistant,
hass: HomeAssistant,
config_entry: NinaConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
@@ -43,7 +46,7 @@ async def async_setup_entry(
message_slots: int = config_entry.data[CONF_MESSAGE_SLOTS]
async_add_entities(
NINAMessage(coordinator, ent, regions[ent], i + 1)
NINAMessage(coordinator, ent, regions[ent], i + 1, config_entry)
for ent in coordinator.data
for i in range(message_slots)
)
@@ -52,7 +55,7 @@ async def async_setup_entry(
PARALLEL_UPDATES = 0
class NINAMessage(NinaEntity, BinarySensorEntity):
class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEntity):
"""Representation of an NINA warning."""
_attr_device_class = BinarySensorDeviceClass.SAFETY
@@ -64,20 +67,31 @@ class NINAMessage(NinaEntity, BinarySensorEntity):
region: str,
region_name: str,
slot_id: int,
config_entry: ConfigEntry,
) -> None:
"""Initialize."""
super().__init__(coordinator, region, region_name, slot_id)
super().__init__(coordinator)
self._attr_translation_key = "warning"
self._region = region
self._warning_index = slot_id - 1
self._attr_name = f"Warning: {region_name} {slot_id}"
self._attr_unique_id = f"{region}-{slot_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, config_entry.entry_id)},
manufacturer="NINA",
entry_type=DeviceEntryType.SERVICE,
)
@property
def is_on(self) -> bool:
"""Return the state of the sensor."""
if self._get_active_warnings_count() <= self._warning_index:
if len(self.coordinator.data[self._region]) <= self._warning_index:
return False
return self._get_warning_data().is_valid
data = self.coordinator.data[self._region][self._warning_index]
return data.is_valid
@property
def extra_state_attributes(self) -> dict[str, Any]:
@@ -85,7 +99,7 @@ class NINAMessage(NinaEntity, BinarySensorEntity):
if not self.is_on:
return {}
data = self._get_warning_data()
data = self.coordinator.data[self._region][self._warning_index]
return {
ATTR_HEADLINE: data.headline,

View File

@@ -12,7 +12,6 @@ from pynina import ApiError, Nina
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
@@ -65,12 +64,6 @@ class NINADataUpdateCoordinator(
]
self.area_filter: str = config_entry.data[CONF_FILTERS][CONF_AREA_FILTER]
self.device_info = DeviceInfo(
identifiers={(DOMAIN, config_entry.entry_id)},
manufacturer="NINA",
entry_type=DeviceEntryType.SERVICE,
)
regions: dict[str, str] = config_entry.data[CONF_REGIONS]
for region in regions:
self._nina.add_region(region)

View File

@@ -1,36 +0,0 @@
"""NINA common entity."""
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import NINADataUpdateCoordinator, NinaWarningData
class NinaEntity(CoordinatorEntity[NINADataUpdateCoordinator]):
"""Base class for NINA entities."""
def __init__(
self,
coordinator: NINADataUpdateCoordinator,
region: str,
region_name: str,
slot_id: int,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._region = region
self._warning_index = slot_id - 1
self._attr_translation_placeholders = {
"region_name": region_name,
"slot_id": str(slot_id),
}
self._attr_device_info = coordinator.device_info
def _get_active_warnings_count(self) -> int:
"""Return the number of active warnings for the region."""
return len(self.coordinator.data[self._region])
def _get_warning_data(self) -> NinaWarningData:
"""Return warning data."""
return self.coordinator.data[self._region][self._warning_index]

View File

@@ -45,13 +45,6 @@
}
}
},
"entity": {
"binary_sensor": {
"warning": {
"name": "Warning: {region_name} {slot_id}"
}
}
},
"options": {
"abort": {
"no_fetch": "[%key:component::nina::config::abort::no_fetch%]",

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aiontfy"],
"quality_scale": "platinum",
"requirements": ["aiontfy==0.8.3"]
"requirements": ["aiontfy==0.8.1"]
}

View File

@@ -76,8 +76,6 @@ class ReceiverManager:
if manager_task in done:
# Something went wrong, so let's return the manager task,
# so that it can be awaited to error out
wait_for_started_task.cancel()
await asyncio.wait((wait_for_started_task,))
return manager_task
return None

View File

@@ -41,7 +41,7 @@ from .const import (
TILT_FACTOR,
ZOOM_FACTOR,
)
from .event_manager import EventManager
from .event import EventManager
from .models import PTZ, Capabilities, DeviceInfo, Profile, Resolution, Video

View File

@@ -1,7 +1,7 @@
{
"domain": "onvif",
"name": "ONVIF",
"codeowners": ["@jterrace"],
"codeowners": ["@hunterjm", "@jterrace"],
"config_flow": true,
"dependencies": ["ffmpeg"],
"dhcp": [

View File

@@ -1,22 +1 @@
"""The PJLink integration."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
_PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up PJLink from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
"""The pjlink component."""

View File

@@ -1,100 +0,0 @@
"""Config flow for the PJLink integration."""
from __future__ import annotations
import logging
from typing import Any
from pypjlink import Projector
from pypjlink.projector import ProjectorError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
from homeassistant.helpers import config_validation as cv
from .const import DEFAULT_PORT, DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_PASSWORD): str,
}
)
def validate_projector_connection(
host: str, port: int | None, password: str | None
) -> str:
"""Validate that we can connect to the projector."""
with Projector.from_address(host, port) as projector:
projector.authenticate(password)
return projector.get_name()
class PJLinkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for PJLink."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match(
{CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT]}
)
try:
projector_name = await self.hass.async_add_executor_job(
validate_projector_connection,
user_input[CONF_HOST],
user_input[CONF_PORT],
user_input.get(CONF_PASSWORD),
)
except TimeoutError, OSError:
errors["base"] = "cannot_connect"
except RuntimeError, ProjectorError:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(title=projector_name, data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_import(
self, import_config: dict[str, Any]
) -> ConfigFlowResult:
"""Import a config entry from configuration.yaml."""
host: str = import_config[CONF_HOST]
port: int = import_config.get(CONF_PORT, DEFAULT_PORT)
password: str | None = import_config.get(CONF_PASSWORD)
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
try:
projector_name = await self.hass.async_add_executor_job(
validate_projector_connection, host, port, password
)
except TimeoutError, OSError:
return self.async_abort(reason="cannot_connect")
except RuntimeError, ProjectorError:
return self.async_abort(reason="invalid_auth")
except Exception:
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
else:
import_data: dict[str, Any] = {CONF_HOST: host, CONF_PORT: port}
if password:
import_data[CONF_PASSWORD] = password
return self.async_create_entry(
title=import_config.get(CONF_NAME, projector_name), data=import_data
)

View File

@@ -2,10 +2,9 @@
"domain": "pjlink",
"name": "PJLink",
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/pjlink",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pypjlink"],
"quality_scale": "legacy",
"requirements": ["pypjlink2==1.2.1"]
}

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
from typing import Any
from pypjlink import MUTE_AUDIO, Projector
from pypjlink.projector import ProjectorError
import voluptuous as vol
@@ -14,15 +12,10 @@ from homeassistant.components.media_player import (
MediaPlayerEntityFeature,
MediaPlayerState,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.entity_platform import (
AddConfigEntryEntitiesCallback,
AddEntitiesCallback,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_ENCODING, DEFAULT_ENCODING, DEFAULT_PORT, DOMAIN
@@ -40,60 +33,30 @@ PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
)
async def async_setup_platform(
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the PJLink platform."""
host = config.get(CONF_HOST)
port = config.get(CONF_PORT)
name = config.get(CONF_NAME)
encoding = config.get(CONF_ENCODING)
password = config.get(CONF_PASSWORD)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
)
if (
result.get("type") is FlowResultType.ABORT
and result.get("reason") != "already_configured"
):
ir.async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_import_issue_{result.get('reason')}",
breaks_in_ha_version="2026.11.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key=f"deprecated_yaml_import_issue_{result.get('reason')}",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "PJLink",
},
)
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
hass_data = hass.data[DOMAIN]
device_label = f"{host}:{port}"
if device_label in hass_data:
return
ir.async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2026.11.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "PJLink",
},
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up PJLink media player."""
async_add_entities([PjLinkDevice(entry)], update_before_add=True)
device = PjLinkDevice(host, port, name, encoding, password)
hass_data[device_label] = device
add_entities([device], True)
def format_input_source(input_source_name, input_source_number):
@@ -111,20 +74,20 @@ class PjLinkDevice(MediaPlayerEntity):
| MediaPlayerEntityFeature.SELECT_SOURCE
)
def __init__(self, entry: ConfigEntry) -> None:
"""Initialize the PJLink device."""
self._host = entry.data[CONF_HOST]
self._port = entry.data[CONF_PORT]
self._password = entry.data.get(CONF_PASSWORD)
self._source_name_mapping: dict[str, Any] = {}
def __init__(self, host, port, name, encoding, password):
"""Iinitialize the PJLink device."""
self._host = host
self._port = port
self._password = password
self._encoding = encoding
self._source_name_mapping = {}
self._attr_name = entry.title
self._attr_name = name
self._attr_is_volume_muted = False
self._attr_state = MediaPlayerState.OFF
self._attr_source = None
self._attr_source_list = []
self._attr_available = False
self._attr_unique_id = entry.entry_id
def _force_off(self):
self._attr_state = MediaPlayerState.OFF

View File

@@ -1,35 +0,0 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]"
}
}
}
},
"issues": {
"deprecated_yaml_import_issue_cannot_connect": {
"description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your configuration, a connection error occurred. Please correct your YAML configuration and restart Home Assistant, or remove the {domain} key from your configuration and configure the integration via the UI.",
"title": "[%key:component::homeassistant::issues::deprecated_yaml::title%]"
},
"deprecated_yaml_import_issue_invalid_auth": {
"description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your configuration, invalid authentication details were found. Please correct your YAML configuration and restart Home Assistant, or remove the {domain} key from your configuration and configure the integration via the UI.",
"title": "[%key:component::homeassistant::issues::deprecated_yaml::title%]"
},
"deprecated_yaml_import_issue_unknown": {
"description": "Configuring {integration_title} via YAML is deprecated and will be removed in a future release. While importing your configuration, an unexpected exception occurred. Please correct your YAML configuration and restart Home Assistant, or remove the {domain} key from your configuration and configure the integration via the UI.",
"title": "[%key:component::homeassistant::issues::deprecated_yaml::title%]"
}
}
}

View File

@@ -88,9 +88,6 @@
},
"charge_set_schedules": {
"service": "mdi:calendar-clock"
},
"charge_start": {
"service": "mdi:ev-station"
}
}
}

View File

@@ -165,11 +165,9 @@ class RenaultVehicleProxy:
return await self._vehicle.set_charge_mode(charge_mode)
@with_error_wrapping
async def set_charge_start(
self, when: datetime | None = None
) -> models.KamereonVehicleChargingStartActionData:
async def set_charge_start(self) -> models.KamereonVehicleChargingStartActionData:
"""Start vehicle charge."""
return await self._vehicle.set_charge_start(when)
return await self._vehicle.set_charge_start()
@with_error_wrapping
async def set_charge_stop(self) -> models.KamereonVehicleChargingStartActionData:

View File

@@ -36,11 +36,6 @@ SERVICE_AC_START_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend(
vol.Optional(ATTR_WHEN): cv.datetime,
}
)
SERVICE_CHARGE_START_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend(
{
vol.Optional(ATTR_WHEN): cv.datetime,
}
)
SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA = vol.Schema(
{
vol.Required("startTime"): cv.string,
@@ -118,16 +113,6 @@ async def ac_start(service_call: ServiceCall) -> None:
LOGGER.debug("A/C start result: %s", result.raw_data)
async def charge_start(service_call: ServiceCall) -> None:
"""Start Charging with optional delay."""
when: datetime | None = service_call.data.get(ATTR_WHEN)
proxy = get_vehicle_proxy(service_call)
LOGGER.debug("Charge start attempt, when: %s", when)
result = await proxy.set_charge_start(when)
LOGGER.debug("Charge start result: %s", result.raw_data)
async def charge_set_schedules(service_call: ServiceCall) -> None:
"""Set charge schedules."""
schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES]
@@ -211,12 +196,6 @@ def async_setup_services(hass: HomeAssistant) -> None:
ac_start,
schema=SERVICE_AC_START_SCHEMA,
)
hass.services.async_register(
DOMAIN,
"charge_start",
charge_start,
schema=SERVICE_CHARGE_START_SCHEMA,
)
hass.services.async_register(
DOMAIN,
"charge_set_schedules",

View File

@@ -54,18 +54,6 @@ ac_set_schedules:
selector:
object:
charge_start:
fields:
vehicle:
required: true
selector:
device:
integration: renault
when:
example: "2026-03-01T17:45:00"
selector:
datetime:
charge_set_schedules:
fields:
vehicle:

View File

@@ -276,20 +276,6 @@
}
},
"name": "Update charge schedule"
},
"charge_start": {
"description": "Starts charging on vehicle.",
"fields": {
"vehicle": {
"description": "[%key:component::renault::services::ac_start::fields::vehicle::description%]",
"name": "Vehicle"
},
"when": {
"description": "Timestamp for charging to start (optional - defaults to now).",
"name": "When"
}
},
"name": "Start charging"
}
}
}

View File

@@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["aiorussound"],
"quality_scale": "silver",
"requirements": ["aiorussound==4.9.1"],
"requirements": ["aiorussound==4.9.0"],
"zeroconf": ["_rio._tcp.local."]
}

View File

@@ -19,7 +19,6 @@ PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.LIGHT,
Platform.REMOTE,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,

View File

@@ -1,70 +0,0 @@
"""Remote platform for SLZB-Ultima."""
import asyncio
from collections.abc import Iterable
from typing import Any
from pysmlight.exceptions import SmlightError
from pysmlight.models import IRPayload
from homeassistant.components.remote import (
ATTR_DELAY_SECS,
ATTR_NUM_REPEATS,
DEFAULT_DELAY_SECS,
DEFAULT_NUM_REPEATS,
RemoteEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import SmConfigEntry, SmDataUpdateCoordinator
from .entity import SmEntity
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
entry: SmConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Initialize remote for SLZB-Ultima device."""
coordinator = entry.runtime_data.data
if coordinator.data.info.has_peripherals:
async_add_entities([SmRemoteEntity(coordinator)])
class SmRemoteEntity(SmEntity, RemoteEntity):
"""Representation of a SLZB-Ultima remote."""
_attr_translation_key = "remote"
_attr_is_on = True
def __init__(self, coordinator: SmDataUpdateCoordinator) -> None:
"""Initialize the SLZB-Ultima remote."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.unique_id}-remote"
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send a sequence of commands to a device."""
num_repeats = kwargs.get(ATTR_NUM_REPEATS, DEFAULT_NUM_REPEATS)
delay_secs = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
for _ in range(num_repeats):
for cmd in command:
try:
await self.coordinator.async_execute_command(
self.coordinator.client.actions.send_ir_code,
IRPayload(code=cmd),
)
except SmlightError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="send_ir_code_failed",
translation_placeholders={"error": str(err)},
) from err
await asyncio.sleep(delay_secs)

View File

@@ -84,11 +84,6 @@
"name": "Ambilight"
}
},
"remote": {
"remote": {
"name": "IR Remote"
}
},
"sensor": {
"core_temperature": {
"name": "Core chip temp"
@@ -164,9 +159,6 @@
},
"firmware_update_failed": {
"message": "Firmware update failed for {device_name}."
},
"send_ir_code_failed": {
"message": "Failed to send IR code: {error}."
}
},
"issues": {

View File

@@ -14,5 +14,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["aiosolaredge", "solaredge_web"],
"requirements": ["aiosolaredge==1.0.2", "solaredge-web==0.0.1"]
"requirements": ["aiosolaredge==0.2.0", "solaredge-web==0.0.1"]
}

View File

@@ -315,6 +315,7 @@ async def make_device_data(
)
devices_data.binary_sensors.append((device, coordinator))
devices_data.sensors.append((device, coordinator))
if isinstance(device, Device) and device.device_type == "AI Art Frame":
coordinator = await coordinator_for_device(
hass, entry, api, device, coordinators_by_id
@@ -322,11 +323,6 @@ async def make_device_data(
devices_data.buttons.append((device, coordinator))
devices_data.sensors.append((device, coordinator))
devices_data.images.append((device, coordinator))
if isinstance(device, Device) and device.device_type == "WeatherStation":
coordinator = await coordinator_for_device(
hass, entry, api, device, coordinators_by_id
)
devices_data.sensors.append((device, coordinator))
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -257,11 +257,6 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
),
"Smart Radiator Thermostat": (BATTERY_DESCRIPTION,),
"AI Art Frame": (BATTERY_DESCRIPTION,),
"WeatherStation": (
BATTERY_DESCRIPTION,
TEMPERATURE_DESCRIPTION,
HUMIDITY_DESCRIPTION,
),
}

Some files were not shown because too many files have changed in this diff Show More