mirror of
https://github.com/home-assistant/core.git
synced 2026-04-17 07:06:24 +02:00
Compare commits
82 Commits
copilot/su
...
bump/pytho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bada82fc6e | ||
|
|
6b2a4df6e0 | ||
|
|
2ac3979f83 | ||
|
|
2fb44bce5d | ||
|
|
d187e61274 | ||
|
|
7f79da2f75 | ||
|
|
4871344138 | ||
|
|
b0ba740024 | ||
|
|
b6f49d2063 | ||
|
|
c8e2a2b520 | ||
|
|
27365a4457 | ||
|
|
4efcb5a700 | ||
|
|
4ffc4b8f71 | ||
|
|
e8fa61ae63 | ||
|
|
c6c469cc7a | ||
|
|
d51afe20e0 | ||
|
|
5dac5ef099 | ||
|
|
a1b93b418b | ||
|
|
22a96583a9 | ||
|
|
01ffa6a676 | ||
|
|
abf37849fb | ||
|
|
72cd7ed178 | ||
|
|
6305ea8cf2 | ||
|
|
815c30b213 | ||
|
|
68cc6df3e0 | ||
|
|
7f700c891a | ||
|
|
e33727a75a | ||
|
|
374a050636 | ||
|
|
90045e5539 | ||
|
|
e30c379979 | ||
|
|
86d80c96d7 | ||
|
|
1bbecec991 | ||
|
|
3970a27369 | ||
|
|
745107c192 | ||
|
|
24530d13af | ||
|
|
7529b9252c | ||
|
|
92a1c4568d | ||
|
|
c6527c9f6d | ||
|
|
05b7fa9602 | ||
|
|
375bd55ae6 | ||
|
|
fbd0cb8666 | ||
|
|
7e061262ad | ||
|
|
0e9c17fb16 | ||
|
|
940de5ea84 | ||
|
|
bf4773d9bc | ||
|
|
0bedcc55ce | ||
|
|
313f97fc47 | ||
|
|
b7d32e0650 | ||
|
|
b9afb2a861 | ||
|
|
d3a01d4c80 | ||
|
|
05bd2d05f5 | ||
|
|
23469d8950 | ||
|
|
26f677dcd1 | ||
|
|
484d9b0cbe | ||
|
|
03ed46aa07 | ||
|
|
e5f4000ac2 | ||
|
|
406598dbfa | ||
|
|
7f23a35155 | ||
|
|
0e521eda2e | ||
|
|
5a72dc8eca | ||
|
|
b11292385f | ||
|
|
2179a5405a | ||
|
|
78f5989cd6 | ||
|
|
cca44c675c | ||
|
|
0ebe65c25b | ||
|
|
f7ee95c4b9 | ||
|
|
d78c05ab62 | ||
|
|
9f41e3341f | ||
|
|
8ab3d482b9 | ||
|
|
a485c3d410 | ||
|
|
38b27d624a | ||
|
|
f437d65d3c | ||
|
|
5ba0764a87 | ||
|
|
69fd6532cc | ||
|
|
ee8bd9f016 | ||
|
|
de5a2d47a5 | ||
|
|
54b2e0285c | ||
|
|
a0e118d411 | ||
|
|
07c33233ee | ||
|
|
962cac902b | ||
|
|
9ff5c9863f | ||
|
|
b60e396241 |
5
.github/copilot-instructions.md
vendored
5
.github/copilot-instructions.md
vendored
@@ -11,10 +11,9 @@
|
||||
|
||||
This repository contains the core of Home Assistant, a Python 3 based home automation application.
|
||||
|
||||
## Code Review Guidelines
|
||||
## Git Commit Guidelines
|
||||
|
||||
**Git commit practices during review:**
|
||||
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
|
||||
- **Do NOT amend, squash, or rebase commits that have already been pushed to the PR branch after the PR is opened** - Reviewers need to follow the commit history, as well as see what changed since their last review
|
||||
|
||||
## Development Commands
|
||||
|
||||
|
||||
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -342,7 +342,7 @@ jobs:
|
||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
steps:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
with:
|
||||
cosign-release: "v2.5.3"
|
||||
|
||||
|
||||
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@@ -1392,7 +1392,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
@@ -1563,7 +1563,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # zizmor: ignore[secrets-outside-env]
|
||||
@@ -1591,7 +1591,7 @@ jobs:
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
report_type: test_results
|
||||
fail_ci_if_error: true
|
||||
|
||||
@@ -87,6 +87,13 @@ repos:
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(homeassistant/.+/manifest\.json|homeassistant/brands/.+\.json|pyproject\.toml|\.pre-commit-config\.yaml|script/gen_requirements_all\.py)$
|
||||
- id: gen_copilot_instructions
|
||||
name: gen_copilot_instructions
|
||||
entry: script/run-in-env.sh python3 -m script.gen_copilot_instructions
|
||||
pass_filenames: false
|
||||
language: script
|
||||
types: [text]
|
||||
files: ^(AGENTS\.md|\.claude/skills/(?!github-pr-reviewer/).+/SKILL\.md|\.github/copilot-instructions\.md|script/gen_copilot_instructions\.py)$
|
||||
- id: hassfest
|
||||
name: hassfest
|
||||
entry: script/run-in-env.sh python3 -m script.hassfest
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
This repository contains the core of Home Assistant, a Python 3 based home automation application.
|
||||
|
||||
## Code Review Guidelines
|
||||
## Git Commit Guidelines
|
||||
|
||||
**Git commit practices during review:**
|
||||
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
|
||||
- **Do NOT amend, squash, or rebase commits that have already been pushed to the PR branch after the PR is opened** - Reviewers need to follow the commit history, as well as see what changed since their last review
|
||||
|
||||
## Development Commands
|
||||
|
||||
|
||||
9
CODEOWNERS
generated
9
CODEOWNERS
generated
@@ -37,6 +37,13 @@ build.json @home-assistant/supervisor
|
||||
# Other code
|
||||
/homeassistant/scripts/check_config.py @kellerza
|
||||
|
||||
# Agent Configurations
|
||||
AGENTS.md @home-assistant/core
|
||||
CLAUDE.md @home-assistant/core
|
||||
/.agent/ @home-assistant/core
|
||||
/.claude/ @home-assistant/core
|
||||
/.gemini/ @home-assistant/core
|
||||
|
||||
# Integrations
|
||||
/homeassistant/components/abode/ @shred86
|
||||
/tests/components/abode/ @shred86
|
||||
@@ -1301,6 +1308,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/pi_hole/ @shenxn
|
||||
/homeassistant/components/picnic/ @corneyl @codesalatdev
|
||||
/tests/components/picnic/ @corneyl @codesalatdev
|
||||
/homeassistant/components/picotts/ @rooggiieerr
|
||||
/tests/components/picotts/ @rooggiieerr
|
||||
/homeassistant/components/ping/ @jpbede
|
||||
/tests/components/ping/ @jpbede
|
||||
/homeassistant/components/plaato/ @JohNan
|
||||
|
||||
@@ -36,7 +36,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
|
||||
translation_key="auth_error",
|
||||
) from err
|
||||
except ActronAirAPIError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_connection_error",
|
||||
) from err
|
||||
|
||||
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
|
||||
for system in systems:
|
||||
|
||||
@@ -64,7 +64,7 @@ rules:
|
||||
status: exempt
|
||||
comment: Not required for this integration at this stage.
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
exception-translations: done
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
"auth_error": {
|
||||
"message": "Authentication failed, please reauthenticate"
|
||||
},
|
||||
"setup_connection_error": {
|
||||
"message": "Failed to connect to the Actron Air API"
|
||||
},
|
||||
"update_error": {
|
||||
"message": "An error occurred while retrieving data from the Actron Air API: {error}"
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@ async def _resolve_attachments(
|
||||
resolved_attachments.append(
|
||||
conversation.Attachment(
|
||||
media_content_id=media_content_id,
|
||||
mime_type=image_data.content_type,
|
||||
mime_type=attachment.get("media_content_type")
|
||||
or image_data.content_type,
|
||||
path=temp_filename,
|
||||
)
|
||||
)
|
||||
@@ -89,7 +90,7 @@ async def _resolve_attachments(
|
||||
resolved_attachments.append(
|
||||
conversation.Attachment(
|
||||
media_content_id=media_content_id,
|
||||
mime_type=media.mime_type,
|
||||
mime_type=attachment.get("media_content_type") or media.mime_type,
|
||||
path=media.path,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["arcam"],
|
||||
"requirements": ["arcam-fmj==1.8.2"],
|
||||
"requirements": ["arcam-fmj==1.8.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -91,6 +91,7 @@ SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
|
||||
value_fn=lambda state: (
|
||||
vp.colorspace.name.lower()
|
||||
if (vp := state.get_incoming_video_parameters()) is not None
|
||||
and vp.colorspace is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
},
|
||||
"services": {
|
||||
"announce": {
|
||||
"description": "Lets a satellite announce a message.",
|
||||
"description": "Lets an Assist satellite announce a message.",
|
||||
"fields": {
|
||||
"media_id": {
|
||||
"description": "The media ID to announce instead of using text-to-speech.",
|
||||
@@ -94,10 +94,10 @@
|
||||
"name": "Preannounce media ID"
|
||||
}
|
||||
},
|
||||
"name": "Announce"
|
||||
"name": "Announce on satellite"
|
||||
},
|
||||
"ask_question": {
|
||||
"description": "Asks a question and gets the user's response.",
|
||||
"description": "Lets an Assist satellite ask a question and get the user's response.",
|
||||
"fields": {
|
||||
"answers": {
|
||||
"description": "Possible answers to the question.",
|
||||
@@ -124,10 +124,10 @@
|
||||
"name": "Question media ID"
|
||||
}
|
||||
},
|
||||
"name": "Ask question"
|
||||
"name": "Ask question on satellite"
|
||||
},
|
||||
"start_conversation": {
|
||||
"description": "Starts a conversation from a satellite.",
|
||||
"description": "Starts a conversation from an Assist satellite.",
|
||||
"fields": {
|
||||
"extra_system_prompt": {
|
||||
"description": "Provide background information to the AI about the request.",
|
||||
@@ -150,13 +150,13 @@
|
||||
"name": "Message"
|
||||
}
|
||||
},
|
||||
"name": "Start conversation"
|
||||
"name": "Start conversation on satellite"
|
||||
}
|
||||
},
|
||||
"title": "Assist satellite",
|
||||
"triggers": {
|
||||
"idle": {
|
||||
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
|
||||
"description": "Triggers after one or more Assist satellites become idle after having processed a command.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
@@ -165,7 +165,7 @@
|
||||
"name": "Satellite became idle"
|
||||
},
|
||||
"listening": {
|
||||
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
|
||||
"description": "Triggers after one or more Assist satellites start listening for a command from someone.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
@@ -174,7 +174,7 @@
|
||||
"name": "Satellite started listening"
|
||||
},
|
||||
"processing": {
|
||||
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
|
||||
"description": "Triggers after one or more Assist satellites start processing a command after having heard it.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
@@ -183,7 +183,7 @@
|
||||
"name": "Satellite started processing"
|
||||
},
|
||||
"responding": {
|
||||
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
|
||||
"description": "Triggers after one or more Assist satellites start responding to a command after having processed it, or start announcing something.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
|
||||
@@ -239,7 +239,7 @@
|
||||
"message": "Provided humidity {humidity} is not valid. Accepted range is {min_humidity} to {max_humidity}."
|
||||
},
|
||||
"low_temp_higher_than_high_temp": {
|
||||
"message": "'Lower target temperature' can not be higher than 'Upper target temperature'."
|
||||
"message": "'Lower target temperature' cannot be higher than 'Upper target temperature'."
|
||||
},
|
||||
"missing_target_temperature_entity_feature": {
|
||||
"message": "Set temperature action was used with the 'Target temperature' parameter but the entity does not support it."
|
||||
|
||||
@@ -21,6 +21,7 @@ from .const import ( # noqa: F401
|
||||
ATTR_DEV_ID,
|
||||
ATTR_GPS,
|
||||
ATTR_HOST_NAME,
|
||||
ATTR_IN_ZONES,
|
||||
ATTR_IP,
|
||||
ATTR_LOCATION_NAME,
|
||||
ATTR_MAC,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import final
|
||||
from typing import Any, final
|
||||
|
||||
from propcache.api import cached_property
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
STATE_NOT_HOME,
|
||||
EntityCategory,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import (
|
||||
DeviceInfo,
|
||||
@@ -33,6 +33,7 @@ from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
ATTR_HOST_NAME,
|
||||
ATTR_IN_ZONES,
|
||||
ATTR_IP,
|
||||
ATTR_MAC,
|
||||
ATTR_SOURCE_TYPE,
|
||||
@@ -223,6 +224,9 @@ class TrackerEntity(
|
||||
_attr_longitude: float | None = None
|
||||
_attr_source_type: SourceType = SourceType.GPS
|
||||
|
||||
__active_zone: State | None = None
|
||||
__in_zones: list[str] | None = None
|
||||
|
||||
@cached_property
|
||||
def should_poll(self) -> bool:
|
||||
"""No polling for entities that have location pushed."""
|
||||
@@ -256,6 +260,18 @@ class TrackerEntity(
|
||||
"""Return longitude value of the device."""
|
||||
return self._attr_longitude
|
||||
|
||||
@callback
|
||||
def _async_write_ha_state(self) -> None:
|
||||
"""Calculate active zones."""
|
||||
if self.available and self.latitude is not None and self.longitude is not None:
|
||||
self.__active_zone, self.__in_zones = zone.async_in_zones(
|
||||
self.hass, self.latitude, self.longitude, self.location_accuracy
|
||||
)
|
||||
else:
|
||||
self.__active_zone = None
|
||||
self.__in_zones = None
|
||||
super()._async_write_ha_state()
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the device."""
|
||||
@@ -263,9 +279,7 @@ class TrackerEntity(
|
||||
return self.location_name
|
||||
|
||||
if self.latitude is not None and self.longitude is not None:
|
||||
zone_state = zone.async_active_zone(
|
||||
self.hass, self.latitude, self.longitude, self.location_accuracy
|
||||
)
|
||||
zone_state = self.__active_zone
|
||||
if zone_state is None:
|
||||
state = STATE_NOT_HOME
|
||||
elif zone_state.entity_id == zone.ENTITY_ID_HOME:
|
||||
@@ -278,12 +292,13 @@ class TrackerEntity(
|
||||
|
||||
@final
|
||||
@property
|
||||
def state_attributes(self) -> dict[str, StateType]:
|
||||
def state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the device state attributes."""
|
||||
attr: dict[str, StateType] = {}
|
||||
attr: dict[str, Any] = {ATTR_IN_ZONES: []}
|
||||
attr.update(super().state_attributes)
|
||||
|
||||
if self.latitude is not None and self.longitude is not None:
|
||||
attr[ATTR_IN_ZONES] = self.__in_zones or []
|
||||
attr[ATTR_LATITUDE] = self.latitude
|
||||
attr[ATTR_LONGITUDE] = self.longitude
|
||||
attr[ATTR_GPS_ACCURACY] = self.location_accuracy
|
||||
|
||||
@@ -43,6 +43,7 @@ ATTR_BATTERY: Final = "battery"
|
||||
ATTR_DEV_ID: Final = "dev_id"
|
||||
ATTR_GPS: Final = "gps"
|
||||
ATTR_HOST_NAME: Final = "host_name"
|
||||
ATTR_IN_ZONES: Final = "in_zones"
|
||||
ATTR_LOCATION_NAME: Final = "location_name"
|
||||
ATTR_MAC: Final = "mac"
|
||||
ATTR_SOURCE_TYPE: Final = "source_type"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"invalid_identifier": "[%key:component::dwd_weather_warnings::config::error::invalid_identifier%]"
|
||||
},
|
||||
"error": {
|
||||
"ambiguous_identifier": "The region identifier and device tracker can not be specified together.",
|
||||
"ambiguous_identifier": "The region identifier and device tracker cannot be specified together.",
|
||||
"attribute_not_found": "The required attributes 'Latitude' and 'Longitude' were not found in the specified device tracker.",
|
||||
"entity_not_found": "The specified device tracker entity was not found.",
|
||||
"invalid_identifier": "The specified region identifier / device tracker is invalid.",
|
||||
|
||||
@@ -24,11 +24,10 @@ class EcowittEntity(Entity):
|
||||
|
||||
self._attr_unique_id = f"{sensor.station.key}-{sensor.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, sensor.station.key),
|
||||
},
|
||||
identifiers={(DOMAIN, sensor.station.key)},
|
||||
name=sensor.station.model,
|
||||
model=sensor.station.model,
|
||||
manufacturer="Ecowitt",
|
||||
sw_version=sensor.station.version,
|
||||
)
|
||||
|
||||
|
||||
@@ -29,9 +29,11 @@ VALID_NAME_PATTERN = re.compile(r"^(?![\d\s])[\w\d \.]*[\w\d]$")
|
||||
|
||||
|
||||
class ConfigFlowEkeyApi(ekey_bionyxpy.AbstractAuth):
|
||||
"""ekey bionyx authentication before a ConfigEntry exists.
|
||||
"""Authentication implementation used during config flow, without refresh.
|
||||
|
||||
This implementation directly provides the token without supporting refresh.
|
||||
This exists to allow the config flow to use the API before it has fully
|
||||
created a config entry required by OAuth2Session. This does not support
|
||||
refreshing tokens, which is fine since it should have been just created.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -10,9 +10,11 @@ from requests.exceptions import RequestException
|
||||
from homeassistant.components.image import ImageEntity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from .const import DOMAIN, Platform
|
||||
from .coordinator import AvmWrapper, FritzConfigEntry
|
||||
from .entity import FritzBoxBaseEntity
|
||||
|
||||
@@ -22,6 +24,32 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def _migrate_to_new_unique_id(
|
||||
hass: HomeAssistant, avm_wrapper: AvmWrapper, ssid: str
|
||||
) -> None:
|
||||
"""Migrate old unique id to new unique id."""
|
||||
|
||||
old_unique_id = slugify(f"{avm_wrapper.unique_id}-{ssid}-qr-code")
|
||||
new_unique_id = f"{avm_wrapper.unique_id}-guest_wifi_qr_code"
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.IMAGE,
|
||||
DOMAIN,
|
||||
old_unique_id,
|
||||
)
|
||||
|
||||
if entity_id is None:
|
||||
return
|
||||
|
||||
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
_LOGGER.debug(
|
||||
"Migrating guest Wi-Fi image unique_id from [%s] to [%s]",
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: FritzConfigEntry,
|
||||
@@ -34,6 +62,8 @@ async def async_setup_entry(
|
||||
avm_wrapper.fritz_guest_wifi.get_info
|
||||
)
|
||||
|
||||
await _migrate_to_new_unique_id(hass, avm_wrapper, guest_wifi_info["NewSSID"])
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
FritzGuestWifiQRImage(
|
||||
@@ -60,7 +90,7 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity):
|
||||
) -> None:
|
||||
"""Initialize the image entity."""
|
||||
self._attr_name = ssid
|
||||
self._attr_unique_id = slugify(f"{avm_wrapper.unique_id}-{ssid}-qr-code")
|
||||
self._attr_unique_id = f"{avm_wrapper.unique_id}-guest_wifi_qr_code"
|
||||
self._current_qr_bytes: bytes | None = None
|
||||
super().__init__(avm_wrapper, device_friendly_name)
|
||||
ImageEntity.__init__(self, hass)
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260325.5"]
|
||||
"requirements": ["home-assistant-frontend==20260325.6"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.84", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.93", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -69,6 +69,10 @@ class HydrawiseEntity(CoordinatorEntity[HydrawiseDataUpdateCoordinator]):
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Get the latest data and updates the state."""
|
||||
# Guard against updates arriving after the controller has been removed
|
||||
# but before the entity has been unsubscribed from the coordinator.
|
||||
if self.controller.id not in self.coordinator.data.controllers:
|
||||
return
|
||||
self.controller = self.coordinator.data.controllers[self.controller.id]
|
||||
self._update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["hyponcloud==0.9.0"]
|
||||
"requirements": ["hyponcloud==0.9.3"]
|
||||
}
|
||||
|
||||
@@ -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.5"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "schemas-upnp-org:device:Basic:1",
|
||||
|
||||
@@ -2,111 +2,29 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_DATA,
|
||||
PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA,
|
||||
BaseNotificationService,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.components.notify import PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
DOMAIN = "lannouncer"
|
||||
|
||||
ATTR_METHOD = "method"
|
||||
ATTR_METHOD_DEFAULT = "speak"
|
||||
ATTR_METHOD_ALLOWED = ["speak", "alarm"]
|
||||
|
||||
DEFAULT_PORT = 1035
|
||||
|
||||
PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
}
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
def get_service(
|
||||
async def async_get_service(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> LannouncerNotificationService:
|
||||
) -> None:
|
||||
"""Get the Lannouncer notification service."""
|
||||
|
||||
@callback
|
||||
def _async_create_issue() -> None:
|
||||
"""Create issue for removed integration."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"integration_removed",
|
||||
is_fixable=False,
|
||||
breaks_in_ha_version="2026.3.0",
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="integration_removed",
|
||||
)
|
||||
|
||||
hass.add_job(_async_create_issue)
|
||||
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
return LannouncerNotificationService(hass, host, port)
|
||||
|
||||
|
||||
class LannouncerNotificationService(BaseNotificationService):
|
||||
"""Implementation of a notification service for Lannouncer."""
|
||||
|
||||
def __init__(self, hass, host, port):
|
||||
"""Initialize the service."""
|
||||
self._hass = hass
|
||||
self._host = host
|
||||
self._port = port
|
||||
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to Lannouncer."""
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
if data is not None and ATTR_METHOD in data:
|
||||
method = data.get(ATTR_METHOD)
|
||||
else:
|
||||
method = ATTR_METHOD_DEFAULT
|
||||
|
||||
if method not in ATTR_METHOD_ALLOWED:
|
||||
_LOGGER.error("Unknown method %s", method)
|
||||
return
|
||||
|
||||
cmd = urlencode({method: message})
|
||||
|
||||
try:
|
||||
# Open socket
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(10)
|
||||
sock.connect((self._host, self._port))
|
||||
|
||||
# Send message
|
||||
_LOGGER.debug("Sending message: %s", cmd)
|
||||
sock.sendall(cmd.encode())
|
||||
sock.sendall(b"&@DONE@\n")
|
||||
|
||||
# Check response
|
||||
buffer = sock.recv(1024)
|
||||
if buffer != b"LANnouncer: OK":
|
||||
_LOGGER.error("Error sending data to Lannnouncer: %s", buffer.decode())
|
||||
|
||||
# Close socket
|
||||
sock.close()
|
||||
except socket.gaierror:
|
||||
_LOGGER.error("Unable to connect to host %s", self._host)
|
||||
except OSError:
|
||||
_LOGGER.exception("Failed to send data to Lannnouncer")
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="integration_removed",
|
||||
)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"description": "The LANnouncer Android app is no longer available, so this integration has been deprecated and will be removed in a future release.\n\nTo resolve this issue:\n1. Remove the LANnouncer integration from your `configuration.yaml`.\n2. Restart the Home Assistant instance.\n\nAfter removal, this issue will disappear.",
|
||||
"title": "LANnouncer integration is deprecated"
|
||||
"description": "The LANnouncer integration has been removed from Home Assistant because the LANnouncer Android app is no longer available.\n\nTo resolve this issue:\n1. Remove the LANnouncer integration from your `configuration.yaml`.\n2. Restart the Home Assistant instance.\n\nAfter removal, this issue will disappear.",
|
||||
"title": "LANnouncer integration has been removed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ SUPPORT_DRY_MODE_DEVICES: set[tuple[int, int]] = {
|
||||
(0x1209, 0x8027),
|
||||
(0x1209, 0x8028),
|
||||
(0x1209, 0x8029),
|
||||
(0x138C, 0x0101),
|
||||
}
|
||||
|
||||
SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
|
||||
@@ -172,6 +173,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
|
||||
(0x1209, 0x8028),
|
||||
(0x1209, 0x8029),
|
||||
(0x131A, 0x1000),
|
||||
(0x138C, 0x0101),
|
||||
}
|
||||
|
||||
SystemModeEnum = clusters.Thermostat.Enums.SystemModeEnum
|
||||
|
||||
@@ -323,7 +323,11 @@ DISCOVERY_SCHEMAS = [
|
||||
required_attributes=(
|
||||
clusters.FanControl.Attributes.FanMode,
|
||||
clusters.FanControl.Attributes.PercentCurrent,
|
||||
clusters.FanControl.Attributes.PercentSetting,
|
||||
),
|
||||
# PercentSetting SHALL be null when FanMode is Auto (spec 4.4.6.3),
|
||||
# so allow null values to not block discovery in that state.
|
||||
allow_none_value=True,
|
||||
optional_attributes=(
|
||||
clusters.FanControl.Attributes.SpeedSetting,
|
||||
clusters.FanControl.Attributes.RockSetting,
|
||||
|
||||
@@ -399,6 +399,47 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
required_attributes=(clusters.OccupancySensing.Attributes.HoldTime,),
|
||||
# HoldTime is shared by PIR-specific numbers as a required attribute.
|
||||
# Keep discovery open so this generic schema does not block them.
|
||||
allow_multi=True,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
key="OccupancySensingPIRUnoccupiedToOccupiedDelay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="detection_delay",
|
||||
native_max_value=65534,
|
||||
native_min_value=0,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
required_attributes=(
|
||||
clusters.OccupancySensing.Attributes.PIRUnoccupiedToOccupiedDelay,
|
||||
# This attribute is mandatory when the PIRUnoccupiedToOccupiedDelay is present
|
||||
clusters.OccupancySensing.Attributes.HoldTime,
|
||||
),
|
||||
featuremap_contains=clusters.OccupancySensing.Bitmaps.Feature.kPassiveInfrared,
|
||||
allow_multi=True,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
key="OccupancySensingPIRUnoccupiedToOccupiedThreshold",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="detection_threshold",
|
||||
native_max_value=254,
|
||||
native_min_value=1,
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
required_attributes=(
|
||||
clusters.OccupancySensing.Attributes.PIRUnoccupiedToOccupiedThreshold,
|
||||
clusters.OccupancySensing.Attributes.HoldTime,
|
||||
),
|
||||
featuremap_contains=clusters.OccupancySensing.Bitmaps.Feature.kPassiveInfrared,
|
||||
allow_multi=True,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
|
||||
@@ -223,6 +223,12 @@
|
||||
"cook_time": {
|
||||
"name": "Cooking time"
|
||||
},
|
||||
"detection_delay": {
|
||||
"name": "Detection delay"
|
||||
},
|
||||
"detection_threshold": {
|
||||
"name": "Detection threshold"
|
||||
},
|
||||
"hold_time": {
|
||||
"name": "Hold time"
|
||||
},
|
||||
|
||||
@@ -168,10 +168,15 @@ class MatterWaterHeater(MatterEntity, WaterHeaterEntity):
|
||||
self._attr_target_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
|
||||
)
|
||||
system_mode = self.get_matter_attribute_value(
|
||||
clusters.Thermostat.Attributes.SystemMode
|
||||
)
|
||||
boost_state = self.get_matter_attribute_value(
|
||||
clusters.WaterHeaterManagement.Attributes.BoostState
|
||||
)
|
||||
if boost_state == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive:
|
||||
if system_mode == clusters.Thermostat.Enums.SystemModeEnum.kOff:
|
||||
self._attr_current_operation = STATE_OFF
|
||||
elif boost_state == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive:
|
||||
self._attr_current_operation = STATE_HIGH_DEMAND
|
||||
else:
|
||||
self._attr_current_operation = STATE_ECO
|
||||
@@ -218,6 +223,7 @@ DISCOVERY_SCHEMAS = [
|
||||
clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit,
|
||||
clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit,
|
||||
clusters.Thermostat.Attributes.LocalTemperature,
|
||||
clusters.Thermostat.Attributes.SystemMode,
|
||||
clusters.WaterHeaterManagement.Attributes.FeatureMap,
|
||||
),
|
||||
optional_attributes=(
|
||||
|
||||
@@ -260,7 +260,7 @@
|
||||
},
|
||||
"clear_playlist": {
|
||||
"description": "Removes all items from a media player's playlist.",
|
||||
"name": "Clear playlist"
|
||||
"name": "Clear media player playlist"
|
||||
},
|
||||
"join": {
|
||||
"description": "Groups media players together for synchronous playback. Only works on supported multiroom audio systems.",
|
||||
@@ -270,44 +270,44 @@
|
||||
"name": "Group members"
|
||||
}
|
||||
},
|
||||
"name": "Join"
|
||||
"name": "Join media players"
|
||||
},
|
||||
"media_next_track": {
|
||||
"description": "Selects the next track.",
|
||||
"name": "Next"
|
||||
"description": "Selects the next track on a media player.",
|
||||
"name": "Next track"
|
||||
},
|
||||
"media_pause": {
|
||||
"description": "Pauses playback on a media player.",
|
||||
"name": "[%key:common::action::pause%]"
|
||||
"name": "Pause media"
|
||||
},
|
||||
"media_play": {
|
||||
"description": "Starts playback on a media player.",
|
||||
"name": "Play"
|
||||
"name": "Play media"
|
||||
},
|
||||
"media_play_pause": {
|
||||
"description": "Toggles play/pause on a media player.",
|
||||
"name": "Play/Pause"
|
||||
"name": "Play/Pause media"
|
||||
},
|
||||
"media_previous_track": {
|
||||
"description": "Selects the previous track.",
|
||||
"name": "Previous"
|
||||
"description": "Selects the previous track on a media player.",
|
||||
"name": "Previous track"
|
||||
},
|
||||
"media_seek": {
|
||||
"description": "Allows you to go to a different part of the media that is currently playing.",
|
||||
"description": "Allows you to go to a different part of the media that is currently playing on a media player.",
|
||||
"fields": {
|
||||
"seek_position": {
|
||||
"description": "Target position in the currently playing media. The format is platform dependent.",
|
||||
"name": "Position"
|
||||
}
|
||||
},
|
||||
"name": "Seek"
|
||||
"name": "Seek media"
|
||||
},
|
||||
"media_stop": {
|
||||
"description": "Stops playback on a media player.",
|
||||
"name": "[%key:common::action::stop%]"
|
||||
"name": "Stop media"
|
||||
},
|
||||
"play_media": {
|
||||
"description": "Starts playing specified media.",
|
||||
"description": "Starts playing specified media on a media player.",
|
||||
"fields": {
|
||||
"announce": {
|
||||
"description": "If the media should be played as an announcement.",
|
||||
@@ -325,14 +325,14 @@
|
||||
"name": "Play media"
|
||||
},
|
||||
"repeat_set": {
|
||||
"description": "Sets the repeat mode.",
|
||||
"description": "Sets the repeat mode of a media player.",
|
||||
"fields": {
|
||||
"repeat": {
|
||||
"description": "Whether the media (one or all) should be played in a loop or not.",
|
||||
"name": "Repeat mode"
|
||||
}
|
||||
},
|
||||
"name": "Set repeat"
|
||||
"name": "Set media player repeat"
|
||||
},
|
||||
"search_media": {
|
||||
"description": "Searches the available media.",
|
||||
@@ -357,14 +357,14 @@
|
||||
"name": "Search media"
|
||||
},
|
||||
"select_sound_mode": {
|
||||
"description": "Selects a specific sound mode.",
|
||||
"description": "Selects a specific sound mode of a media player.",
|
||||
"fields": {
|
||||
"sound_mode": {
|
||||
"description": "Name of the sound mode to switch to.",
|
||||
"name": "Sound mode"
|
||||
}
|
||||
},
|
||||
"name": "Select sound mode"
|
||||
"name": "Select media player sound mode"
|
||||
},
|
||||
"select_source": {
|
||||
"description": "Sends a media player the command to change the input source.",
|
||||
@@ -374,37 +374,37 @@
|
||||
"name": "Source"
|
||||
}
|
||||
},
|
||||
"name": "Select source"
|
||||
"name": "Select media player source"
|
||||
},
|
||||
"shuffle_set": {
|
||||
"description": "Enables or disables the shuffle mode.",
|
||||
"description": "Enables or disables the shuffle mode of a media player.",
|
||||
"fields": {
|
||||
"shuffle": {
|
||||
"description": "Whether the media should be played in randomized order or not.",
|
||||
"name": "Shuffle mode"
|
||||
}
|
||||
},
|
||||
"name": "Set shuffle"
|
||||
"name": "Set media player shuffle"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Toggles a media player on/off.",
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
"name": "Toggle media player"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Turns off the power of a media player.",
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
"name": "Turn off media player"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Turns on the power of a media player.",
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
"name": "Turn on media player"
|
||||
},
|
||||
"unjoin": {
|
||||
"description": "Removes a media player from a group. Only works on platforms which support player groups.",
|
||||
"name": "Unjoin"
|
||||
"name": "Unjoin media player"
|
||||
},
|
||||
"volume_down": {
|
||||
"description": "Turns down the volume of a media player.",
|
||||
"name": "Turn down volume"
|
||||
"name": "Turn down media player volume"
|
||||
},
|
||||
"volume_mute": {
|
||||
"description": "Mutes or unmutes a media player.",
|
||||
@@ -414,7 +414,7 @@
|
||||
"name": "Muted"
|
||||
}
|
||||
},
|
||||
"name": "Mute/unmute volume"
|
||||
"name": "Mute/unmute media player"
|
||||
},
|
||||
"volume_set": {
|
||||
"description": "Sets the volume level of a media player.",
|
||||
@@ -424,11 +424,11 @@
|
||||
"name": "Level"
|
||||
}
|
||||
},
|
||||
"name": "Set volume"
|
||||
"name": "Set media player volume"
|
||||
},
|
||||
"volume_up": {
|
||||
"description": "Turns up the volume of a media player.",
|
||||
"name": "Turn up volume"
|
||||
"name": "Turn up media player volume"
|
||||
}
|
||||
},
|
||||
"title": "Media player",
|
||||
|
||||
@@ -2,32 +2,26 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MutesyncUpdateCoordinator
|
||||
from .coordinator import MutesyncConfigEntry, MutesyncUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MutesyncConfigEntry) -> bool:
|
||||
"""Set up mütesync from a config entry."""
|
||||
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
|
||||
MutesyncUpdateCoordinator(hass, entry)
|
||||
)
|
||||
coordinator = MutesyncUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MutesyncConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"""mütesync binary sensor entities."""
|
||||
|
||||
from homeassistant.components.binary_sensor import 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 DOMAIN
|
||||
from .coordinator import MutesyncUpdateCoordinator
|
||||
from .coordinator import MutesyncConfigEntry, MutesyncUpdateCoordinator
|
||||
|
||||
SENSORS = (
|
||||
"in_meeting",
|
||||
@@ -18,11 +17,11 @@ SENSORS = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: MutesyncConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the mütesync button."""
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
"""Set up the mütesync binary sensors."""
|
||||
coordinator = config_entry.runtime_data
|
||||
async_add_entities(
|
||||
[MuteStatus(coordinator, sensor_type) for sensor_type in SENSORS], True
|
||||
)
|
||||
|
||||
@@ -15,18 +15,20 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
from .const import DOMAIN, UPDATE_INTERVAL_IN_MEETING, UPDATE_INTERVAL_NOT_IN_MEETING
|
||||
|
||||
type MutesyncConfigEntry = ConfigEntry[MutesyncUpdateCoordinator]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MutesyncUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Coordinator for the mütesync integration."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: MutesyncConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MutesyncConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
|
||||
@@ -7,7 +7,6 @@ from nibe.connection.modbus import Modbus
|
||||
from nibe.connection.nibegw import NibeGW, ProductInfo
|
||||
from nibe.heatpump import HeatPump, Model
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_MODEL,
|
||||
@@ -30,7 +29,7 @@ from .const import (
|
||||
CONF_WORD_SWAP,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import CoilCoordinator
|
||||
from .coordinator import CoilCoordinator, NibeHeatpumpConfigEntry
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -45,7 +44,9 @@ PLATFORMS: list[Platform] = [
|
||||
COIL_READ_RETRIES = 5
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: NibeHeatpumpConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Nibe Heat Pump from a config entry."""
|
||||
|
||||
heatpump = HeatPump(Model[entry.data[CONF_MODEL]])
|
||||
@@ -83,8 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
coordinator = CoilCoordinator(hass, entry, heatpump, connection)
|
||||
|
||||
data = hass.data.setdefault(DOMAIN, {})
|
||||
data[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
reg = dr.async_get(hass)
|
||||
device_entry = reg.async_get_or_create(
|
||||
@@ -113,9 +113,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: NibeHeatpumpConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -5,24 +5,22 @@ from __future__ import annotations
|
||||
from nibe.coil import Coil, CoilData
|
||||
|
||||
from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CoilCoordinator
|
||||
from .coordinator import CoilCoordinator, NibeHeatpumpConfigEntry
|
||||
from .entity import CoilEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: NibeHeatpumpConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up platform."""
|
||||
|
||||
coordinator: CoilCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
BinarySensor(coordinator, coil)
|
||||
|
||||
@@ -6,24 +6,23 @@ from nibe.coil_groups import UNIT_COILGROUPS, UnitCoilGroup
|
||||
from nibe.exceptions import CoilNotFoundException
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .coordinator import CoilCoordinator
|
||||
from .const import LOGGER
|
||||
from .coordinator import CoilCoordinator, NibeHeatpumpConfigEntry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: NibeHeatpumpConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up platform."""
|
||||
|
||||
coordinator: CoilCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
def reset_buttons():
|
||||
if unit := UNIT_COILGROUPS.get(coordinator.series, {}).get("main"):
|
||||
|
||||
@@ -24,31 +24,29 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
VALUES_COOL_WITH_ROOM_SENSOR_OFF,
|
||||
VALUES_MIXING_VALVE_CLOSED_STATE,
|
||||
VALUES_PRIORITY_COOLING,
|
||||
VALUES_PRIORITY_HEATING,
|
||||
)
|
||||
from .coordinator import CoilCoordinator
|
||||
from .coordinator import CoilCoordinator, NibeHeatpumpConfigEntry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: NibeHeatpumpConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up platform."""
|
||||
|
||||
coordinator: CoilCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
main_unit = UNIT_COILGROUPS[coordinator.series]["main"]
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
type NibeHeatpumpConfigEntry = ConfigEntry[CoilCoordinator]
|
||||
|
||||
|
||||
class ContextCoordinator[_DataTypeT, _ContextTypeT](DataUpdateCoordinator[_DataTypeT]):
|
||||
"""Update coordinator with context adjustments."""
|
||||
@@ -73,12 +75,12 @@ class ContextCoordinator[_DataTypeT, _ContextTypeT](DataUpdateCoordinator[_DataT
|
||||
class CoilCoordinator(ContextCoordinator[dict[int, CoilData], int]):
|
||||
"""Update coordinator for nibe heat pumps."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: NibeHeatpumpConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: NibeHeatpumpConfigEntry,
|
||||
heatpump: HeatPump,
|
||||
connection: Connection,
|
||||
) -> None:
|
||||
|
||||
@@ -5,24 +5,22 @@ from __future__ import annotations
|
||||
from nibe.coil import Coil, CoilData
|
||||
|
||||
from homeassistant.components.number import ENTITY_ID_FORMAT, NumberEntity, NumberMode
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CoilCoordinator
|
||||
from .coordinator import CoilCoordinator, NibeHeatpumpConfigEntry
|
||||
from .entity import CoilEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: NibeHeatpumpConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up platform."""
|
||||
|
||||
coordinator: CoilCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
Number(coordinator, coil)
|
||||
|
||||
@@ -5,24 +5,22 @@ from __future__ import annotations
|
||||
from nibe.coil import Coil, CoilData
|
||||
|
||||
from homeassistant.components.select import ENTITY_ID_FORMAT, SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CoilCoordinator
|
||||
from .coordinator import CoilCoordinator, NibeHeatpumpConfigEntry
|
||||
from .entity import CoilEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: NibeHeatpumpConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up platform."""
|
||||
|
||||
coordinator: CoilCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
Select(coordinator, coil)
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
@@ -28,8 +27,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CoilCoordinator
|
||||
from .coordinator import CoilCoordinator, NibeHeatpumpConfigEntry
|
||||
from .entity import CoilEntity
|
||||
|
||||
UNIT_DESCRIPTIONS = {
|
||||
@@ -185,12 +183,12 @@ UNIT_DESCRIPTIONS = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: NibeHeatpumpConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up platform."""
|
||||
|
||||
coordinator: CoilCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
Sensor(coordinator, coil, UNIT_DESCRIPTIONS.get(coil.unit))
|
||||
|
||||
@@ -7,24 +7,22 @@ from typing import Any
|
||||
from nibe.coil import Coil, CoilData
|
||||
|
||||
from homeassistant.components.switch import ENTITY_ID_FORMAT, SwitchEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CoilCoordinator
|
||||
from .coordinator import CoilCoordinator, NibeHeatpumpConfigEntry
|
||||
from .entity import CoilEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: NibeHeatpumpConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up platform."""
|
||||
|
||||
coordinator: CoilCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
Switch(coordinator, coil)
|
||||
|
||||
@@ -14,29 +14,27 @@ from homeassistant.components.water_heater import (
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
VALUES_TEMPORARY_LUX_INACTIVE,
|
||||
VALUES_TEMPORARY_LUX_ONE_TIME_INCREASE,
|
||||
)
|
||||
from .coordinator import CoilCoordinator
|
||||
from .coordinator import CoilCoordinator, NibeHeatpumpConfigEntry
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: NibeHeatpumpConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up platform."""
|
||||
|
||||
coordinator: CoilCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
def water_heaters():
|
||||
for key, group in WATER_HEATER_COILGROUPS.get(coordinator.series, ()).items():
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"same_station": "[%key:component::nmbs::config::error::same_station%]"
|
||||
},
|
||||
"error": {
|
||||
"same_station": "Departure and arrival station can not be the same."
|
||||
"same_station": "The departure and arrival station cannot be the same."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
||||
@@ -224,7 +224,7 @@ class NumberDeviceClass(StrEnum):
|
||||
FREQUENCY = "frequency"
|
||||
"""Frequency.
|
||||
|
||||
Unit of measurement: `Hz`, `kHz`, `MHz`, `GHz`
|
||||
Unit of measurement: `mHz`, `Hz`, `kHz`, `MHz`, `GHz`
|
||||
"""
|
||||
|
||||
GAS = "gas"
|
||||
|
||||
@@ -8,6 +8,8 @@ from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from opendisplay import (
|
||||
AuthenticationFailedError,
|
||||
AuthenticationRequiredError,
|
||||
BLEConnectionError,
|
||||
BLETimeoutError,
|
||||
GlobalConfig,
|
||||
@@ -19,7 +21,7 @@ from homeassistant.components.bluetooth import async_ble_device_from_address
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -27,7 +29,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
if TYPE_CHECKING:
|
||||
from opendisplay.models import FirmwareVersion
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_ENCRYPTION_KEY, DOMAIN
|
||||
from .coordinator import OpenDisplayCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
@@ -51,6 +53,23 @@ class OpenDisplayRuntimeData:
|
||||
type OpenDisplayConfigEntry = ConfigEntry[OpenDisplayRuntimeData]
|
||||
|
||||
|
||||
def _get_encryption_key(entry: OpenDisplayConfigEntry) -> bytes | None:
|
||||
"""Return the encryption key bytes from entry data, or None."""
|
||||
raw = entry.data.get(CONF_ENCRYPTION_KEY)
|
||||
if raw is None:
|
||||
return None
|
||||
if len(raw) != 32:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Stored OpenDisplay encryption key is invalid; reauthentication required"
|
||||
)
|
||||
try:
|
||||
return bytes.fromhex(raw)
|
||||
except ValueError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Stored OpenDisplay encryption key is invalid; reauthentication required"
|
||||
) from err
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the OpenDisplay integration."""
|
||||
async_setup_services(hass)
|
||||
@@ -69,12 +88,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: OpenDisplayConfigEntry)
|
||||
f"Could not find OpenDisplay device with address {address}"
|
||||
)
|
||||
|
||||
encryption_key = _get_encryption_key(entry)
|
||||
|
||||
try:
|
||||
async with OpenDisplayDevice(
|
||||
mac_address=address, ble_device=ble_device
|
||||
mac_address=address, ble_device=ble_device, encryption_key=encryption_key
|
||||
) as device:
|
||||
fw = await device.read_firmware_version()
|
||||
is_flex = device.is_flex
|
||||
except (AuthenticationFailedError, AuthenticationRequiredError) as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Encryption key rejected by OpenDisplay device: {err}"
|
||||
) from err
|
||||
except (BLEConnectionError, BLETimeoutError, OpenDisplayError) as err:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Failed to connect to OpenDisplay device: {err}"
|
||||
|
||||
@@ -2,11 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from opendisplay import (
|
||||
MANUFACTURER_ID,
|
||||
AuthenticationFailedError,
|
||||
AuthenticationRequiredError,
|
||||
BLEConnectionError,
|
||||
OpenDisplayDevice,
|
||||
OpenDisplayError,
|
||||
@@ -21,11 +24,14 @@ from homeassistant.components.bluetooth import (
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_ENCRYPTION_KEY, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_ENCRYPTION_KEY_VALIDATOR = vol.All(str.strip, str.lower, vol.Match(r"^[0-9a-f]{32}$"))
|
||||
|
||||
|
||||
class OpenDisplayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for OpenDisplay."""
|
||||
|
||||
@@ -34,14 +40,16 @@ class OpenDisplayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._discovery_info: BluetoothServiceInfoBleak | None = None
|
||||
self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
|
||||
|
||||
async def _async_test_connection(self, address: str) -> None:
|
||||
async def _async_test_connection(
|
||||
self, address: str, encryption_key: bytes | None = None
|
||||
) -> None:
|
||||
"""Connect to the device and verify it responds."""
|
||||
ble_device = async_ble_device_from_address(self.hass, address, connectable=True)
|
||||
if ble_device is None:
|
||||
raise BLEConnectionError(f"Could not find connectable device for {address}")
|
||||
|
||||
async with OpenDisplayDevice(
|
||||
mac_address=address, ble_device=ble_device
|
||||
mac_address=address, ble_device=ble_device, encryption_key=encryption_key
|
||||
) as device:
|
||||
await device.read_firmware_version()
|
||||
|
||||
@@ -56,6 +64,8 @@ class OpenDisplayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
try:
|
||||
await self._async_test_connection(discovery_info.address)
|
||||
except AuthenticationRequiredError:
|
||||
return await self.async_step_encryption_key()
|
||||
except OpenDisplayError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except Exception:
|
||||
@@ -92,6 +102,11 @@ class OpenDisplayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
try:
|
||||
await self._async_test_connection(address)
|
||||
except AuthenticationRequiredError:
|
||||
self.context["title_placeholders"] = {
|
||||
"name": self._discovered_devices[address].name
|
||||
}
|
||||
return await self.async_step_encryption_key()
|
||||
except OpenDisplayError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
@@ -128,3 +143,100 @@ class OpenDisplayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _async_try_connection(
|
||||
self,
|
||||
address: str,
|
||||
encryption_key: bytes | None,
|
||||
errors: dict[str, str],
|
||||
) -> bool:
|
||||
"""Test connection, populate errors, and return True on success."""
|
||||
try:
|
||||
await self._async_test_connection(address, encryption_key)
|
||||
except AuthenticationFailedError, AuthenticationRequiredError:
|
||||
errors[CONF_ENCRYPTION_KEY] = "invalid_auth"
|
||||
except OpenDisplayError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def async_step_encryption_key(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the encryption key step."""
|
||||
errors: dict[str, str] = {}
|
||||
name: str = self.context["title_placeholders"]["name"]
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
key: str = _ENCRYPTION_KEY_VALIDATOR(user_input[CONF_ENCRYPTION_KEY])
|
||||
except vol.Invalid:
|
||||
errors[CONF_ENCRYPTION_KEY] = "invalid_key_format"
|
||||
else:
|
||||
if TYPE_CHECKING:
|
||||
assert self.unique_id is not None
|
||||
if await self._async_try_connection(
|
||||
self.unique_id, bytes.fromhex(key), errors
|
||||
):
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data={CONF_ENCRYPTION_KEY: key},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="encryption_key",
|
||||
data_schema=vol.Schema({vol.Required(CONF_ENCRYPTION_KEY): str}),
|
||||
description_placeholders={"name": name},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle re-authentication."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth confirmation."""
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
key: str | None = None
|
||||
if user_input[CONF_ENCRYPTION_KEY].strip():
|
||||
try:
|
||||
key = _ENCRYPTION_KEY_VALIDATOR(user_input[CONF_ENCRYPTION_KEY])
|
||||
except vol.Invalid:
|
||||
errors[CONF_ENCRYPTION_KEY] = "invalid_key_format"
|
||||
|
||||
if not errors:
|
||||
address = reauth_entry.unique_id
|
||||
if TYPE_CHECKING:
|
||||
assert address is not None
|
||||
if await self._async_try_connection(
|
||||
address, bytes.fromhex(key) if key is not None else None, errors
|
||||
):
|
||||
new_data = dict(reauth_entry.data)
|
||||
if key is not None:
|
||||
new_data[CONF_ENCRYPTION_KEY] = key
|
||||
else:
|
||||
new_data.pop(CONF_ENCRYPTION_KEY, None)
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
data=new_data,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Optional(CONF_ENCRYPTION_KEY, default=""): str}
|
||||
),
|
||||
description_placeholders={"name": reauth_entry.title},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
"""Constants for the OpenDisplay integration."""
|
||||
|
||||
DOMAIN = "opendisplay"
|
||||
CONF_ENCRYPTION_KEY = "encryption_key"
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["opendisplay"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["py-opendisplay==5.5.0"]
|
||||
"requirements": ["py-opendisplay==5.9.0"]
|
||||
}
|
||||
|
||||
@@ -33,9 +33,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: Devices do not require authentication.
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
|
||||
@@ -12,6 +12,8 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
import aiohttp
|
||||
from opendisplay import (
|
||||
AuthenticationFailedError,
|
||||
AuthenticationRequiredError,
|
||||
DitherMode,
|
||||
FitMode,
|
||||
OpenDisplayDevice,
|
||||
@@ -38,7 +40,7 @@ from homeassistant.helpers.selector import MediaSelector, MediaSelectorConfig
|
||||
if TYPE_CHECKING:
|
||||
from . import OpenDisplayConfigEntry
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_ENCRYPTION_KEY, DOMAIN
|
||||
|
||||
ATTR_IMAGE = "image"
|
||||
ATTR_ROTATION = "rotation"
|
||||
@@ -193,10 +195,25 @@ async def _async_upload_image(call: ServiceCall) -> None:
|
||||
else:
|
||||
pil_image = await _async_download_image(call.hass, media.url)
|
||||
|
||||
raw_key = entry.data.get(CONF_ENCRYPTION_KEY)
|
||||
if raw_key is not None and len(raw_key) != 32:
|
||||
entry.async_start_reauth(call.hass)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="authentication_error"
|
||||
)
|
||||
try:
|
||||
encryption_key = bytes.fromhex(raw_key) if raw_key is not None else None
|
||||
except ValueError as err:
|
||||
entry.async_start_reauth(call.hass)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="authentication_error"
|
||||
) from err
|
||||
|
||||
async with OpenDisplayDevice(
|
||||
mac_address=address,
|
||||
ble_device=ble_device,
|
||||
config=entry.runtime_data.device_config,
|
||||
encryption_key=encryption_key,
|
||||
) as device:
|
||||
await device.upload_image(
|
||||
pil_image,
|
||||
@@ -208,6 +225,11 @@ async def _async_upload_image(call: ServiceCall) -> None:
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
return
|
||||
except (AuthenticationFailedError, AuthenticationRequiredError) as err:
|
||||
entry.async_start_reauth(call.hass)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="authentication_error"
|
||||
) from err
|
||||
except OpenDisplayError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="upload_error"
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_key_format": "The encryption key must be exactly 32 hexadecimal characters (0-9, a-f).",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"flow_title": "{name}",
|
||||
@@ -16,6 +19,26 @@
|
||||
"bluetooth_confirm": {
|
||||
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
|
||||
},
|
||||
"encryption_key": {
|
||||
"data": {
|
||||
"encryption_key": "Encryption key"
|
||||
},
|
||||
"data_description": {
|
||||
"encryption_key": "Enter the 32-character hexadecimal AES-128 encryption key for this device."
|
||||
},
|
||||
"description": "{name} requires an encryption key to connect.",
|
||||
"title": "Encryption required"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"encryption_key": "[%key:component::opendisplay::config::step::encryption_key::data::encryption_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"encryption_key": "[%key:component::opendisplay::config::step::encryption_key::data_description::encryption_key%]"
|
||||
},
|
||||
"description": "Authentication failed for {name}. Enter the correct encryption key, or leave blank if encryption has been disabled on the device.",
|
||||
"title": "Re-authentication required"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"address": "[%key:common::config_flow::data::device%]"
|
||||
@@ -35,6 +58,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"authentication_error": {
|
||||
"message": "Authentication failed. Please update the encryption key."
|
||||
},
|
||||
"device_not_found": {
|
||||
"message": "Could not find Bluetooth device with address `{address}`."
|
||||
},
|
||||
|
||||
@@ -2,31 +2,28 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_BASE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import BASE_UPDATE_INTERVAL, DOMAIN, LOGGER
|
||||
from .coordinator import OpenexchangeratesCoordinator
|
||||
from .coordinator import OpenexchangeratesConfigEntry, OpenexchangeratesCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: OpenexchangeratesConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Open Exchange Rates from a config entry."""
|
||||
api_key: str = entry.data[CONF_API_KEY]
|
||||
base: str = entry.data[CONF_BASE]
|
||||
|
||||
# Create one coordinator per base currency per API key.
|
||||
existing_coordinators: dict[str, OpenexchangeratesCoordinator] = hass.data.get(
|
||||
DOMAIN, {}
|
||||
)
|
||||
existing_coordinator_for_api_key = {
|
||||
existing_coordinator
|
||||
for config_entry_id, existing_coordinator in existing_coordinators.items()
|
||||
if (config_entry := hass.config_entries.async_get_entry(config_entry_id))
|
||||
and config_entry.data[CONF_API_KEY] == api_key
|
||||
existing_entry.runtime_data
|
||||
for existing_entry in hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
if existing_entry.data[CONF_API_KEY] == api_key
|
||||
}
|
||||
|
||||
# Adjust update interval by coordinators per API key.
|
||||
@@ -48,16 +45,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: OpenexchangeratesConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -20,16 +20,18 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
from .const import CLIENT_TIMEOUT, DOMAIN, LOGGER
|
||||
|
||||
type OpenexchangeratesConfigEntry = ConfigEntry[OpenexchangeratesCoordinator]
|
||||
|
||||
|
||||
class OpenexchangeratesCoordinator(DataUpdateCoordinator[Latest]):
|
||||
"""Represent a coordinator for Open Exchange Rates API."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: OpenexchangeratesConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OpenexchangeratesConfigEntry,
|
||||
session: ClientSession,
|
||||
api_key: str,
|
||||
base: str,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_QUOTE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
@@ -11,19 +10,19 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OpenexchangeratesCoordinator
|
||||
from .coordinator import OpenexchangeratesConfigEntry, OpenexchangeratesCoordinator
|
||||
|
||||
ATTRIBUTION = "Data provided by openexchangerates.org"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OpenexchangeratesConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Open Exchange Rates sensor."""
|
||||
quote: str = config_entry.data.get(CONF_QUOTE, "EUR")
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
OpenexchangeratesSensor(
|
||||
@@ -43,7 +42,7 @@ class OpenexchangeratesSensor(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OpenexchangeratesConfigEntry,
|
||||
coordinator: OpenexchangeratesCoordinator,
|
||||
quote: str,
|
||||
enabled: bool,
|
||||
|
||||
@@ -18,6 +18,8 @@ from .services import async_setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type OpenhomeConfigEntry = ConfigEntry[Device]
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.UPDATE]
|
||||
|
||||
@@ -30,7 +32,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OpenhomeConfigEntry,
|
||||
) -> bool:
|
||||
"""Set up the configuration config entry."""
|
||||
_LOGGER.debug("Setting up config entry: %s", config_entry.unique_id)
|
||||
@@ -44,18 +46,15 @@ async def async_setup_entry(
|
||||
|
||||
_LOGGER.debug("Initialised device: %s", device.uuid())
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = device
|
||||
config_entry.runtime_data = device
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: OpenhomeConfigEntry
|
||||
) -> bool:
|
||||
"""Cleanup before removing config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
@@ -19,11 +19,11 @@ from homeassistant.components.media_player import (
|
||||
MediaType,
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import OpenhomeConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
SUPPORT_OPENHOME = (
|
||||
@@ -37,14 +37,14 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OpenhomeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Openhome config entry."""
|
||||
|
||||
_LOGGER.debug("Setting up config entry: %s", config_entry.unique_id)
|
||||
|
||||
device = hass.data[DOMAIN][config_entry.entry_id]
|
||||
device = config_entry.runtime_data
|
||||
|
||||
entity = OpenhomeDevice(device)
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@ from homeassistant.components.update import (
|
||||
UpdateEntity,
|
||||
UpdateEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import OpenhomeConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -26,14 +26,14 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OpenhomeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up update entities for Reolink component."""
|
||||
|
||||
_LOGGER.debug("Setting up config entry: %s", config_entry.unique_id)
|
||||
|
||||
device = hass.data[DOMAIN][config_entry.entry_id]
|
||||
device = config_entry.runtime_data
|
||||
|
||||
entity = OpenhomeUpdateEntity(device)
|
||||
|
||||
|
||||
@@ -7,21 +7,20 @@ import logging
|
||||
import aiohttp
|
||||
from ovoenergy import OVOEnergy
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_ACCOUNT, DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
from .coordinator import OVOEnergyDataUpdateCoordinator
|
||||
from .const import CONF_ACCOUNT
|
||||
from .coordinator import OVOEnergyConfigEntry, OVOEnergyDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: OVOEnergyConfigEntry) -> bool:
|
||||
"""Set up OVO Energy from a config entry."""
|
||||
|
||||
client = OVOEnergy(
|
||||
@@ -45,26 +44,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
coordinator = OVOEnergyDataUpdateCoordinator(hass, entry, client)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_CLIENT: client,
|
||||
DATA_COORDINATOR: coordinator,
|
||||
}
|
||||
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Setup components
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: OVOEnergyConfigEntry) -> bool:
|
||||
"""Unload OVO Energy config entry."""
|
||||
# Unload sensors
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
del hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -2,6 +2,4 @@
|
||||
|
||||
DOMAIN = "ovo_energy"
|
||||
|
||||
DATA_CLIENT = "ovo_client"
|
||||
DATA_COORDINATOR = "coordinator"
|
||||
CONF_ACCOUNT = "account"
|
||||
|
||||
@@ -21,16 +21,18 @@ from .const import CONF_ACCOUNT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type OVOEnergyConfigEntry = ConfigEntry[OVOEnergyDataUpdateCoordinator]
|
||||
|
||||
|
||||
class OVOEnergyDataUpdateCoordinator(DataUpdateCoordinator[OVODailyUsage]):
|
||||
"""Class to manage fetching OVO Energy data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: OVOEnergyConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: OVOEnergyConfigEntry,
|
||||
client: OVOEnergy,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ovoenergy import OVOEnergy
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -16,15 +14,6 @@ class OVOEnergyEntity(CoordinatorEntity[OVOEnergyDataUpdateCoordinator]):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: OVOEnergyDataUpdateCoordinator,
|
||||
client: OVOEnergy,
|
||||
) -> None:
|
||||
"""Initialize the OVO Energy entity."""
|
||||
super().__init__(coordinator)
|
||||
self._client = client
|
||||
|
||||
|
||||
class OVOEnergyDeviceEntity(OVOEnergyEntity):
|
||||
"""Defines a OVO Energy device entity."""
|
||||
@@ -34,7 +23,7 @@ class OVOEnergyDeviceEntity(OVOEnergyEntity):
|
||||
"""Return device information about this OVO Energy instance."""
|
||||
return DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, self._client.account_id)},
|
||||
identifiers={(DOMAIN, self.coordinator.client.account_id)},
|
||||
manufacturer="OVO Energy",
|
||||
name=self._client.username,
|
||||
name=self.coordinator.client.username,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ import dataclasses
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Final
|
||||
|
||||
from ovoenergy import OVOEnergy
|
||||
from ovoenergy.models import OVODailyUsage
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -16,15 +15,14 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
|
||||
from .coordinator import OVOEnergyDataUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import OVOEnergyConfigEntry, OVOEnergyDataUpdateCoordinator
|
||||
from .entity import OVOEnergyDeviceEntity
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=300)
|
||||
@@ -114,14 +112,11 @@ SENSOR_TYPES_GAS: tuple[OVOEnergySensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: OVOEnergyConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up OVO Energy sensor based on a config entry."""
|
||||
coordinator: OVOEnergyDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
|
||||
DATA_COORDINATOR
|
||||
]
|
||||
client: OVOEnergy = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entities = []
|
||||
|
||||
@@ -139,7 +134,7 @@ async def async_setup_entry(
|
||||
coordinator.data.electricity[-1].cost.currency_unit
|
||||
),
|
||||
)
|
||||
entities.append(OVOEnergySensor(coordinator, description, client))
|
||||
entities.append(OVOEnergySensor(coordinator, description))
|
||||
if coordinator.data.gas:
|
||||
for description in SENSOR_TYPES_GAS:
|
||||
if (
|
||||
@@ -153,7 +148,7 @@ async def async_setup_entry(
|
||||
-1
|
||||
].cost.currency_unit,
|
||||
)
|
||||
entities.append(OVOEnergySensor(coordinator, description, client))
|
||||
entities.append(OVOEnergySensor(coordinator, description))
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
@@ -167,11 +162,12 @@ class OVOEnergySensor(OVOEnergyDeviceEntity, SensorEntity):
|
||||
self,
|
||||
coordinator: OVOEnergyDataUpdateCoordinator,
|
||||
description: OVOEnergySensorEntityDescription,
|
||||
client: OVOEnergy,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, client)
|
||||
self._attr_unique_id = f"{DOMAIN}_{client.account_id}_{description.key}"
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = (
|
||||
f"{DOMAIN}_{coordinator.client.account_id}_{description.key}"
|
||||
)
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
ATTR_DEVICE_INFO,
|
||||
ATTR_REMOTE,
|
||||
ATTR_UDN,
|
||||
CONF_APP_ID,
|
||||
CONF_ENCRYPTION_KEY,
|
||||
@@ -29,6 +28,8 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
type PanasonicVieraConfigEntry = ConfigEntry[Remote]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@@ -68,10 +69,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: PanasonicVieraConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Panasonic Viera from a config entry."""
|
||||
panasonic_viera_data = hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
config = config_entry.data
|
||||
|
||||
host = config[CONF_HOST]
|
||||
@@ -88,7 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
remote = Remote(hass, host, port, on_action, **params)
|
||||
await remote.async_create_remote_control(during_setup=True)
|
||||
|
||||
panasonic_viera_data[config_entry.entry_id] = {ATTR_REMOTE: remote}
|
||||
config_entry.runtime_data = remote
|
||||
|
||||
# Add device_info to older config entries
|
||||
if ATTR_DEVICE_INFO not in config or config[ATTR_DEVICE_INFO] is None:
|
||||
@@ -112,15 +113,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: PanasonicVieraConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
|
||||
class Remote:
|
||||
|
||||
@@ -17,17 +17,16 @@ from homeassistant.components.media_player import (
|
||||
MediaType,
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import PanasonicVieraConfigEntry
|
||||
from .const import (
|
||||
ATTR_DEVICE_INFO,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL_NUMBER,
|
||||
ATTR_REMOTE,
|
||||
ATTR_UDN,
|
||||
DEFAULT_MANUFACTURER,
|
||||
DEFAULT_MODEL_NUMBER,
|
||||
@@ -39,14 +38,14 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PanasonicVieraConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Panasonic Viera TV from a config entry."""
|
||||
|
||||
config = config_entry.data
|
||||
|
||||
remote = hass.data[DOMAIN][config_entry.entry_id][ATTR_REMOTE]
|
||||
remote = config_entry.runtime_data
|
||||
name = config[CONF_NAME]
|
||||
device_info = config[ATTR_DEVICE_INFO]
|
||||
|
||||
|
||||
@@ -6,18 +6,16 @@ from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.remote import RemoteEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import Remote
|
||||
from . import PanasonicVieraConfigEntry, Remote
|
||||
from .const import (
|
||||
ATTR_DEVICE_INFO,
|
||||
ATTR_MANUFACTURER,
|
||||
ATTR_MODEL_NUMBER,
|
||||
ATTR_REMOTE,
|
||||
ATTR_UDN,
|
||||
DEFAULT_MANUFACTURER,
|
||||
DEFAULT_MODEL_NUMBER,
|
||||
@@ -27,14 +25,14 @@ from .const import (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PanasonicVieraConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Panasonic Viera TV Remote from a config entry."""
|
||||
|
||||
config = config_entry.data
|
||||
|
||||
remote = hass.data[DOMAIN][config_entry.entry_id][ATTR_REMOTE]
|
||||
remote = config_entry.runtime_data
|
||||
name = config[CONF_NAME]
|
||||
device_info = config[ATTR_DEVICE_INFO]
|
||||
|
||||
|
||||
@@ -4,37 +4,39 @@ from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_PHONE_NUMBER, DOMAIN
|
||||
from .coordinator import PecoOutageCoordinator, PecoSmartMeterCoordinator
|
||||
from .const import CONF_PHONE_NUMBER
|
||||
from .coordinator import (
|
||||
PecoConfigEntry,
|
||||
PecoOutageCoordinator,
|
||||
PecoRuntimeData,
|
||||
PecoSmartMeterCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PecoConfigEntry) -> bool:
|
||||
"""Set up PECO Outage Counter from a config entry."""
|
||||
outage_coordinator = PecoOutageCoordinator(hass, entry)
|
||||
await outage_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
"outage_count": outage_coordinator
|
||||
}
|
||||
|
||||
meter_coordinator: PecoSmartMeterCoordinator | None = None
|
||||
if phone_number := entry.data.get(CONF_PHONE_NUMBER):
|
||||
meter_coordinator = PecoSmartMeterCoordinator(hass, entry, phone_number)
|
||||
await meter_coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN][entry.entry_id]["smart_meter"] = meter_coordinator
|
||||
|
||||
entry.runtime_data = PecoRuntimeData(
|
||||
outage_coordinator=outage_coordinator,
|
||||
meter_coordinator=meter_coordinator,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: PecoConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -8,28 +8,23 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PecoSmartMeterCoordinator
|
||||
from .coordinator import PecoConfigEntry, PecoSmartMeterCoordinator
|
||||
|
||||
PARALLEL_UPDATES: Final = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PecoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up binary sensor for PECO."""
|
||||
if "smart_meter" not in hass.data[DOMAIN][config_entry.entry_id]:
|
||||
if (coordinator := config_entry.runtime_data.meter_coordinator) is None:
|
||||
return
|
||||
coordinator: PecoSmartMeterCoordinator = hass.data[DOMAIN][config_entry.entry_id][
|
||||
"smart_meter"
|
||||
]
|
||||
|
||||
async_add_entities(
|
||||
[PecoBinarySensor(coordinator, phone_number=config_entry.data["phone_number"])]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""DataUpdateCoordinator for the PECO Outage Counter integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -28,12 +30,23 @@ class PECOCoordinatorData:
|
||||
alerts: AlertResults
|
||||
|
||||
|
||||
@dataclass
|
||||
class PecoRuntimeData:
|
||||
"""Runtime data for the PECO integration."""
|
||||
|
||||
outage_coordinator: PecoOutageCoordinator
|
||||
meter_coordinator: PecoSmartMeterCoordinator | None = None
|
||||
|
||||
|
||||
type PecoConfigEntry = ConfigEntry[PecoRuntimeData]
|
||||
|
||||
|
||||
class PecoOutageCoordinator(DataUpdateCoordinator[PECOCoordinatorData]):
|
||||
"""Coordinator for PECO outage data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: PecoConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
def __init__(self, hass: HomeAssistant, entry: PecoConfigEntry) -> None:
|
||||
"""Initialize the outage coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
@@ -65,10 +78,10 @@ class PecoOutageCoordinator(DataUpdateCoordinator[PECOCoordinatorData]):
|
||||
class PecoSmartMeterCoordinator(DataUpdateCoordinator[bool]):
|
||||
"""Coordinator for PECO smart meter data."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: PecoConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, entry: ConfigEntry, phone_number: str
|
||||
self, hass: HomeAssistant, entry: PecoConfigEntry, phone_number: str
|
||||
) -> None:
|
||||
"""Initialize the smart meter coordinator."""
|
||||
super().__init__(
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -19,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import ATTR_CONTENT, CONF_COUNTY, DOMAIN
|
||||
from .coordinator import PECOCoordinatorData, PecoOutageCoordinator
|
||||
from .coordinator import PecoConfigEntry, PECOCoordinatorData, PecoOutageCoordinator
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -72,12 +71,12 @@ SENSOR_LIST: tuple[PECOSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PecoConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
county: str = config_entry.data[CONF_COUNTY]
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]["outage_count"]
|
||||
coordinator = config_entry.runtime_data.outage_coordinator
|
||||
|
||||
async_add_entities(
|
||||
PecoSensor(sensor, county, coordinator) for sensor in SENSOR_LIST
|
||||
|
||||
@@ -6,7 +6,6 @@ import logging
|
||||
|
||||
from mypermobil import MyPermobil, MyPermobilClientException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_CODE,
|
||||
CONF_EMAIL,
|
||||
@@ -19,15 +18,15 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import APPLICATION, DOMAIN
|
||||
from .coordinator import MyPermobilCoordinator
|
||||
from .const import APPLICATION
|
||||
from .coordinator import MyPermobilCoordinator, PermobilConfigEntry
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PermobilConfigEntry) -> bool:
|
||||
"""Set up MyPermobil from a config entry."""
|
||||
|
||||
# create the API object from the config and save it in hass
|
||||
@@ -51,15 +50,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinator = MyPermobilCoordinator(hass, entry, p_api)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: PermobilConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import Any
|
||||
|
||||
from mypermobil import BATTERY_CHARGING
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
@@ -16,8 +15,7 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MyPermobilCoordinator
|
||||
from .coordinator import PermobilConfigEntry
|
||||
from .entity import PermobilEntity
|
||||
|
||||
|
||||
@@ -41,12 +39,12 @@ BINARY_SENSOR_DESCRIPTIONS: tuple[PermobilBinarySensorEntityDescription, ...] =
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
config_entry: PermobilConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Create and setup the binary sensor."""
|
||||
|
||||
coordinator: MyPermobilCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
PermobilbinarySensor(coordinator=coordinator, description=description)
|
||||
|
||||
@@ -13,6 +13,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type PermobilConfigEntry = ConfigEntry[MyPermobilCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MyPermobilData:
|
||||
@@ -26,10 +28,10 @@ class MyPermobilData:
|
||||
class MyPermobilCoordinator(DataUpdateCoordinator[MyPermobilData]):
|
||||
"""MyPermobil coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: PermobilConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, p_api: MyPermobil
|
||||
self, hass: HomeAssistant, config_entry: PermobilConfigEntry, p_api: MyPermobil
|
||||
) -> None:
|
||||
"""Initialize my coordinator."""
|
||||
super().__init__(
|
||||
|
||||
@@ -23,7 +23,6 @@ from mypermobil import (
|
||||
USAGE_DISTANCE,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
@@ -34,8 +33,8 @@ from homeassistant.const import PERCENTAGE, UnitOfEnergy, UnitOfLength, UnitOfTi
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import BATTERY_ASSUMED_VOLTAGE, DOMAIN, KM, MILES
|
||||
from .coordinator import MyPermobilCoordinator
|
||||
from .const import BATTERY_ASSUMED_VOLTAGE, KM, MILES
|
||||
from .coordinator import PermobilConfigEntry
|
||||
from .entity import PermobilEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -176,12 +175,12 @@ DISTANCE_UNITS: dict[Any, UnitOfLength] = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: config_entries.ConfigEntry,
|
||||
config_entry: PermobilConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Create sensors from a config entry created in the integrations UI."""
|
||||
|
||||
coordinator: MyPermobilCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
PermobilSensor(coordinator=coordinator, description=description)
|
||||
|
||||
@@ -11,6 +11,7 @@ import voluptuous as vol
|
||||
from homeassistant.auth import EVENT_USER_REMOVED
|
||||
from homeassistant.components import persistent_notification, websocket_api
|
||||
from homeassistant.components.device_tracker import (
|
||||
ATTR_IN_ZONES,
|
||||
ATTR_SOURCE_TYPE,
|
||||
DOMAIN as DEVICE_TRACKER_DOMAIN,
|
||||
SourceType,
|
||||
@@ -435,6 +436,7 @@ class Person(
|
||||
self._unsub_track_device: Callable[[], None] | None = None
|
||||
self._attr_state: str | None = None
|
||||
self.device_trackers: list[str] = []
|
||||
self._in_zones: list[str] = []
|
||||
|
||||
self._attr_unique_id = config[CONF_ID]
|
||||
self._set_attrs_from_config()
|
||||
@@ -552,6 +554,7 @@ class Person(
|
||||
self._latitude = None
|
||||
self._longitude = None
|
||||
self._gps_accuracy = None
|
||||
self._in_zones = []
|
||||
|
||||
self._update_extra_state_attributes()
|
||||
self.async_write_ha_state()
|
||||
@@ -566,7 +569,8 @@ class Person(
|
||||
self._source = state.entity_id
|
||||
self._latitude = coordinates.attributes.get(ATTR_LATITUDE)
|
||||
self._longitude = coordinates.attributes.get(ATTR_LONGITUDE)
|
||||
self._gps_accuracy = state.attributes.get(ATTR_GPS_ACCURACY)
|
||||
self._gps_accuracy = coordinates.attributes.get(ATTR_GPS_ACCURACY)
|
||||
self._in_zones = coordinates.attributes.get(ATTR_IN_ZONES, [])
|
||||
|
||||
@callback
|
||||
def _update_extra_state_attributes(self) -> None:
|
||||
@@ -575,6 +579,7 @@ class Person(
|
||||
ATTR_EDITABLE: self.editable,
|
||||
ATTR_ID: self.unique_id,
|
||||
ATTR_DEVICE_TRACKERS: self.device_trackers,
|
||||
ATTR_IN_ZONES: self._in_zones,
|
||||
}
|
||||
|
||||
if self._latitude is not None:
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
|
||||
from python_picnic_api2 import PicnicAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_COUNTRY_CODE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_API, CONF_COORDINATOR, DOMAIN
|
||||
from .coordinator import PicnicUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PicnicConfigEntry, PicnicUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
@@ -24,7 +23,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def create_picnic_client(entry: ConfigEntry):
|
||||
def create_picnic_client(entry: PicnicConfigEntry):
|
||||
"""Create an instance of the PicnicAPI client."""
|
||||
return PicnicAPI(
|
||||
auth_token=entry.data.get(CONF_ACCESS_TOKEN),
|
||||
@@ -32,7 +31,7 @@ def create_picnic_client(entry: ConfigEntry):
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PicnicConfigEntry) -> bool:
|
||||
"""Set up Picnic from a config entry."""
|
||||
picnic_client = await hass.async_add_executor_job(create_picnic_client, entry)
|
||||
picnic_coordinator = PicnicUpdateCoordinator(hass, picnic_client, entry)
|
||||
@@ -40,21 +39,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
await picnic_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
CONF_API: picnic_client,
|
||||
CONF_COORDINATOR: picnic_coordinator,
|
||||
}
|
||||
entry.runtime_data = picnic_coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: PicnicConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -4,9 +4,6 @@ from __future__ import annotations
|
||||
|
||||
DOMAIN = "picnic"
|
||||
|
||||
CONF_API = "api"
|
||||
CONF_COORDINATOR = "coordinator"
|
||||
|
||||
SERVICE_ADD_PRODUCT_TO_CART = "add_product"
|
||||
|
||||
ATTR_PRODUCT_ID = "product_id"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Coordinator to fetch data from the Picnic API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
import copy
|
||||
@@ -17,17 +19,19 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
from .const import ADDRESS, CART_DATA, LAST_ORDER_DATA, NEXT_DELIVERY_DATA, SLOT_DATA
|
||||
|
||||
type PicnicConfigEntry = ConfigEntry[PicnicUpdateCoordinator]
|
||||
|
||||
|
||||
class PicnicUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""The coordinator to fetch data from the Picnic API at a set interval."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: PicnicConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
picnic_api_client: PicnicAPI,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PicnicConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the coordinator with the given Picnic API client."""
|
||||
self.picnic_api_client = picnic_api_client
|
||||
|
||||
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CURRENCY_EURO
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
@@ -23,7 +22,6 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
ATTRIBUTION,
|
||||
CONF_COORDINATOR,
|
||||
DOMAIN,
|
||||
SENSOR_CART_ITEMS_COUNT,
|
||||
SENSOR_CART_TOTAL_PRICE,
|
||||
@@ -42,7 +40,7 @@ from .const import (
|
||||
SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE,
|
||||
SENSOR_SELECTED_SLOT_START,
|
||||
)
|
||||
from .coordinator import PicnicUpdateCoordinator
|
||||
from .coordinator import PicnicConfigEntry, PicnicUpdateCoordinator
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -202,11 +200,11 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PicnicConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Picnic sensor entries."""
|
||||
picnic_coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR]
|
||||
picnic_coordinator = config_entry.runtime_data
|
||||
|
||||
# Add an entity for each sensor type
|
||||
async_add_entities(
|
||||
@@ -225,7 +223,7 @@ class PicnicSensor(SensorEntity, CoordinatorEntity[PicnicUpdateCoordinator]):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PicnicUpdateCoordinator,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PicnicConfigEntry,
|
||||
description: PicnicSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Init a Picnic sensor."""
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import cast
|
||||
from python_picnic_api2 import PicnicAPI
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -16,10 +17,10 @@ from .const import (
|
||||
ATTR_PRODUCT_ID,
|
||||
ATTR_PRODUCT_IDENTIFIERS,
|
||||
ATTR_PRODUCT_NAME,
|
||||
CONF_API,
|
||||
DOMAIN,
|
||||
SERVICE_ADD_PRODUCT_TO_CART,
|
||||
)
|
||||
from .coordinator import PicnicConfigEntry
|
||||
|
||||
|
||||
class PicnicServiceException(Exception):
|
||||
@@ -50,10 +51,14 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
async def get_api_client(hass: HomeAssistant, config_entry_id: str) -> PicnicAPI:
|
||||
"""Get the right Picnic API client based on the device id, else get the default one."""
|
||||
if config_entry_id not in hass.data[DOMAIN]:
|
||||
"""Get the right Picnic API client based on the config entry id."""
|
||||
|
||||
entry: PicnicConfigEntry | None = hass.config_entries.async_get_entry(
|
||||
config_entry_id
|
||||
)
|
||||
if entry is None or entry.state != ConfigEntryState.LOADED:
|
||||
raise ValueError(f"Config entry with id {config_entry_id} not found!")
|
||||
return hass.data[DOMAIN][config_entry_id][CONF_API]
|
||||
return entry.runtime_data.picnic_api_client
|
||||
|
||||
|
||||
async def handle_add_product(
|
||||
|
||||
@@ -11,15 +11,14 @@ from homeassistant.components.todo import (
|
||||
TodoListEntity,
|
||||
TodoListEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
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 CONF_COORDINATOR, DOMAIN
|
||||
from .coordinator import PicnicUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PicnicConfigEntry, PicnicUpdateCoordinator
|
||||
from .services import product_search
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -27,11 +26,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PicnicConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Picnic shopping cart todo platform config entry."""
|
||||
picnic_coordinator = hass.data[DOMAIN][config_entry.entry_id][CONF_COORDINATOR]
|
||||
picnic_coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities([PicnicCart(picnic_coordinator, config_entry)])
|
||||
|
||||
@@ -46,7 +45,7 @@ class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PicnicUpdateCoordinator,
|
||||
config_entry: ConfigEntry,
|
||||
config_entry: PicnicConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize PicnicCart."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
@@ -1 +1,31 @@
|
||||
"""Support for pico integration."""
|
||||
"""The Pico TTS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.TTS]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Pico TTS from a config entry."""
|
||||
if await hass.async_add_executor_job(shutil.which, "pico2wave") is None:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="binary_not_found"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
49
homeassistant/components/picotts/config_flow.py
Normal file
49
homeassistant/components/picotts/config_flow.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Config flow for Pico TTS integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.tts import CONF_LANG
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
|
||||
from .const import DEFAULT_LANG, DOMAIN, SUPPORT_LANGUAGES
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)}
|
||||
)
|
||||
|
||||
|
||||
class PicoTTSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Pico TTS."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if await self.hass.async_add_executor_job(shutil.which, "pico2wave") is None:
|
||||
return self.async_abort(reason="binary_not_found")
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
language = user_input[CONF_LANG]
|
||||
|
||||
self._async_abort_entries_match({CONF_LANG: language})
|
||||
|
||||
title = f"Pico TTS {language}"
|
||||
data = {
|
||||
CONF_LANG: language,
|
||||
}
|
||||
|
||||
return self.async_create_entry(title=title, data=data)
|
||||
|
||||
async def async_step_import(self, import_info: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Import Pico TTS config from yaml."""
|
||||
|
||||
return await self.async_step_user(import_info)
|
||||
6
homeassistant/components/picotts/const.py
Normal file
6
homeassistant/components/picotts/const.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Constants for the Pico TTS integration."""
|
||||
|
||||
DEFAULT_LANG = "en-US"
|
||||
DOMAIN = "picotts"
|
||||
|
||||
SUPPORT_LANGUAGES = ["en-US", "en-GB", "de-DE", "es-ES", "fr-FR", "it-IT"]
|
||||
25
homeassistant/components/picotts/issue.py
Normal file
25
homeassistant/components/picotts/issue.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Issues for Pico TTS integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def deprecate_yaml_issue(hass: HomeAssistant) -> None:
|
||||
"""Deprecate yaml issue."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"deprecated_yaml_{DOMAIN}",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
breaks_in_ha_version="2026.10.0",
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Pico TTS",
|
||||
},
|
||||
)
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"domain": "picotts",
|
||||
"name": "Pico TTS",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@rooggiieerr"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/picotts",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "legacy"
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_push"
|
||||
}
|
||||
|
||||
38
homeassistant/components/picotts/strings.json
Normal file
38
homeassistant/components/picotts/strings.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"common": {
|
||||
"binary_not_found": "pico2wave binary could not be found"
|
||||
},
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"binary_not_found": "[%key:component::picotts::common::binary_not_found%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "[%key:common::config_flow::data::language%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"binary_not_found": {
|
||||
"message": "[%key:component::picotts::common::binary_not_found%]"
|
||||
},
|
||||
"file_read_error": {
|
||||
"message": "Error trying to read {filename}"
|
||||
},
|
||||
"returncode_error": {
|
||||
"message": "Error running pico2wave, return code: {returncode}"
|
||||
},
|
||||
"timeout_error": {
|
||||
"message": "Timeout running pico2wave"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nThe actions `tts.{domain}_*_say` will be removed and automations should be updated to use the `tts.speak` action with the new tts entities. Then remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
|
||||
"title": "[%key:component::homeassistant::issues::deprecated_yaml::title%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Support for the Pico TTS speech service."""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
@@ -13,32 +14,114 @@ from homeassistant.components.tts import (
|
||||
CONF_LANG,
|
||||
PLATFORM_SCHEMA as TTS_PLATFORM_SCHEMA,
|
||||
Provider,
|
||||
TextToSpeechEntity,
|
||||
TtsAudioType,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import DEFAULT_LANG, DOMAIN, SUPPORT_LANGUAGES
|
||||
from .issue import deprecate_yaml_issue
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SUPPORT_LANGUAGES = ["en-US", "en-GB", "de-DE", "es-ES", "fr-FR", "it-IT"]
|
||||
|
||||
DEFAULT_LANG = "en-US"
|
||||
|
||||
PLATFORM_SCHEMA = TTS_PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_LANG, default=DEFAULT_LANG): vol.In(SUPPORT_LANGUAGES)}
|
||||
)
|
||||
|
||||
|
||||
def get_engine(hass, config, discovery_info=None):
|
||||
async def async_get_engine(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> Provider | None:
|
||||
"""Set up Pico speech component."""
|
||||
if shutil.which("pico2wave") is None:
|
||||
if await hass.async_add_executor_job(shutil.which, "pico2wave") is None:
|
||||
_LOGGER.error("'pico2wave' was not found")
|
||||
return False
|
||||
return None
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
)
|
||||
|
||||
deprecate_yaml_issue(hass)
|
||||
|
||||
return PicoProvider(config[CONF_LANG])
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Pico TTS speech component via config entry."""
|
||||
async_add_entities([PicoTTSEntity(config_entry, config_entry.data[CONF_LANG])])
|
||||
|
||||
|
||||
class PicoTTSEntity(TextToSpeechEntity):
|
||||
"""The Pico TTS API entity."""
|
||||
|
||||
_attr_supported_languages = SUPPORT_LANGUAGES
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry, lang: str) -> None:
|
||||
"""Initialize Pico TTS service."""
|
||||
self._attr_default_language = lang
|
||||
self._attr_name = f"Pico TTS {lang}"
|
||||
self._attr_unique_id = config_entry.entry_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
model="Pico TTS",
|
||||
name=f"Pico TTS {lang}",
|
||||
)
|
||||
|
||||
def get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
"""Load TTS using pico2wave."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmpf:
|
||||
fname = tmpf.name
|
||||
|
||||
cmd = ["pico2wave", "--wave", fname, "-l", language]
|
||||
try:
|
||||
subprocess.run(cmd, text=True, input=message, check=True, timeout=30)
|
||||
with open(fname, "rb") as voice:
|
||||
data = voice.read()
|
||||
except subprocess.CalledProcessError as exc:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="returncode_error",
|
||||
translation_placeholders={"returncode": str(exc.returncode)},
|
||||
) from exc
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout_error",
|
||||
) from exc
|
||||
except OSError as exc:
|
||||
_LOGGER.debug("Full exception %s", exc)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="file_read_error",
|
||||
translation_placeholders={"filename": fname},
|
||||
) from exc
|
||||
finally:
|
||||
with contextlib.suppress(OSError):
|
||||
os.remove(fname)
|
||||
|
||||
return "wav", data
|
||||
|
||||
|
||||
class PicoProvider(Provider):
|
||||
"""The Pico TTS API provider."""
|
||||
|
||||
def __init__(self, lang):
|
||||
def __init__(self, lang: str) -> None:
|
||||
"""Initialize Pico TTS provider."""
|
||||
self._lang = lang
|
||||
self.name = "PicoTTS"
|
||||
@@ -68,15 +151,15 @@ class PicoProvider(Provider):
|
||||
_LOGGER.error(
|
||||
"Error running pico2wave, return code: %s", result.returncode
|
||||
)
|
||||
return (None, None)
|
||||
return None, None
|
||||
with open(fname, "rb") as voice:
|
||||
data = voice.read()
|
||||
except OSError:
|
||||
_LOGGER.error("Error trying to read %s", fname)
|
||||
return (None, None)
|
||||
return None, None
|
||||
finally:
|
||||
os.remove(fname)
|
||||
|
||||
if data:
|
||||
return ("wav", data)
|
||||
return (None, None)
|
||||
return None, None
|
||||
|
||||
@@ -60,6 +60,14 @@ ENDPOINT_BUTTONS: tuple[PortainerButtonDescription, ...] = (
|
||||
)
|
||||
),
|
||||
),
|
||||
PortainerButtonDescription(
|
||||
key="volumes_prune",
|
||||
translation_key="volumes_prune",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action=(
|
||||
lambda portainer, endpoint_id, _: portainer.prune_volumes(endpoint_id)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
CONTAINER_BUTTONS: tuple[PortainerButtonDescription, ...] = (
|
||||
|
||||
@@ -26,7 +26,7 @@ from pyportainer.models.stacks import Stack
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, ContainerState, EndpointStatus
|
||||
@@ -118,13 +118,13 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except PortainerConnectionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except PortainerTimeoutError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="timeout_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
},
|
||||
"resume_container": {
|
||||
"default": "mdi:play"
|
||||
},
|
||||
"volumes_prune": {
|
||||
"default": "mdi:delete-sweep"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyportainer==1.0.33"]
|
||||
"requirements": ["pyportainer==1.0.36"]
|
||||
}
|
||||
|
||||
@@ -74,6 +74,9 @@
|
||||
},
|
||||
"resume_container": {
|
||||
"name": "Resume container"
|
||||
},
|
||||
"volumes_prune": {
|
||||
"name": "Prune unused volumes"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -10,25 +10,24 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS = [Platform.ALARM_CONTROL_PANEL, Platform.CAMERA]
|
||||
|
||||
type ProsegurConfigEntry = ConfigEntry[Auth]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ProsegurConfigEntry) -> bool:
|
||||
"""Set up Prosegur Alarm from a config entry."""
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = Auth(
|
||||
auth = Auth(
|
||||
session,
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_COUNTRY],
|
||||
)
|
||||
await hass.data[DOMAIN][entry.entry_id].login()
|
||||
await auth.login()
|
||||
|
||||
except ConnectionRefusedError as error:
|
||||
_LOGGER.error("Configured credential are invalid, %s", error)
|
||||
@@ -39,15 +38,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.error("Could not connect with Prosegur backend: %s", error)
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
entry.runtime_data = auth
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ProsegurConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -12,12 +12,12 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN
|
||||
from . import ProsegurConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -31,12 +31,12 @@ STATE_MAPPING = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: ProsegurConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Prosegur alarm control panel platform."""
|
||||
async_add_entities(
|
||||
[ProsegurAlarm(entry.data["contract"], hass.data[DOMAIN][entry.entry_id])],
|
||||
[ProsegurAlarm(entry.data["contract"], entry.runtime_data)],
|
||||
update_before_add=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ from pyprosegur.exceptions import ProsegurException
|
||||
from pyprosegur.installation import Camera as InstallationCamera, Installation
|
||||
|
||||
from homeassistant.components.camera import Camera
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@@ -17,15 +16,15 @@ from homeassistant.helpers.entity_platform import (
|
||||
async_get_current_platform,
|
||||
)
|
||||
|
||||
from . import DOMAIN
|
||||
from .const import SERVICE_REQUEST_IMAGE
|
||||
from . import ProsegurConfigEntry
|
||||
from .const import DOMAIN, SERVICE_REQUEST_IMAGE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: ProsegurConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Prosegur camera platform."""
|
||||
@@ -38,12 +37,12 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
_installation = await Installation.retrieve(
|
||||
hass.data[DOMAIN][entry.entry_id], entry.data["contract"]
|
||||
entry.runtime_data, entry.data["contract"]
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
ProsegurCamera(_installation, camera, hass.data[DOMAIN][entry.entry_id])
|
||||
ProsegurCamera(_installation, camera, entry.runtime_data)
|
||||
for camera in _installation.cameras
|
||||
],
|
||||
update_before_add=True,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user