mirror of
https://github.com/home-assistant/core.git
synced 2026-04-02 05:26:17 +00:00
Compare commits
2 Commits
python-3.1
...
edenhaus-t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aae63cb397 | ||
|
|
78aa5b9913 |
96
.github/workflows/builder.yml
vendored
96
.github/workflows/builder.yml
vendored
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.14.3
|
||||
3.14.2
|
||||
|
||||
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -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
4
Dockerfile
generated
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -12,11 +12,6 @@
|
||||
"resume": {
|
||||
"default": "mdi:play"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"dimming_time": {
|
||||
"default": "mdi:timer-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -39,11 +39,6 @@
|
||||
"resume": {
|
||||
"name": "Resume dimming"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"dimming_time": {
|
||||
"name": "Dimming time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -49,10 +49,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -65,10 +65,5 @@
|
||||
"unit_of_measurement": "souvenirs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["goodwe"],
|
||||
"requirements": ["goodwe==0.4.10"]
|
||||
"requirements": ["goodwe==0.4.8"]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -57,11 +57,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -42,10 +42,5 @@
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["habiticalib"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["habiticalib==0.4.7"]
|
||||
"requirements": ["habiticalib==0.4.6"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
)
|
||||
@@ -1,11 +1,4 @@
|
||||
{
|
||||
"entity": {
|
||||
"event": {
|
||||
"event": {
|
||||
"default": "mdi:gesture-tap-button"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"dismiss": {
|
||||
"service": "mdi:bell-off"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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."
|
||||
},
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ rules:
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2026.3.0"]
|
||||
"requirements": ["pydrawise==2025.9.0"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioimmich==0.12.1"]
|
||||
"requirements": ["aioimmich==0.12.0"]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -50,10 +50,5 @@
|
||||
"name": "Total balance"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
@@ -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%]",
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiontfy"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiontfy==0.8.3"]
|
||||
"requirements": ["aiontfy==0.8.1"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "onvif",
|
||||
"name": "ONVIF",
|
||||
"codeowners": ["@jterrace"],
|
||||
"codeowners": ["@hunterjm", "@jterrace"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["ffmpeg"],
|
||||
"dhcp": [
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,9 +88,6 @@
|
||||
},
|
||||
"charge_set_schedules": {
|
||||
"service": "mdi:calendar-clock"
|
||||
},
|
||||
"charge_start": {
|
||||
"service": "mdi:ev-station"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.LIGHT,
|
||||
Platform.REMOTE,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.UPDATE,
|
||||
|
||||
@@ -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)
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user