Compare commits

..

82 Commits

Author SHA1 Message Date
Ludovic BOUÉ
bada82fc6e Bump python-roborock to 5.5.1 2026-04-06 16:25:53 +02:00
Erwin Douna
6b2a4df6e0 Bump pyportainer 1.0.36 (#167319) 2026-04-03 21:09:05 +02:00
g4bri3lDev
2ac3979f83 Add opendisplay encryption support (#167251) 2026-04-03 20:11:10 +02:00
Raj Laud
2fb44bce5d Fix victron ble reauth flow title (#167307)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-03 19:48:41 +02:00
MarkGodwin
d187e61274 Update to tplink-omada-client 1.5.7 (#167313) 2026-04-03 19:46:53 +02:00
Erwin Douna
7f79da2f75 Add prune volumes button to Portainer (#167314) 2026-04-03 19:45:55 +02:00
Ludovic BOUÉ
4871344138 Add Hisense AC (0x138C/0x0101) to Matter dry and fan mode device lists (#167282)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-03 19:41:41 +02:00
Ludovic BOUÉ
b0ba740024 Matter Pir unoccupied to occupied delay (#162435)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-03 18:44:29 +02:00
epenet
b6f49d2063 Refactor None handling in renault diagnostics (#167295) 2026-04-03 18:01:36 +02:00
Franck Nijhof
c8e2a2b520 Extract type casting template functions into a type cast Jinja2 extension (#167280) 2026-04-03 18:00:58 +02:00
epenet
27365a4457 Refactor None handling renault device_tracker (#167298) 2026-04-03 17:54:51 +02:00
epenet
4efcb5a700 Use PEP-695 syntax in Renault sensors (#167301) 2026-04-03 17:54:07 +02:00
Denis Shulyaka
4ffc4b8f71 Allow users to overwrite content type for AI task attachments (#167302) 2026-04-03 17:45:00 +02:00
Pete Sage
e8fa61ae63 Sonos alarm switch entities may not be created when speaker offline initially (#167303) 2026-04-03 17:42:09 +02:00
Simone Chemelli
c6c469cc7a Migrate image unique_id for Fritz (#167209) 2026-04-03 16:39:35 +02:00
Richard Polzer
d51afe20e0 Clarify ekeybionyx config flow oauth2 implementation handling (#167169)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-04-03 15:45:06 +02:00
epenet
5dac5ef099 Add missing availability check in device_tracker _async_write_ha_state (#167297) 2026-04-03 15:44:23 +02:00
Pete Sage
a1b93b418b Bump soco to 0.30.15 (#167299) 2026-04-03 15:38:59 +02:00
dependabot[bot]
22a96583a9 Bump codecov/codecov-action from 5.5.3 to 6.0.0 (#167267)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 15:22:12 +02:00
Norbert Rittel
01ffa6a676 Improve Recorder action naming consistency (#167244) 2026-04-03 15:21:58 +02:00
epenet
abf37849fb Remove unnecessary attribute from Renault sensor entity descriptions (#167268) 2026-04-03 15:21:36 +02:00
Ludovic BOUÉ
72cd7ed178 Fix to allow Matter Fan percent setting to be null when FanMode is Auto (#167279)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-03 15:21:14 +02:00
Erwin Douna
6305ea8cf2 Bump pyportainer 1.0.35 (#167288) 2026-04-03 15:20:49 +02:00
Ludovic BOUÉ
815c30b213 Fix Matter water heater off mode (#167286)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-03 15:20:17 +02:00
Joost Lekkerkerker
68cc6df3e0 Make sure we take all Zinvolt battery units in account (#167294) 2026-04-03 15:13:17 +02:00
Norbert Rittel
7f700c891a Improve Media player action naming consistency (#167274)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-03 15:11:35 +02:00
Joost Lekkerkerker
e33727a75a Bump Zinvolt to 0.4.1 (#167296) 2026-04-03 15:09:33 +02:00
Bram Kragten
374a050636 Update frontend to 20260325.6 (#167285) 2026-04-03 14:29:17 +02:00
Wendelin
90045e5539 Fix zwave_js subscribe_rebuild_routes_progress initial event (#167178) 2026-04-03 14:18:37 +02:00
Joost Lekkerkerker
e30c379979 Bump zinvolt to 0.4.0 (#167276) 2026-04-03 14:11:20 +02:00
Norbert Rittel
86d80c96d7 Improve Assist satellite action naming consistency (#167278) 2026-04-03 12:36:54 +02:00
Jan Bouwhuis
1bbecec991 Fix tuya energy sensor units (#160392) 2026-04-03 11:49:40 +02:00
dotlambda
3970a27369 Bump psutil to 7.2.2 (#167263) 2026-04-03 11:38:19 +02:00
Kevin O'Brien
745107c192 Fix Proxmox VE storage usage percentage crash on missing used_fraction (#167136)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 10:41:09 +02:00
Norbert Rittel
24530d13af Fix spelling of "cannot" in dwd_weather_warnings error string (#167138) 2026-04-03 10:04:32 +02:00
epenet
7529b9252c Remove unnecessary None checks in Renault numbers and binary sensors (#167271) 2026-04-03 10:00:55 +02:00
g4bri3lDev
92a1c4568d Bump py-opendisplay version to 5.9.0 (#167250) 2026-04-03 10:00:51 +02:00
Hai-Nam Nguyen
c6527c9f6d Bump hyponcloud to 0.9.3 (#167273) 2026-04-03 10:00:35 +02:00
Andrew Jackson
05b7fa9602 Remove Transmission port forward sensor (#167269) 2026-04-03 09:49:38 +02:00
Joakim Plate
375bd55ae6 Update arcam to 1.8.3 (#167249) 2026-04-02 23:39:00 +02:00
LTek
fbd0cb8666 Fix Ring snapshots (#164337)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-02 23:37:14 +02:00
Max R
7e061262ad Add pre-commit hook for copilot instructions (#167219) 2026-04-02 22:35:53 +01:00
G Johansson
0e9c17fb16 Bump holiday library to 0.93 (#167217) 2026-04-02 23:32:36 +02:00
G Johansson
940de5ea84 Fix SMHI (#167212) 2026-04-02 23:28:44 +02:00
Marc Mueller
bf4773d9bc Fix asyncio loop scopes for pytest fixtures (#166758) 2026-04-02 22:25:17 +02:00
Abílio Costa
0bedcc55ce Set codeowners for agent configurations (#167222) 2026-04-02 19:45:09 +02:00
32u-nd
313f97fc47 Add missing mHz docstrings (#167226) 2026-04-02 19:18:57 +02:00
epenet
b7d32e0650 Adjust git commit guidelines for AI agents (#167184) 2026-04-02 17:20:17 +01:00
Pete Sage
b9afb2a861 Fix Sonos reporting wrong state when media title is whitespace (#167223) 2026-04-02 17:02:14 +01:00
32u-nd
d3a01d4c80 Add millihertz (mHz) to UnitOfFrequency (#167087)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-04-02 17:27:40 +02:00
Steve Easley
05bd2d05f5 Bump pykaleidescape to v1.1.5 (#167203) 2026-04-02 15:59:01 +02:00
Joost Lekkerkerker
23469d8950 Remove not implemented supported feature from Wiim (#167205) 2026-04-02 15:58:07 +02:00
Erwin Douna
26f677dcd1 Portainer refactor async_setup (#166544) 2026-04-02 15:55:50 +02:00
Stefan Agner
484d9b0cbe Fix test_receive_backup test error when run in isolation (#167204) 2026-04-02 15:36:24 +02:00
tzagim
03ed46aa07 Bump python-telegram-bot to 22.7 (#167062) 2026-04-02 15:14:58 +02:00
Joost Lekkerkerker
e5f4000ac2 Add manufacturer to Ecowitt device (#167199) 2026-04-02 14:56:13 +02:00
Norbert Rittel
406598dbfa Fix agreement mismatch and spelling of "cannot" in nmbs (#167137) 2026-04-02 14:47:42 +02:00
epenet
7f23a35155 Migrate qnap to use runtime_data (#167198)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:47:09 +02:00
epenet
0e521eda2e Migrate qbittorrent to use runtime_data (#167196)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:46:28 +02:00
Kurt Chrisford
5a72dc8eca Add exception translations to Actron Air (#167159) 2026-04-02 14:45:42 +02:00
epenet
b11292385f Use runtime_data in ovo_energy (#167141)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:45:19 +02:00
epenet
2179a5405a Migrate progettihwsw to use runtime_data (#167157)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:45:08 +02:00
epenet
78f5989cd6 Migrate permobil to use runtime_data (#167170)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:44:35 +02:00
Norbert Rittel
cca44c675c Fix spelling of "cannot" in climate exception string (#167139) 2026-04-02 14:41:26 +02:00
Ariel Ebersberger
0ebe65c25b Fix hydrawise crashes when controllers/zones are added (#166708) 2026-04-02 14:32:57 +02:00
dependabot[bot]
f7ee95c4b9 Bump sigstore/cosign-installer from 4.1.0 to 4.1.1 (#167156)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 14:11:33 +02:00
Erik Montnemery
d78c05ab62 Propagate the in_zones attribute from device trackers in person entities (#167192) 2026-04-02 14:09:57 +02:00
epenet
9f41e3341f Migrate peco to use runtime_data (#167147)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:48:58 +02:00
epenet
8ab3d482b9 Migrate prusalink to use runtime_data (#167164)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:48:06 +02:00
epenet
a485c3d410 Migrate prosegur to use runtime_data (#167161)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 13:00:06 +02:00
Erik Montnemery
38b27d624a Add new state attribute in_zones to device_tracker (#166573) 2026-04-02 12:24:35 +02:00
Manu
f437d65d3c Remove deprecated LANnouncer integration (#166838) 2026-04-02 12:06:47 +02:00
Erik Montnemery
5ba0764a87 Fix propagation of GPS accuracy in person entity (#167174) 2026-04-02 11:58:48 +02:00
epenet
69fd6532cc Migrate openhome to use runtime_data (#167183)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:51:31 +02:00
epenet
ee8bd9f016 Migrate picnic to use runtime_data (#167151)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:48:49 +02:00
epenet
de5a2d47a5 Migrate pushbullet to use runtime_data (#167166)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:48:39 +02:00
epenet
54b2e0285c Migrate panasonic_viera to use runtime_data (#167171)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:48:36 +02:00
epenet
a0e118d411 Migrate mutesync to use runtime_data (#167180)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-02 11:47:51 +02:00
epenet
07c33233ee Migrate nibe_heatpump to use runtime_data (#167181)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:47:30 +02:00
rrooggiieerr
962cac902b Add Config Flow to Pico TTS (#163114)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-04-02 11:41:47 +02:00
epenet
9ff5c9863f Migrate openexchangerates to use runtime_data (#167182)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:40:10 +02:00
epenet
b60e396241 Migrate pvoutput to use runtime_data (#167167)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 11:03:16 +02:00
254 changed files with 9973 additions and 12776 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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%]"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"]
}

View File

@@ -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"]
}

View File

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

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["hyponcloud==0.9.0"]
"requirements": ["hyponcloud==0.9.3"]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -223,6 +223,12 @@
"cook_time": {
"name": "Cooking time"
},
"detection_delay": {
"name": "Detection delay"
},
"detection_threshold": {
"name": "Detection threshold"
},
"hold_time": {
"name": "Hold time"
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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():

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
"""Constants for the OpenDisplay integration."""
DOMAIN = "opendisplay"
CONF_ENCRYPTION_KEY = "encryption_key"

View File

@@ -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"]
}

View File

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

View File

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

View File

@@ -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}`."
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,4 @@
DOMAIN = "ovo_energy"
DATA_CLIENT = "ovo_client"
DATA_COORDINATOR = "coordinator"
CONF_ACCOUNT = "account"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"])]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)

View 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"]

View 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",
},
)

View File

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

View 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%]"
}
}
}

View File

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

View File

@@ -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, ...] = (

View File

@@ -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)},

View File

@@ -6,6 +6,9 @@
},
"resume_container": {
"default": "mdi:play"
},
"volumes_prune": {
"default": "mdi:delete-sweep"
}
},
"sensor": {

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["pyportainer==1.0.33"]
"requirements": ["pyportainer==1.0.36"]
}

View File

@@ -74,6 +74,9 @@
},
"resume_container": {
"name": "Resume container"
},
"volumes_prune": {
"name": "Prune unused volumes"
}
},
"sensor": {

View File

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

View File

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

View File

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