Compare commits

...

77 Commits

Author SHA1 Message Date
Erik Montnemery 6ad8ad5715 Call state change listeners immediately instead of deferring them to the event loop (#173974) 2026-06-16 16:50:32 +02:00
Åke Strandberg a45867b896 Use username as config entry title in aqvify (#174008) 2026-06-16 14:30:25 +02:00
Franck Nijhof 000e075a8e Add missing subentry flow translations in scrape (#174006) 2026-06-16 14:26:12 +02:00
Franck Nijhof 0899d016b9 Add missing flow form field translations in ecobee (#174002) 2026-06-16 14:25:40 +02:00
Franck Nijhof 3375f2ed76 Add missing flow form field translation in otp (#173994) 2026-06-16 14:25:29 +02:00
Franck Nijhof 3f5778e71b Add missing flow form field translations in tractive (#174005) 2026-06-16 14:10:47 +02:00
Franck Nijhof 86c39694d3 Add missing flow form field translation in iskra (#174004) 2026-06-16 13:48:58 +02:00
Franck Nijhof a53a6644c0 Fix flow form field translations in modem_callerid (#173999) 2026-06-16 13:47:01 +02:00
Franck Nijhof 18fdfacf45 Fix flow form field translation key in sia (#173998) 2026-06-16 13:46:27 +02:00
Franck Nijhof bd9bd29f2c Add missing flow form field translation in airvisual (#174000) 2026-06-16 13:46:19 +02:00
Franck Nijhof 334c6614cc Fix flow form field translations in local_calendar (#173997) 2026-06-16 13:43:22 +02:00
Franck Nijhof aa772f6ecd Add missing flow form field translation in honeywell (#173996) 2026-06-16 13:41:18 +02:00
Franck Nijhof 87169921ae Fix flow form field translations in hlk_sw16 (#173993) 2026-06-16 13:39:40 +02:00
Franck Nijhof 16338b8b6b Fix flow form field translation keys in here_travel_time (#173992) 2026-06-16 13:38:50 +02:00
Åke Strandberg 519da3c9c9 Add aqvify devices dynamically (#173534) 2026-06-16 13:37:42 +02:00
Åke Strandberg 6f34718c1f Bump pyaqvify to 0.0.11 (#173989) 2026-06-16 13:37:02 +02:00
Tim Laing e4287bb43c Bump PyiCloud to 2.6.5 (#173928) 2026-06-16 13:05:50 +02:00
Mike O'Driscoll d724ebac2a casper_glow: add bluetooth reachability diagnostics (#173921) 2026-06-16 13:02:46 +02:00
Raphael Hehl dc480051db Use console name in UniFi Network discovery title (#173931) 2026-06-16 12:57:51 +02:00
Franck Nijhof 63b6ced9c4 Bump evolutionhttp to 0.0.19 (#173911) 2026-06-16 12:46:21 +02:00
Franck Nijhof 34e9b3ff1e Bump lunatone-rest-api-client to 0.9.2 (#173918) 2026-06-16 12:45:19 +02:00
Robert Resch 210746525e Fix missing full sha as hidden field in requirements check aw (#173900) 2026-06-16 11:05:08 +02:00
Robert Resch 0134e99366 Token views should behave the same (#173500) 2026-06-16 10:46:18 +02:00
Oscar Calvo 06de89d6a3 Fix CCM15 temperature unit to follow the device's C/F setting (#173788) 2026-06-16 10:41:58 +02:00
Paul Bottein 4c267617f8 Publish numeric sensor device classes as generated sensor.json (#173919) 2026-06-16 11:41:27 +03:00
Franck Nijhof a82f1a7a1d Bump pyfireservicerota to 0.0.49 (#173935) 2026-06-16 10:36:34 +02:00
Franck Nijhof d234f65dd9 Bump heatmiserV3 to 2.0.6 (#173913)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-06-16 10:36:03 +02:00
John Pettitt 30148980e1 Add API_GEN_4 support to Subaru integration (#173956) 2026-06-16 10:32:02 +02:00
Franck Nijhof 1fa9a3353c Bump eufylife-ble-client to 0.1.10 (#173934) 2026-06-16 10:30:52 +02:00
Raphael Hehl 2dbbd70085 Use console name in UniFi Protect discovery title (#173966) 2026-06-16 10:25:50 +02:00
Franck Nijhof 73903b0bfc Bump pysesame2 to 1.0.2 (#173904) 2026-06-16 10:20:23 +02:00
Franck Nijhof b09f54ce3b Bump foobot_async to 1.0.1 (#173905) 2026-06-16 10:19:38 +02:00
Franck Nijhof 6d9e41da07 Bump pencompy to 0.0.4 (#173906) 2026-06-16 10:17:46 +02:00
Franck Nijhof f5600a602f Bump webexpythonsdk to 2.0.6 (#173916) 2026-06-16 09:50:37 +02:00
Franck Nijhof d83cd941a7 Bump hole to 0.9.2 (#173936) 2026-06-16 09:37:58 +02:00
Franck Nijhof 2120cad533 Bump pdunehd to 1.3.3 (#173907)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-06-16 09:34:44 +02:00
dependabot[bot] fb4e72af77 Bump home-assistant/builder from 2026.03.2 to 2026.06.0 (#173963)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-16 09:22:22 +02:00
Paul Bottein badd4130b6 Bump yoto-api to 4.3.0 (#173910) 2026-06-16 09:18:10 +02:00
Franck Nijhof 7a4ca4dcfd Bump roombapy to 1.9.1 (#173922) 2026-06-16 09:14:40 +02:00
Franck Nijhof 9b47a0d440 Bump atenpdu to 0.3.6 (#173932) 2026-06-16 09:10:16 +02:00
Franck Nijhof 4b99e81a8a Bump influxdb to 5.3.2 (#173891) 2026-06-16 08:54:20 +02:00
Franck Nijhof 62e5238f43 Bump tellcore-py to 1.1.3 (#173894) 2026-06-16 08:53:50 +02:00
Franck Nijhof 149c884a89 Bump DoorBirdPy to 3.0.12 (#173923) 2026-06-16 08:52:50 +02:00
Franck Nijhof 71ca453c42 Bump pyhomematic to 0.1.78 (#173925) 2026-06-16 08:52:27 +02:00
Franck Nijhof aad6080307 Bump omnilogic to 0.4.9 (#173938) 2026-06-16 08:51:40 +02:00
Franck Nijhof 2db2e0b0cf Bump aioairq to 0.4.8 (#173940) 2026-06-16 08:50:50 +02:00
Franck Nijhof 3fc36ab6f9 Bump messagebird to 1.2.1 (#173942) 2026-06-16 08:49:56 +02:00
Denis Shulyaka 0fad24393c Fix docs-data-update IQS for Anthropic (#173947) 2026-06-16 08:21:09 +02:00
Raphael Hehl a992a58367 Use console name in UniFi Access discovery title (#173962) 2026-06-16 08:20:29 +02:00
jasonjhofmann f0cefe2f2e Add network MAC connection to Rain Bird controller (#173672)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:15:33 +02:00
jasonjhofmann 40264992a2 Add network MAC connection to AnthemAV main zone device (#173682)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:12:52 +02:00
jasonjhofmann c29aebd60e Add network MAC connection to PlayStation 4 devices (#173681)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:11:25 +02:00
jasonjhofmann 36b74d6f05 Add network MAC connection to iAlarm device (#173676)
Co-authored-by: jasonjhofmann <16144532+jasonjhofmann@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:02:49 +02:00
jasonjhofmann 2c626fa8f0 Add network MAC connection to Rabbit Air devices (#173684)
Co-authored-by: jasonjhofmann <16144532+jasonjhofmann@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:01:22 +02:00
jasonjhofmann cab0d015f6 Add network MAC connection to Aprilaire devices (#173675)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:00:13 +02:00
Erik Montnemery c544f95979 Prime condition durations from history (#173426) 2026-06-16 07:53:41 +02:00
renovate[bot] 2189d0ae74 Update infrared-protocols to 6.0.1 (#173958) 2026-06-16 07:53:22 +02:00
Raphael Hehl 9e96a06aff Bump unifi-discovery to 1.5.0 (#173927) 2026-06-16 00:06:01 +02:00
Franck Nijhof d16e0e9867 Bump greeclimate to 2.1.4 (#173924) 2026-06-15 22:53:59 +02:00
Franck Nijhof 2209996919 Bump pyipma to 3.0.10 (#173943) 2026-06-15 22:09:00 +02:00
Franck Nijhof d88767155b Bump pykrakenapi to 0.1.9 (#173933) 2026-06-15 21:47:44 +02:00
Franck Nijhof 334d02077f Bump pypck to 0.9.13 (#173914) 2026-06-15 21:46:57 +02:00
Franck Nijhof 2b7e9289d2 Bump librouteros to 3.2.1 (#173937) 2026-06-15 21:41:42 +02:00
Åke Strandberg c57358dd23 Bump pyaqvify to 0.0.10 (#173926) 2026-06-15 20:55:05 +02:00
alexborro e151478d78 Add reauthentication flow to Aquacell (#173110) 2026-06-15 20:37:03 +02:00
Mick Vleeshouwer e41b1f5279 Use device.supports_command in Overkiz (#173280) 2026-06-15 20:18:05 +02:00
LG-ThinQ-Integration 4203aed863 Add off operation_mode to SYSTEM_BOILER in LG ThinQ (#173070)
Co-authored-by: YunseonPark-LGE <yunseon.park@lge.com>
2026-06-15 20:15:08 +02:00
Nikolai Rahimi e7e116843f Raise an error when a Mitsubishi Comfort command is rejected (#173363) 2026-06-15 20:03:18 +02:00
Petro31 d781baca7e Add xy color to template lights (#173296) 2026-06-15 20:02:09 +02:00
Marcello 855962dcd0 Add reauthentication flow to Fluss+ (#173341)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:01:20 +02:00
Tomasz Dylewski cf914f559f Add battery sensor support for PAJ GPS devices (#173123) 2026-06-15 19:47:03 +02:00
Crocmagnon a420a6c990 data grand lyon: pick velo'v stop (#173407) 2026-06-15 19:46:17 +02:00
Peter Grauvogel 5f470d49a5 Add cheapest duration actions to Green Planet Energy integration (#162577)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-06-15 19:38:17 +02:00
Jason Bonta bd2638f144 Add long_press support for HomeWorks QSX in lutron_caseta (#172634)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 19:36:27 +02:00
Franck Nijhof b397d6fd05 Bump pygtfs to 0.1.11 (#173917) 2026-06-15 18:56:53 +02:00
Kevin Stillhammer eb2ee43e6f Remove eifinger as Broadlink codeowner (#173908) 2026-06-15 18:28:36 +02:00
Franck Nijhof 9d16e59899 Bump pykaleidescape to 1.1.6 (#173912) 2026-06-15 18:27:34 +02:00
168 changed files with 4097 additions and 663 deletions
+2 -2
View File
@@ -193,7 +193,7 @@ jobs:
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
- name: Build base image
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
uses: home-assistant/builder/actions/build-image@4de35182ce1e329181bffcbcc84d33db5e2c7e10 # 2026.06.0
with:
arch: ${{ matrix.arch }}
build-args: |
@@ -264,7 +264,7 @@ jobs:
fi
- name: Build machine image
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
uses: home-assistant/builder/actions/build-image@4de35182ce1e329181bffcbcc84d33db5e2c7e10 # 2026.06.0
with:
arch: ${{ matrix.arch }}
build-args: |
+5 -4
View File
@@ -1,4 +1,4 @@
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"328ce915f8b178bbcfcb1b69f397ac996456a4ab50490805b5b3bdd26cbf58fe","body_hash":"0665c72fe4b0a14b3a0b396dc293ce78235771fe15ebc894662fece33b189135","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"7b142e96e0f8b454cdcc9c0c25070cf9a52c44d83a6b1fbc3ad6725b6567337c","body_hash":"3894ded07d5934ac5f29d160ffb1f9115cf72b6da8a7e453a4d4f69e8641a48e","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.79.6","version":"v0.79.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]}
# ___ _ _
# / _ \ | | (_)
@@ -1500,11 +1500,12 @@ jobs:
echo "Artifact has no head_sha; running the agent."
exit 0
fi
# Recover the commit recorded in the most recent requirements-check comment.
# Recover the commit recorded in the most recent requirements-check
# comment from the "Checked at commit" link
PRIOR=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" \
--jq '.[] | select(.body | contains("<!-- requirements-check -->")) | .body' \
| grep -oE '<!-- requirements-check-sha: [0-9a-f]{7,40} -->' \
| grep -oE '[0-9a-f]{7,40}' | tail -1 || true)
| grep -oiE '/commit/[0-9a-f]{40}' \
| grep -oiE '[0-9a-f]{40}' | tail -1 || true)
if [ -z "${PRIOR}" ]; then
echo "No previous comment with a recorded commit; running the agent."
exit 0
+6 -7
View File
@@ -51,11 +51,12 @@ jobs:
echo "Artifact has no head_sha; running the agent."
exit 0
fi
# Recover the commit recorded in the most recent requirements-check comment.
# Recover the commit recorded in the most recent requirements-check
# comment from the "Checked at commit" link
PRIOR=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" \
--jq '.[] | select(.body | contains("<!-- requirements-check -->")) | .body' \
| grep -oE '<!-- requirements-check-sha: [0-9a-f]{7,40} -->' \
| grep -oE '[0-9a-f]{7,40}' | tail -1 || true)
| grep -oiE '/commit/[0-9a-f]{40}' \
| grep -oiE '[0-9a-f]{40}' | tail -1 || true)
if [ -z "${PRIOR}" ]; then
echo "No previous comment with a recorded commit; running the agent."
exit 0
@@ -188,10 +189,8 @@ Then stop. Do not improvise a verdict.
Replace every placeholder with the resolved value and emit
`rendered_comment` via `add_comment`. Preserve the leading
`<!-- requirements-check -->` marker and the
`<!-- requirements-check-sha: … -->` marker that follows it — the next
run reads the recorded commit from it to decide whether anything changed.
The PR target is already wired; do not pass `item_number`.
`<!-- requirements-check -->` marker. The PR target is already wired;
do not pass `item_number`.
## Check instructions
+1 -1
View File
@@ -102,7 +102,7 @@ repos:
pass_filenames: false
language: script
types: [text]
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/.+/(conditions|quality_scale|services|triggers)\.yaml|homeassistant/brands/.*\.json|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/.+/(conditions|quality_scale|services|triggers)\.yaml|homeassistant/brands/.*\.json|script/hassfest/(?!metadata|mypy_config).+\.py|homeassistant/components/sensor/const\.py|requirements.+\.txt)$
- id: hassfest-metadata
name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker
Generated
+2 -2
View File
@@ -262,8 +262,8 @@ CLAUDE.md @home-assistant/core
/tests/components/braviatv/ @bieniu @Drafteed
/homeassistant/components/bring/ @miaucl @tr4nt0r
/tests/components/bring/ @miaucl @tr4nt0r
/homeassistant/components/broadlink/ @danielhiversen @felipediel @L-I-Am @eifinger
/tests/components/broadlink/ @danielhiversen @felipediel @L-I-Am @eifinger
/homeassistant/components/broadlink/ @danielhiversen @felipediel @L-I-Am
/tests/components/broadlink/ @danielhiversen @felipediel @L-I-Am
/homeassistant/components/brother/ @bieniu
/tests/components/brother/ @bieniu
/homeassistant/components/brottsplatskartan/ @gjohansson-ST
+1 -1
View File
@@ -7,7 +7,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairq"],
"requirements": ["aioairq==0.4.7"],
"requirements": ["aioairq==0.4.8"],
"zeroconf": [
{
"properties": {
@@ -37,6 +37,9 @@
"title": "Re-authenticate AirVisual"
},
"user": {
"data": {
"type": "Integration type"
},
"description": "Pick what type of AirVisual data you want to monitor.",
"title": "Configure AirVisual"
}
@@ -12,7 +12,7 @@ from homeassistant.components.media_player import (
)
from homeassistant.const import CONF_MAC, CONF_MODEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -87,9 +87,12 @@ class AnthemAVR(MediaPlayerEntity):
via_device=(DOMAIN, mac_address),
)
else:
# Zone 1 is the physical receiver that owns the network MAC; higher
# zones are via_device children and carry no connection.
self._attr_unique_id = mac_address
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, mac_address)},
connections={(CONNECTION_NETWORK_MAC, mac_address)},
name=name,
manufacturer=MANUFACTURER,
model=model,
@@ -52,10 +52,7 @@ rules:
status: exempt
comment: |
Service integration, no discovery.
docs-data-update:
status: exempt
comment: |
No data updates.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
@@ -193,6 +193,7 @@ class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol):
device_info = DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
connections={(dr.CONNECTION_NETWORK_MAC, data[Attribute.MAC_ADDRESS])},
name=self.create_device_name(data),
manufacturer="Aprilaire",
)
@@ -1,5 +1,6 @@
"""Config flow for Aquacell integration."""
from collections.abc import Mapping
from datetime import datetime
import logging
from typing import Any
@@ -31,6 +32,12 @@ DATA_SCHEMA = vol.Schema(
}
)
STEP_REAUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
)
class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Aquacell."""
@@ -77,3 +84,48 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=DATA_SCHEMA,
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
session = async_get_clientsession(self.hass)
api = AquacellApi(
session, reauth_entry.data.get(CONF_BRAND, Brand.AQUACELL)
)
try:
refresh_token = await api.authenticate(
reauth_entry.data[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except ApiException, TimeoutError:
errors["base"] = "cannot_connect"
except AuthenticationFailed:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates={
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_REFRESH_TOKEN: refresh_token,
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_REAUTH_SCHEMA,
description_placeholders={"email": reauth_entry.data[CONF_EMAIL]},
errors=errors,
)
@@ -14,7 +14,7 @@ from aioaquacell import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
@@ -79,7 +79,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
softeners = await self.aquacell_api.get_all_softeners()
except AuthenticationFailed as err:
raise ConfigEntryError from err
raise ConfigEntryAuthFailed from err
except (AquacellApiException, TimeoutError) as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -9,6 +10,13 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"description": "The password for {email} is no longer valid. Enter your current softener mobile app password to reconnect.",
"title": "[%key:common::config_flow::title::reauth%]"
},
"user": {
"data": {
"brand": "Brand",
@@ -59,7 +59,9 @@ class AqvifyConfigFlow(ConfigFlow, domain=DOMAIN):
self._get_reconfigure_entry(), data_updates=user_input
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title="Aqvify", data=user_input)
return self.async_create_entry(
title=account_data.name or "Aqvify", data=user_input
)
return self.async_show_form(
step_id="user",
@@ -152,3 +152,10 @@ class AqvifyCoordinator(DataUpdateCoordinator[AqvifyCoordinatorData]):
devices=devices,
device_data=device_data,
)
def async_add_devices(self, added_devices: set[str]) -> tuple[set[str], set[str]]:
"""Return newly discovered device keys and the full current device set."""
current_devices = set(self.data.devices.devices)
new_devices: set[str] = current_devices - added_devices
return (new_devices, current_devices)
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["pyaqvify"],
"quality_scale": "silver",
"requirements": ["pyaqvify==0.0.9"]
"requirements": ["pyaqvify==0.0.11"]
}
+17 -5
View File
@@ -59,11 +59,23 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Aqvify sensor entities from a config entry."""
async_add_entities(
AqvifySensor(entry.runtime_data, description, device_key)
for description in ENTITIES
for device_key in entry.runtime_data.data.devices.devices
)
coordinator = entry.runtime_data
added_devices: set[str] = set()
def _async_add_new_devices() -> None:
nonlocal added_devices
new_devices_set, current_devices = coordinator.async_add_devices(added_devices)
added_devices = current_devices
async_add_entities(
AqvifySensor(coordinator, description, device_key)
for description in ENTITIES
for device_key in new_devices_set
)
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
_async_add_new_devices()
class AqvifySensor(AqvifyBaseEntity, SensorEntity):
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/aten_pe",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["atenpdu==0.3.2"]
"requirements": ["atenpdu==0.3.6"]
}
@@ -123,7 +123,14 @@ class _BrandsBaseView(HomeAssistantView):
)
if not authenticated:
if hdrs.AUTHORIZATION in request.headers:
# A failed request that carried an Authorization header is a real
# Bearer auth attempt — return 401 and let the ban middleware count
# it as a wrong login.
raise web.HTTPUnauthorized
# No Authorization header: most likely a benign signed-URL / query-
# token request whose token has expired (e.g. a browser tab left
# open that re-fetches resources later). Return 403 so it doesn't
# register as a wrong login and ban the user's own IP.
raise web.HTTPForbidden
async def _serve_from_custom_integration(
@@ -1,7 +1,7 @@
{
"domain": "broadlink",
"name": "Broadlink",
"codeowners": ["@danielhiversen", "@felipediel", "@L-I-Am", "@eifinger"],
"codeowners": ["@danielhiversen", "@felipediel", "@L-I-Am"],
"config_flow": true,
"dhcp": [
{
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bryant_evolution",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["evolutionhttp==0.0.18"]
"requirements": ["evolutionhttp==0.0.19"]
}
+1 -1
View File
@@ -460,7 +460,7 @@ class EventTrigger(Trigger):
listener = TargetCalendarEventListener(
self._hass, target_selection, self._event_type, offset, run_action
)
return listener.async_setup()
return await listener.async_setup()
class EventStartedTrigger(EventTrigger):
+10 -4
View File
@@ -785,7 +785,9 @@ class CameraView(HomeAssistantView):
async def get(self, request: web.Request, entity_id: str) -> web.StreamResponse:
"""Start a GET request."""
if (camera := self.component.get_entity(entity_id)) is None:
raise web.HTTPNotFound
raise (
web.HTTPNotFound if request[KEY_AUTHENTICATED] else web.HTTPUnauthorized
)
authenticated = (
request[KEY_AUTHENTICATED]
@@ -793,11 +795,15 @@ class CameraView(HomeAssistantView):
)
if not authenticated:
# Attempt with invalid bearer token, raise unauthorized
# so ban middleware can handle it.
if hdrs.AUTHORIZATION in request.headers:
# A failed request that carried an Authorization header is a real
# Bearer auth attempt — return 401 and let the ban middleware count
# it as a wrong login.
raise web.HTTPUnauthorized
# Invalid sigAuth or camera access token
# No Authorization header: most likely a benign signed-URL / query-
# token request whose token has expired (e.g. a browser tab left
# open that re-fetches resources later). Return 403 so it doesn't
# register as a wrong login and ban the user's own IP.
raise web.HTTPForbidden
if not camera.is_on:
@@ -3,6 +3,7 @@
from pycasperglow import CasperGlow
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothReachabilityIntent
from homeassistant.const import CONF_ADDRESS, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
@@ -27,7 +28,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={"address": address},
translation_placeholders={
"address": address,
"reason": bluetooth.async_address_reachability_diagnostics(
hass, address.upper(), BluetoothReachabilityIntent.CONNECTION
),
},
)
glow = CasperGlow(ble_device)
@@ -56,7 +56,7 @@
"message": "An error occurred while communicating with the Casper Glow: {error}"
},
"device_not_found": {
"message": "Could not find Casper Glow device with address {address}"
"message": "Could not find Casper Glow device with address {address}: {reason}"
}
}
}
+7 -1
View File
@@ -49,7 +49,6 @@ async def async_setup_entry(
class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
"""Climate device for CCM15 coordinator."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_has_entity_name = True
_attr_target_temperature_step = PRECISION_WHOLE
_attr_hvac_modes = [
@@ -93,6 +92,13 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
"""Return device data."""
return self.coordinator.get_ac_data(self._ac_index)
@property
def temperature_unit(self) -> str:
"""Return the unit of measurement reported by the device."""
if (data := self.data) is not None and not data.is_celsius:
return UnitOfTemperature.FAHRENHEIT
return UnitOfTemperature.CELSIUS
@property
def current_temperature(self) -> int | None:
"""Return current temperature."""
@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["webexpythonsdk"],
"quality_scale": "legacy",
"requirements": ["webexpythonsdk==2.0.1"]
"requirements": ["webexpythonsdk==2.0.6"]
}
@@ -5,7 +5,12 @@ import logging
from typing import Any
from aiohttp import ClientError, ClientResponseError
from data_grand_lyon_ha import DataGrandLyonClient, TclStop, find_tcl_stop_by_id
from data_grand_lyon_ha import (
DataGrandLyonClient,
TclStop,
VelovStation,
find_tcl_stop_by_id,
)
import voluptuous as vol
from homeassistant.config_entries import (
@@ -49,12 +54,6 @@ STEP_RECONFIGURE_SCHEMA = vol.Schema(
}
)
STEP_VELOV_STATION_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_STATION_ID): vol.Coerce(int),
}
)
class DataGrandLyonConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Data Grand Lyon."""
@@ -302,27 +301,96 @@ def _stop_label(stop: TclStop) -> str:
class VelovStationSubentryFlowHandler(ConfigSubentryFlow):
"""Handle a subentry flow for adding a Vélo'v station."""
def __init__(self) -> None:
"""Initialize the flow."""
self._stations: list[VelovStation] = []
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Handle the user step to add a new Vélo'v station."""
entry = self._get_entry()
"""Pick a station from the list fetched from the API, or enter one manually."""
if not self._stations:
if error := await self._async_load_stations():
return self.async_abort(reason=error)
errors: dict[str, str] = {}
if user_input is not None:
station_id = user_input[CONF_STATION_ID]
unique_id = f"velov_{station_id}"
try:
station_id = int(user_input[CONF_STATION_ID])
except ValueError:
errors[CONF_STATION_ID] = "invalid_station_id"
else:
entry = self._get_entry()
unique_id = f"velov_{station_id}"
for subentry in entry.subentries.values():
if subentry.unique_id == unique_id:
return self.async_abort(reason="already_configured")
for subentry in entry.subentries.values():
if subentry.unique_id == unique_id:
return self.async_abort(reason="already_configured")
return self.async_create_entry(
title=f"Vélo'v {station_id}",
data={CONF_STATION_ID: station_id},
unique_id=unique_id,
return self.async_create_entry(
title=f"Vélo'v {station_id}",
data={CONF_STATION_ID: station_id},
unique_id=unique_id,
)
options = [
SelectOptionDict(
value=str(station.number), label=_velov_station_label(station)
)
for station in sorted(
self._stations,
key=lambda s: (s.name, s.commune or "", s.number or 0),
)
]
schema = vol.Schema(
{
vol.Required(CONF_STATION_ID): SelectSelector(
SelectSelectorConfig(
options=options,
mode=SelectSelectorMode.DROPDOWN,
sort=False,
custom_value=True,
)
)
}
)
return self.async_show_form(
step_id="user",
data_schema=STEP_VELOV_STATION_DATA_SCHEMA,
data_schema=schema,
errors=errors,
)
async def _async_load_stations(self) -> str | None:
"""Fetch Vélo'v stations from the API, returning an error key on failure."""
entry = self._get_entry()
session = async_get_clientsession(self.hass)
client = DataGrandLyonClient(
session=session,
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
)
try:
self._stations = await client.get_velov_stations()
except ClientResponseError as err:
if err.status in (401, 403):
return "invalid_auth"
return "cannot_connect"
except ClientError, TimeoutError:
return "cannot_connect"
except Exception:
_LOGGER.exception(
"Unexpected error fetching Data Grand Lyon Vélo'v stations"
)
return "unknown"
return None
def _velov_station_label(station: VelovStation) -> str:
label = station.name
if station.address or station.commune:
label += (
" (" + ", ".join(filter(None, [station.address, station.commune])) + ")"
)
label += f" - {station.number}"
return label
@@ -76,16 +76,25 @@
},
"velov_station": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"entry_type": "Vélo'v station",
"error": {
"invalid_station_id": "Station ID must be a number."
},
"initiate_flow": {
"user": "Add Vélo'v station"
},
"step": {
"user": {
"data": {
"station_id": "Station ID"
"station_id": "Station"
},
"data_description": {
"station_id": "Search by station name, address or city, or enter a station ID directly."
}
}
}
@@ -8,7 +8,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["doorbirdpy"],
"requirements": ["DoorBirdPy==3.0.11"],
"requirements": ["DoorBirdPy==3.0.12"],
"zeroconf": [
{
"properties": {
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pdunehd"],
"requirements": ["pdunehd==1.3.2"]
"requirements": ["pdunehd==1.3.3"]
}
+3 -1
View File
@@ -30,7 +30,9 @@
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
"api_key": "[%key:common::config_flow::data::api_key%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"description": "Please enter the API key obtained from ecobee.com."
}
@@ -2,6 +2,12 @@
"domain": "eufylife_ble",
"name": "EufyLife",
"bluetooth": [
{
"local_name": "eufy T9120"
},
{
"local_name": "eufy T9130"
},
{
"local_name": "eufy T9140"
},
@@ -16,6 +22,9 @@
},
{
"local_name": "eufy T9149"
},
{
"local_name": "eufy T9150"
}
],
"codeowners": ["@bdr99"],
@@ -24,5 +33,5 @@
"documentation": "https://www.home-assistant.io/integrations/eufylife_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["eufylife-ble-client==0.1.8"]
"requirements": ["eufylife-ble-client==0.1.10"]
}
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyfireservicerota"],
"requirements": ["pyfireservicerota==0.0.46"]
"requirements": ["pyfireservicerota==0.0.49"]
}
+44 -12
View File
@@ -1,5 +1,6 @@
"""Config flow for Fluss+ integration."""
from collections.abc import Mapping
from typing import Any
from fluss_api import (
@@ -22,6 +23,21 @@ STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): cv.string})
class FlussConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fluss+."""
async def _validate_api_key(self, api_key: str) -> dict[str, str]:
"""Validate the API key and return any errors."""
errors: dict[str, str] = {}
client = FlussApiClient(api_key, session=async_get_clientsession(self.hass))
try:
await client.async_get_devices()
except FlussApiClientCommunicationError:
errors["base"] = "cannot_connect"
except FlussApiClientAuthenticationError:
errors["base"] = "invalid_auth"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception occurred")
errors["base"] = "unknown"
return errors
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -31,18 +47,7 @@ class FlussConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
api_key = user_input[CONF_API_KEY]
self._async_abort_entries_match({CONF_API_KEY: api_key})
client = FlussApiClient(
user_input[CONF_API_KEY], session=async_get_clientsession(self.hass)
)
try:
await client.async_get_devices()
except FlussApiClientCommunicationError:
errors["base"] = "cannot_connect"
except FlussApiClientAuthenticationError:
errors["base"] = "invalid_auth"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception occurred")
errors["base"] = "unknown"
errors = await self._validate_api_key(api_key)
if not errors:
return self.async_create_entry(
title="My Fluss+ Devices", data=user_input
@@ -51,3 +56,30 @@ class FlussConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication when the API key is no longer valid."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm re-authentication with a new API key."""
errors: dict[str, str] = {}
if user_input is not None:
api_key = user_input[CONF_API_KEY]
self._async_abort_entries_match({CONF_API_KEY: api_key})
errors = await self._validate_api_key(api_key)
if not errors:
return self.async_update_reload_and_abort(
self._get_reauth_entry(),
data_updates={CONF_API_KEY: api_key},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
)
@@ -12,7 +12,7 @@ from fluss_api import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -60,7 +60,7 @@ class FlussDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]
try:
devices = await self.api.async_get_devices()
except FlussApiClientAuthenticationError as err:
raise ConfigEntryError(f"Authentication failed: {err}") from err
raise ConfigEntryAuthFailed(f"Authentication failed: {err}") from err
except FlussApiClientError as err:
raise UpdateFailed(f"Error fetching Fluss devices: {err}") from err
@@ -29,7 +29,7 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: todo
# Gold
entity-translations: done
+11 -1
View File
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -9,6 +10,15 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "The API key found in the profile page of the Fluss+ app."
},
"description": "The Fluss+ API key is no longer valid. Get your API key from the profile page of the Fluss+ app, or generate a new one, and enter it below."
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["foobot_async"],
"quality_scale": "legacy",
"requirements": ["foobot_async==1.0.0"]
"requirements": ["foobot_async==1.0.1"]
}
+1 -1
View File
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/gree",
"iot_class": "local_polling",
"loggers": ["greeclimate"],
"requirements": ["greeclimate==2.1.1"]
"requirements": ["greeclimate==2.1.4"]
}
@@ -1,15 +1,132 @@
"""Green Planet Energy integration for Home Assistant."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from datetime import timedelta
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import GreenPlanetEnergyUpdateCoordinator
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type GreenPlanetEnergyConfigEntry = ConfigEntry[GreenPlanetEnergyUpdateCoordinator]
PLATFORMS: list[Platform] = [Platform.SENSOR]
# Service constants
SERVICE_GET_CHEAPEST_DURATION = "get_cheapest_duration"
ATTR_DURATION = "duration"
ATTR_TIME_RANGE = "time_range"
# Time range options
TIME_RANGE_DAY = "day"
TIME_RANGE_NIGHT = "night"
TIME_RANGE_FULL_DAY = "full_day"
SERVICE_GET_CHEAPEST_DURATION_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DURATION): vol.All(
vol.Coerce(float), vol.Range(min=0.5, max=24)
),
vol.Optional(ATTR_TIME_RANGE, default=TIME_RANGE_FULL_DAY): vol.In(
[TIME_RANGE_DAY, TIME_RANGE_NIGHT, TIME_RANGE_FULL_DAY]
),
}
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Green Planet Energy component."""
async def get_cheapest_duration(call: ServiceCall) -> ServiceResponse:
"""Handle the get_cheapest_duration service call."""
# This integration has single_config_entry, so get the first entry
entries = hass.config_entries.async_entries(DOMAIN)
if not entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_config_entry",
)
entry = entries[0]
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="config_entry_not_loaded",
)
coordinator: GreenPlanetEnergyUpdateCoordinator = entry.runtime_data
duration = call.data[ATTR_DURATION]
time_range = call.data[ATTR_TIME_RANGE]
data = coordinator.data
api = coordinator.api
now = dt_util.now()
current_hour = now.hour
result: tuple[float | None, int | None]
if time_range == TIME_RANGE_DAY:
result = api.get_cheapest_duration_day(data, duration, current_hour)
elif time_range == TIME_RANGE_NIGHT:
result = api.get_cheapest_duration_night(data, duration, current_hour)
else: # TIME_RANGE_FULL_DAY
result = api.get_cheapest_duration(data, duration, current_hour)
avg_price, start_hour_result = result
if avg_price is None or start_hour_result is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_data_available",
)
start_time = dt_util.start_of_local_day(now).replace(
hour=start_hour_result, minute=0, second=0, microsecond=0
)
# If the calculated start time is in the past, shift to tomorrow
if start_time < now:
start_time = start_time + timedelta(days=1)
end_time = start_time + timedelta(hours=duration)
hours_until_start = (start_time - now).total_seconds() / 3600
return {
"duration": duration,
"average_price": round(avg_price / 100, 4),
"start_time": start_time.isoformat(),
"end_time": end_time.isoformat(),
"hours_until_start": round(hours_until_start, 1),
"time_range": time_range,
}
hass.services.async_register(
DOMAIN,
SERVICE_GET_CHEAPEST_DURATION,
get_cheapest_duration,
schema=SERVICE_GET_CHEAPEST_DURATION_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
return True
async def async_setup_entry(
hass: HomeAssistant, entry: GreenPlanetEnergyConfigEntry
@@ -21,6 +138,7 @@ async def async_setup_entry(
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -0,0 +1,7 @@
{
"services": {
"get_cheapest_duration": {
"service": "mdi:clock-check"
}
}
}
@@ -0,0 +1,25 @@
# Describes the format for available Green Planet Energy services
get_cheapest_duration:
fields:
duration:
required: true
example: 2.5
selector:
number:
min: 0.5
max: 24
step: 0.25
unit_of_measurement: "h"
time_range:
required: false
default: "full_day"
selector:
select:
options:
- label: Full day (00:00-24:00)
value: "full_day"
- label: Day (06:00-18:00)
value: "day"
- label: Night (18:00-06:00)
value: "night"
@@ -43,8 +43,33 @@
"api_error": {
"message": "API error: {error}"
},
"config_entry_not_loaded": {
"message": "This integration instance is not currently loaded"
},
"connection_error": {
"message": "Connection error: {error}"
},
"no_config_entry": {
"message": "No matching integration instance was found"
},
"no_data_available": {
"message": "No price data available for the requested duration and time range"
}
},
"services": {
"get_cheapest_duration": {
"description": "Retrieve electricity price data and find the cheapest consecutive time window for a given duration.",
"fields": {
"duration": {
"description": "Duration in hours for which to find the cheapest time window.",
"name": "Duration"
},
"time_range": {
"description": "Time range to search within.",
"name": "Time range"
}
},
"name": "Get cheapest duration"
}
}
}
+1 -1
View File
@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pygtfs"],
"quality_scale": "legacy",
"requirements": ["pygtfs==0.1.9"]
"requirements": ["pygtfs==0.1.11"]
}
@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["heatmiserV3"],
"quality_scale": "legacy",
"requirements": ["heatmiserV3==2.0.4"]
"requirements": ["heatmiserV3==2.0.6"]
}
@@ -15,7 +15,7 @@
},
"title": "[%key:component::here_travel_time::config::step::destination_menu::title%]"
},
"destination_entity_id": {
"destination_entity": {
"data": {
"destination_entity_id": "Destination using an entity"
},
@@ -34,7 +34,7 @@
},
"title": "[%key:component::here_travel_time::config::step::origin_menu::title%]"
},
"origin_entity_id": {
"origin_entity": {
"data": {
"origin_entity_id": "Origin using an entity"
},
@@ -12,8 +12,7 @@
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your Hi-Link HLK-SW-16 device."
@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["pyhomematic"],
"quality_scale": "legacy",
"requirements": ["pyhomematic==0.1.77"]
"requirements": ["pyhomematic==0.1.78"]
}
@@ -10,7 +10,8 @@
"step": {
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"description": "The Honeywell integration needs to re-authenticate your account",
"title": "[%key:common::config_flow::title::reauth%]"
@@ -6,7 +6,7 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelState,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -41,6 +41,7 @@ class IAlarmPanel(
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.mac)},
connections={(CONNECTION_NETWORK_MAC, coordinator.mac)},
manufacturer="Antifurto365 - Meian",
name="iAlarm",
)
@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["keyrings.alt", "pyicloud"],
"requirements": ["pyicloud==2.4.1"]
"requirements": ["pyicloud==2.6.5"]
}
+10 -4
View File
@@ -326,7 +326,9 @@ class ImageView(HomeAssistantView):
) -> ImageEntity:
"""Authenticate request and return image entity."""
if (image_entity := self.component.get_entity(entity_id)) is None:
raise web.HTTPNotFound
raise (
web.HTTPNotFound if request[KEY_AUTHENTICATED] else web.HTTPUnauthorized
)
authenticated = (
request[KEY_AUTHENTICATED]
@@ -334,11 +336,15 @@ class ImageView(HomeAssistantView):
)
if not authenticated:
# Attempt with invalid bearer token, raise unauthorized
# so ban middleware can handle it.
if hdrs.AUTHORIZATION in request.headers:
# A failed request that carried an Authorization header is a real
# Bearer auth attempt — return 401 and let the ban middleware count
# it as a wrong login.
raise web.HTTPUnauthorized
# Invalid sigAuth or image entity access token
# No Authorization header: most likely a benign signed-URL / query-
# token request whose token has expired (e.g. a browser tab left
# open that re-fetches resources later). Return 403 so it doesn't
# register as a wrong login and ban the user's own IP.
raise web.HTTPForbidden
return image_entity
@@ -7,6 +7,6 @@
"documentation": "https://www.home-assistant.io/integrations/influxdb",
"iot_class": "local_push",
"loggers": ["influxdb", "influxdb_client"],
"requirements": ["influxdb==5.3.1", "influxdb-client==1.50.0"],
"requirements": ["influxdb==5.3.2", "influxdb-client==1.50.0"],
"single_config_entry": true
}
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/infrared",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["infrared-protocols==6.0.0"]
"requirements": ["infrared-protocols==6.0.1"]
}
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["geopy", "pyipma"],
"requirements": ["pyipma==3.0.9"]
"requirements": ["pyipma==3.0.10"]
}
+2 -1
View File
@@ -31,7 +31,8 @@
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
"host": "[%key:common::config_flow::data::host%]",
"protocol": "Protocol"
},
"data_description": {
"host": "Hostname or IP address of your Iskra device."
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/kaleidescape",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["pykaleidescape==1.1.5"],
"requirements": ["pykaleidescape==1.1.6"],
"ssdp": [
{
"deviceType": "schemas-upnp-org:device:Basic:1",
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["krakenex", "pykrakenapi"],
"requirements": ["krakenex==2.2.2", "pykrakenapi==0.1.8"]
"requirements": ["krakenex==2.2.2", "pykrakenapi==0.1.9"]
}
+1 -1
View File
@@ -10,5 +10,5 @@
"iot_class": "local_polling",
"loggers": ["pypck"],
"quality_scale": "silver",
"requirements": ["pypck==0.9.11", "lcn-frontend==0.2.9"]
"requirements": ["pypck==0.9.13", "lcn-frontend==0.2.9"]
}
+1 -1
View File
@@ -175,7 +175,7 @@ class LcnLedLogicSensor(LcnEntity, SensorEntity):
async def async_update(self) -> None:
"""Update the state of the entity."""
self._attr_available = (
await self.device_connection.request_status_led_and_logic_ops(
await self.device_connection.request_status_leds_and_logic_ops(
SCAN_INTERVAL.seconds
)
is not None
@@ -40,7 +40,6 @@ DEVICE_OP_MODE_TO_HA = {
"auto": STATE_ECO,
"heat_pump": STATE_HEAT_PUMP,
"turbo": STATE_PERFORMANCE,
"vacation": STATE_OFF,
}
HA_STATE_TO_DEVICE_OP_MODE = {v: k for k, v in DEVICE_OP_MODE_TO_HA.items()}
@@ -96,8 +95,6 @@ class ThinQWaterHeaterEntity(ThinQEntity, WaterHeaterEntity):
self._attr_operation_list = [
DEVICE_OP_MODE_TO_HA.get(mode, mode) for mode in modes
]
else:
self._attr_operation_list = [STATE_HEAT_PUMP]
def _update_status(self) -> None:
"""Update status itself."""
@@ -183,6 +180,8 @@ class ThinQWaterBoilerEntity(ThinQWaterHeaterEntity):
"""Initialize a water_heater entity."""
super().__init__(coordinator, entity_description, property_id)
self._attr_supported_features |= WaterHeaterEntityFeature.ON_OFF
# For SYSTEM_BOILER, we only support heat pump mode and off mode.
self._attr_operation_list = [STATE_HEAT_PUMP, STATE_OFF]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
@@ -197,3 +196,16 @@ class ThinQWaterBoilerEntity(ThinQWaterHeaterEntity):
"[%s:%s] async_turn_off", self.coordinator.device_name, self.property_id
)
await self.async_call_api(self.coordinator.api.async_turn_off(self.property_id))
async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set new operation mode."""
_LOGGER.debug(
"[%s:%s] async_set_operation_mode: %s",
self.coordinator.device_name,
self.property_id,
operation_mode,
)
if operation_mode == STATE_OFF:
await self.async_turn_off()
else:
await self.async_turn_on()
@@ -7,7 +7,10 @@
"invalid_ics_file": "There was a problem reading the calendar information. See the error log for additional details."
},
"step": {
"import": {
"import_ics_file": {
"data": {
"ics_file": "ICS file"
},
"description": "You can import events in iCal format (.ics file)."
},
"user": {
@@ -7,7 +7,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["lunatone-rest-api-client==0.9.1"],
"requirements": ["lunatone-rest-api-client==0.9.2"],
"zeroconf": [
{
"properties": {
@@ -23,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.typing import ConfigType
from .const import (
ACTION_LONG_PRESS,
ACTION_MULTITAP,
ACTION_PRESS,
ACTION_RELEASE,
@@ -35,6 +36,7 @@ from .const import (
ATTR_SERIAL,
ATTR_TYPE,
BRIDGE_DEVICE_ID,
BUTTON_STATUS_LONG_HOLD,
CONF_CA_CERTS,
CONF_CERTFILE,
CONF_KEYFILE,
@@ -450,6 +452,8 @@ def _async_subscribe_keypad_events(
action = ACTION_PRESS
elif event_type == BUTTON_STATUS_MULTITAP:
action = ACTION_MULTITAP
elif event_type == BUTTON_STATUS_LONG_HOLD:
action = ACTION_LONG_PRESS
else:
action = ACTION_RELEASE
@@ -29,10 +29,21 @@ ATTR_DEVICE_NAME = "device_name"
ATTR_AREA_NAME = "area_name"
ATTR_ACTION = "action"
ACTION_LONG_PRESS = "long_press"
ACTION_MULTITAP = "multi_tap"
ACTION_PRESS = "press"
ACTION_RELEASE = "release"
# Raw EventType string sent by the Lutron LEAP protocol for a long hold.
# pylutron-caseta passes all EventType values through without filtering.
BUTTON_STATUS_LONG_HOLD = "LongHold"
# Bridge DeviceType strings (bridge_device["type"]) that are known to send
# native LongHold events over LEAP. Used to gate the long_press device
# trigger so it only appears in the automation UI for supported hardware.
# Caseta and RadioRA3 processors do not emit LongHold.
BRIDGE_DEVICE_TYPES_WITH_LONG_HOLD = frozenset({"HWQSProcessor"})
CONF_SUBTYPE = "subtype"
CONNECT_TIMEOUT = 9
@@ -19,11 +19,13 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from .const import (
ACTION_LONG_PRESS,
ACTION_MULTITAP,
ACTION_PRESS,
ACTION_RELEASE,
ATTR_ACTION,
ATTR_BUTTON_TYPE,
BRIDGE_DEVICE_TYPES_WITH_LONG_HOLD,
CONF_SUBTYPE,
DOMAIN,
LUTRON_CASETA_BUTTON_EVENT,
@@ -38,7 +40,18 @@ def _reverse_dict(forward_dict: dict) -> dict:
return {v: k for k, v in forward_dict.items()}
SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_MULTITAP, ACTION_RELEASE]
SUPPORTED_INPUTS_EVENTS_TYPES = [
ACTION_PRESS,
ACTION_LONG_PRESS,
ACTION_MULTITAP,
ACTION_RELEASE,
]
# Triggers that are only available on specific bridge types.
# Actions absent from this dict are supported by all bridge types.
TRIGGER_REQUIRED_BRIDGE_TYPES: dict[str, frozenset[str]] = {
ACTION_LONG_PRESS: BRIDGE_DEVICE_TYPES_WITH_LONG_HOLD,
}
LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
{
@@ -414,6 +427,14 @@ async def async_get_triggers(
keypad_button_names_to_leap[keypad["lutron_device_id"]],
)
bridge_type = data.bridge_device.get("type", "")
supported_triggers = [
t
for t in SUPPORTED_INPUTS_EVENTS_TYPES
if t not in TRIGGER_REQUIRED_BRIDGE_TYPES
or bridge_type in TRIGGER_REQUIRED_BRIDGE_TYPES[t]
]
return [
{
CONF_PLATFORM: "device",
@@ -422,7 +443,7 @@ async def async_get_triggers(
CONF_TYPE: trigger,
CONF_SUBTYPE: subtype,
}
for trigger in SUPPORTED_INPUTS_EVENTS_TYPES
for trigger in supported_triggers
for subtype in valid_buttons
]
@@ -75,6 +75,8 @@
"stop_all": "Stop all"
},
"trigger_type": {
"long_press": "\"{subtype}\" long pressed",
"multi_tap": "\"{subtype}\" multi-tapped",
"press": "\"{subtype}\" pressed",
"release": "\"{subtype}\" released"
}
@@ -16,7 +16,7 @@ from typing import Any, Final, Required, TypedDict, final
from urllib.parse import quote, urlparse
import aiohttp
from aiohttp import web
from aiohttp import hdrs, web
from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE
from aiohttp.typedefs import LooseHeaders
from propcache.api import cached_property
@@ -1271,12 +1271,9 @@ class MediaPlayerImageView(HomeAssistantView):
) -> web.Response:
"""Start a get request."""
if (player := self.component.get_entity(entity_id)) is None:
status = (
HTTPStatus.NOT_FOUND
if request[KEY_AUTHENTICATED]
else HTTPStatus.UNAUTHORIZED
raise (
web.HTTPNotFound if request[KEY_AUTHENTICATED] else web.HTTPUnauthorized
)
return web.Response(status=status)
assert isinstance(player, MediaPlayerEntity)
authenticated = (
@@ -1285,7 +1282,16 @@ class MediaPlayerImageView(HomeAssistantView):
)
if not authenticated:
return web.Response(status=HTTPStatus.UNAUTHORIZED)
if hdrs.AUTHORIZATION in request.headers:
# A failed request that carried an Authorization header is a real
# Bearer auth attempt — return 401 and let the ban middleware count
# it as a wrong login.
raise web.HTTPUnauthorized
# No Authorization header: most likely a benign signed-URL / query-
# token request whose token has expired (e.g. a browser tab left
# open that re-fetches resources later). Return 403 so it doesn't
# register as a wrong login and ban the user's own IP.
raise web.HTTPForbidden
if media_content_type and media_content_id:
media_image_id = request.query.get("media_image_id")
@@ -6,5 +6,5 @@
"iot_class": "cloud_push",
"loggers": ["messagebird"],
"quality_scale": "legacy",
"requirements": ["messagebird==1.2.0"]
"requirements": ["messagebird==1.2.1"]
}
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["librouteros"],
"requirements": ["librouteros==3.2.0"]
"requirements": ["librouteros==3.2.1"]
}
@@ -1,6 +1,7 @@
"""Climate entity for Mitsubishi Comfort integration."""
from typing import Any
import logging
from typing import Any, NoReturn
from mitsubishi_comfort import FanSpeed, IndoorUnit, Mode, VaneDirection
@@ -14,11 +15,15 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import MitsubishiComfortConfigEntry, MitsubishiComfortCoordinator
from .entity import MitsubishiComfortEntity
_LOGGER = logging.getLogger(__name__)
_MODE_TO_HVAC: dict[str, HVACMode] = {
"off": HVACMode.OFF,
"cool": HVACMode.COOL,
@@ -219,32 +224,47 @@ class MitsubishiComfortClimate(MitsubishiComfortEntity, ClimateEntity):
features |= ClimateEntityFeature.SWING_MODE
return features
def _command_failed(self, translation_key: str) -> NoReturn:
"""Raise a translated error after the device rejects a command."""
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key=translation_key,
translation_placeholders={"device_name": self._device.name},
)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
lib_mode = _HVAC_TO_MODE.get(hvac_mode)
if lib_mode is None:
_LOGGER.debug("Ignoring unsupported HVAC mode %s", hvac_mode)
return
result = await self._device.set_mode(lib_mode)
if result.success:
self._optimistic[_OPT_MODE] = result.value
self.async_write_ha_state()
if not result.success:
self._command_failed("set_hvac_mode_failed")
self._optimistic[_OPT_MODE] = result.value
self.async_write_ha_state()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the target temperature."""
mode = self._effective_mode
wrote = False
failed = False
if ATTR_TARGET_TEMP_HIGH in kwargs:
result = await self._device.set_cool_setpoint(kwargs[ATTR_TARGET_TEMP_HIGH])
if result.success:
self._optimistic[_OPT_COOL_SETPOINT] = result.value
wrote = True
else:
failed = True
if ATTR_TARGET_TEMP_LOW in kwargs:
result = await self._device.set_heat_setpoint(kwargs[ATTR_TARGET_TEMP_LOW])
if result.success:
self._optimistic[_OPT_HEAT_SETPOINT] = result.value
wrote = True
else:
failed = True
temp = kwargs.get(ATTR_TEMPERATURE)
if temp is not None:
@@ -253,34 +273,51 @@ class MitsubishiComfortClimate(MitsubishiComfortEntity, ClimateEntity):
if result.success:
self._optimistic[_OPT_COOL_SETPOINT] = result.value
wrote = True
else:
failed = True
elif mode in ("heat", "autoHeat"):
result = await self._device.set_heat_setpoint(temp)
if result.success:
self._optimistic[_OPT_HEAT_SETPOINT] = result.value
wrote = True
else:
failed = True
else:
_LOGGER.debug(
"Ignoring temperature for %s: no setpoint applies in mode %s",
self._device.name,
mode,
)
# Apply whatever succeeded before surfacing the failure to the user.
if wrote:
self.async_write_ha_state()
if failed:
self._command_failed("set_temperature_failed")
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the fan mode."""
speed = _FAN_SPEED_MAP.get(fan_mode)
if speed is None:
_LOGGER.debug("Ignoring unsupported fan mode %s", fan_mode)
return
result = await self._device.set_fan_speed(speed)
if result.success:
self._optimistic[_OPT_FAN_SPEED] = result.value
self.async_write_ha_state()
if not result.success:
self._command_failed("set_fan_mode_failed")
self._optimistic[_OPT_FAN_SPEED] = result.value
self.async_write_ha_state()
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set the swing mode."""
direction = _VANE_DIR_MAP.get(swing_mode)
if direction is None:
_LOGGER.debug("Ignoring unsupported swing mode %s", swing_mode)
return
result = await self._device.set_vane_direction(direction)
if result.success:
self._optimistic[_OPT_VANE_DIRECTION] = result.value
self.async_write_ha_state()
if not result.success:
self._command_failed("set_swing_mode_failed")
self._optimistic[_OPT_VANE_DIRECTION] = result.value
self.async_write_ha_state()
async def async_turn_off(self) -> None:
"""Turn the entity off."""
@@ -29,6 +29,18 @@
"no_devices": {
"message": "No devices were found in your Mitsubishi Comfort account"
},
"set_fan_mode_failed": {
"message": "{device_name} did not accept the requested fan mode"
},
"set_hvac_mode_failed": {
"message": "{device_name} did not accept the requested HVAC mode"
},
"set_swing_mode_failed": {
"message": "{device_name} did not accept the requested swing mode"
},
"set_temperature_failed": {
"message": "{device_name} did not accept the requested temperature"
},
"update_failed": {
"message": "{device_name} returned no data"
}
@@ -14,8 +14,7 @@
},
"user": {
"data": {
"name": "[%key:common::config_flow::data::name%]",
"port": "[%key:common::config_flow::data::port%]"
"device": "[%key:common::config_flow::data::device%]"
},
"description": "This is an integration for landline calls using a CX93001 voice modem. This can retrieve caller ID information with an option to reject an incoming call."
}
@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["config", "omnilogic"],
"requirements": ["omnilogic==0.4.5"],
"requirements": ["omnilogic==0.4.9"],
"single_config_entry": true
}
+2 -1
View File
@@ -11,7 +11,8 @@
"step": {
"confirm": {
"data": {
"code": "Verification code (OTP)"
"code": "Verification code (OTP)",
"qr_code": "QR code"
},
"data_description": {
"code": "The six-digit code currently displayed in your authentication app."
@@ -64,7 +64,7 @@ class AtlanticElectricalTowelDryer(OverkizEntity, ClimateEntity):
# Not all AtlanticElectricalTowelDryer models support temporary presets,
# thus we check if the command is available and then extend the presets
if self.executor.has_command(OverkizCommand.SET_TOWEL_DRYER_TEMPORARY_STATE):
if self.device.supports_command(OverkizCommand.SET_TOWEL_DRYER_TEMPORARY_STATE):
# Extend preset modes with supported temporary presets, avoiding duplicates
self._attr_preset_modes += [
mode
@@ -48,7 +48,7 @@ class AtlanticPassAPCZoneControl(OverkizEntity, ClimateEntity):
def is_auto_hvac_mode_available(self) -> bool:
"""Check if auto mode is available on the ZoneControl."""
return self.executor.has_command(
return self.device.supports_command(
OverkizCommand.SET_HEATING_COOLING_AUTO_SWITCH
) and self.executor.has_state(OverkizState.CORE_HEATING_COOLING_AUTO_SWITCH)
@@ -160,7 +160,7 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone):
def is_using_derogated_temperature_fallback(self) -> bool:
"""Check if the device behave like the Pass APC Heating Zone."""
return self.executor.has_command(
return self.device.supports_command(
OverkizCommand.SET_DEROGATED_TARGET_TEMPERATURE
)
+15 -9
View File
@@ -561,46 +561,52 @@ class OverkizCover(OverkizDescriptiveEntity, CoverEntity):
# and HA sets by default open/close as supported feature which conflicts
supported_features = CoverEntityFeature(0)
if self.entity_description.open_command and self.executor.has_command(
if self.entity_description.open_command and self.device.supports_command(
self.entity_description.open_command
):
supported_features |= CoverEntityFeature.OPEN
if self.entity_description.stop_command and self.executor.has_command(
if self.entity_description.stop_command and self.device.supports_command(
self.entity_description.stop_command
):
supported_features |= CoverEntityFeature.STOP
if self.entity_description.close_command and self.executor.has_command(
if self.entity_description.close_command and self.device.supports_command(
self.entity_description.close_command
):
supported_features |= CoverEntityFeature.CLOSE
if self.entity_description.open_tilt_command and self.executor.has_command(
if self.entity_description.open_tilt_command and self.device.supports_command(
self.entity_description.open_tilt_command
):
supported_features |= CoverEntityFeature.OPEN_TILT
if self.entity_description.stop_tilt_command and self.executor.has_command(
if (
self.entity_description.stop_tilt_command
and self.device.supports_command(
self.entity_description.stop_tilt_command
)
):
supported_features |= CoverEntityFeature.STOP_TILT
if self.entity_description.close_tilt_command and self.executor.has_command(
if self.entity_description.close_tilt_command and self.device.supports_command(
self.entity_description.close_tilt_command
):
supported_features |= CoverEntityFeature.CLOSE_TILT
if (
self.entity_description.set_tilt_position_command
and self.executor.has_command(
and self.device.supports_command(
self.entity_description.set_tilt_position_command
)
):
supported_features |= CoverEntityFeature.SET_TILT_POSITION
if self.entity_description.set_position_command and self.executor.has_command(
if (
self.entity_description.set_position_command
and self.device.supports_command(
self.entity_description.set_position_command
)
):
supported_features |= CoverEntityFeature.SET_POSITION
@@ -742,7 +748,7 @@ class OverkizCover(OverkizDescriptiveEntity, CoverEntity):
motor to stop between commands on some devices (e.g.
Somfy DynamicExteriorVenetianBlind).
"""
if not self.executor.has_command(OverkizCommand.SET_CLOSURE_AND_ORIENTATION):
if not self.device.supports_command(OverkizCommand.SET_CLOSURE_AND_ORIENTATION):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="unsupported_set_position_and_tilt",
@@ -45,15 +45,6 @@ class OverkizExecutor:
f"{self.device.identifier.base_device_url}#{index}"
)
def select_command(self, *commands: str) -> str | None:
"""Select first existing command in a list of commands."""
existing_commands = self.device.definition.commands
return next((c for c in commands if c in existing_commands), None)
def has_command(self, *commands: str) -> bool:
"""Return True if a command exists in a list of commands."""
return self.select_command(*commands) is not None
def select_definition_state(self, *states: str) -> StateDefinition | None:
"""Select first existing definition state in a list of states."""
for state_name in states:
+2 -2
View File
@@ -44,9 +44,9 @@ class OverkizLight(OverkizEntity, LightEntity):
self._attr_supported_color_modes: set[ColorMode] = set()
if self.executor.has_command(OverkizCommand.SET_RGB):
if self.device.supports_command(OverkizCommand.SET_RGB):
self._attr_color_mode = ColorMode.RGB
elif self.executor.has_command(OverkizCommand.SET_INTENSITY):
elif self.device.supports_command(OverkizCommand.SET_INTENSITY):
self._attr_color_mode = ColorMode.BRIGHTNESS
else:
self._attr_color_mode = ColorMode.ONOFF
@@ -236,20 +236,22 @@ class DomesticHotWaterProduction(OverkizEntity, WaterHeaterEntity):
"""Set new target temperature."""
target_temperature = kwargs[ATTR_TEMPERATURE]
if self.executor.has_command(OverkizCommand.SET_TARGET_TEMPERATURE):
if self.device.supports_command(OverkizCommand.SET_TARGET_TEMPERATURE):
await self.executor.async_execute_command(
OverkizCommand.SET_TARGET_TEMPERATURE, target_temperature
)
elif self.executor.has_command(OverkizCommand.SET_WATER_TARGET_TEMPERATURE):
elif self.device.supports_command(OverkizCommand.SET_WATER_TARGET_TEMPERATURE):
await self.executor.async_execute_command(
OverkizCommand.SET_WATER_TARGET_TEMPERATURE, target_temperature
)
if self.executor.has_command(OverkizCommand.REFRESH_TARGET_TEMPERATURE):
if self.device.supports_command(OverkizCommand.REFRESH_TARGET_TEMPERATURE):
await self.executor.async_execute_command(
OverkizCommand.REFRESH_TARGET_TEMPERATURE
)
elif self.executor.has_command(OverkizCommand.REFRESH_WATER_TARGET_TEMPERATURE):
elif self.device.supports_command(
OverkizCommand.REFRESH_WATER_TARGET_TEMPERATURE
):
await self.executor.async_execute_command(
OverkizCommand.REFRESH_WATER_TARGET_TEMPERATURE
)
@@ -275,12 +277,12 @@ class DomesticHotWaterProduction(OverkizEntity, WaterHeaterEntity):
"""Set new target operation mode."""
if operation_mode == STATE_PERFORMANCE:
if self.executor.has_command(OverkizCommand.SET_BOOST_MODE):
if self.device.supports_command(OverkizCommand.SET_BOOST_MODE):
await self.executor.async_execute_command(
OverkizCommand.SET_BOOST_MODE, OverkizCommand.ON
)
if self.executor.has_command(OverkizCommand.SET_BOOST_MODE_DURATION):
if self.device.supports_command(OverkizCommand.SET_BOOST_MODE_DURATION):
await self.executor.async_execute_command(
OverkizCommand.SET_BOOST_MODE_DURATION, 7
)
@@ -288,7 +290,7 @@ class DomesticHotWaterProduction(OverkizEntity, WaterHeaterEntity):
OverkizCommand.REFRESH_BOOST_MODE_DURATION
)
if self.executor.has_command(OverkizCommand.SET_CURRENT_OPERATING_MODE):
if self.device.supports_command(OverkizCommand.SET_CURRENT_OPERATING_MODE):
current_operating_mode = self.executor.select_state(
OverkizState.CORE_OPERATING_MODE
)
@@ -307,12 +309,12 @@ class DomesticHotWaterProduction(OverkizEntity, WaterHeaterEntity):
if self._is_boost_mode_on:
# We're setting a non Boost mode and the device is currently in Boost mode
# The following code removes all boost operations
if self.executor.has_command(OverkizCommand.SET_BOOST_MODE):
if self.device.supports_command(OverkizCommand.SET_BOOST_MODE):
await self.executor.async_execute_command(
OverkizCommand.SET_BOOST_MODE, OverkizCommand.OFF
)
if self.executor.has_command(OverkizCommand.SET_CURRENT_OPERATING_MODE):
if self.device.supports_command(OverkizCommand.SET_CURRENT_OPERATING_MODE):
current_operating_mode = self.executor.select_state(
OverkizState.CORE_OPERATING_MODE
)
@@ -330,10 +332,10 @@ class DomesticHotWaterProduction(OverkizEntity, WaterHeaterEntity):
OverkizCommand.SET_DHW_MODE, self.operation_mode_to_overkiz[operation_mode]
)
if self.executor.has_command(OverkizCommand.REFRESH_BOOST_MODE_DURATION):
if self.device.supports_command(OverkizCommand.REFRESH_BOOST_MODE_DURATION):
await self.executor.async_execute_command(
OverkizCommand.REFRESH_BOOST_MODE_DURATION
)
if self.executor.has_command(OverkizCommand.REFRESH_DHW_MODE):
if self.device.supports_command(OverkizCommand.REFRESH_DHW_MODE):
await self.executor.async_execute_command(OverkizCommand.REFRESH_DHW_MODE)
+15 -3
View File
@@ -1,7 +1,7 @@
"""Platform for PAJ GPS sensor integration."""
from collections.abc import Callable
from dataclasses import dataclass
from dataclasses import dataclass, field
from pajgps_api.models.trackpoint import TrackPoint
@@ -11,12 +11,12 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfSpeed
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfSpeed
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import PajGpsConfigEntry
from .coordinator import PajGpsCoordinator
from .coordinator import Device, PajGpsCoordinator
from .entity import PajGpsEntity
PARALLEL_UPDATES = 0
@@ -27,6 +27,7 @@ class PajGpsSensorEntityDescription(SensorEntityDescription):
"""Describes a PAJ GPS sensor entity."""
value_fn: Callable[[TrackPoint], int | None]
supported_fn: Callable[[Device], bool] = field(default=lambda _: True)
SENSOR_DESCRIPTIONS: tuple[PajGpsSensorEntityDescription, ...] = (
@@ -38,6 +39,16 @@ SENSOR_DESCRIPTIONS: tuple[PajGpsSensorEntityDescription, ...] = (
suggested_display_precision=0,
value_fn=lambda tp: tp.speed,
),
PajGpsSensorEntityDescription(
key="battery",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
suggested_display_precision=0,
value_fn=lambda tp: tp.battery_level,
supported_fn=lambda device: device.has_battery,
),
)
@@ -62,6 +73,7 @@ async def async_setup_entry(
PajGpsSensor(coordinator, device_id, description)
for device_id in sorted_new_ids
for description in SENSOR_DESCRIPTIONS
if description.supported_fn(coordinator.data.devices[device_id])
)
known_device_ids.update(sorted_new_ids)
@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pencompy"],
"quality_scale": "legacy",
"requirements": ["pencompy==0.0.3"]
"requirements": ["pencompy==0.0.4"]
}
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["hole"],
"requirements": ["hole==0.9.0"]
"requirements": ["hole==0.9.2"]
}
@@ -352,6 +352,8 @@ class PS4Device(MediaPlayerEntity):
for device in d_registry.devices.get_devices_for_config_entry_id(
self._entry_id
):
# Rebuilt from the existing device entry, which already carries
# the network MAC connection added by the live-status branch.
self._attr_device_info = DeviceInfo(
identifiers=device.identifiers,
manufacturer=device.manufacturer,
@@ -365,7 +367,9 @@ class PS4Device(MediaPlayerEntity):
_sw_version = status["system-version"]
_sw_version = _sw_version[1:4]
sw_version = f"{_sw_version[0]}.{_sw_version[1:]}"
# status["host-id"] is the console's network MAC address.
self._attr_device_info = DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, status["host-id"])},
identifiers={(DOMAIN, status["host-id"])},
manufacturer="Sony Interactive Entertainment Inc.",
model="PlayStation 4",
+2 -1
View File
@@ -6,7 +6,7 @@ from typing import Any
from rabbitair import Model
from homeassistant.const import CONF_MAC
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
@@ -36,6 +36,7 @@ class RabbitAirBaseEntity(CoordinatorEntity[RabbitAirDataUpdateCoordinator]):
self._attr_unique_id = entry.unique_id
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.data[CONF_MAC])},
connections={(CONNECTION_NETWORK_MAC, entry.data[CONF_MAC])},
manufacturer="Rabbit Air",
model=MODELS.get(coordinator.data.model),
name=entry.title,
@@ -13,9 +13,10 @@ from pyrainbird.async_client import (
)
from pyrainbird.data import ModelAndVersion, Schedule
from homeassistant.const import CONF_MAC
from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, MANUFACTURER, TIMEOUT_SECONDS
@@ -104,13 +105,18 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
"""Return information about the device."""
if self._unique_id is None:
return None
return DeviceInfo(
device_info = DeviceInfo(
name=self.device_name,
identifiers={(DOMAIN, self._unique_id)},
manufacturer=MANUFACTURER,
model=self._model_info.model_name,
sw_version=f"{self._model_info.major}.{self._model_info.minor}",
)
# The unique id is the formatted MAC for current config entries, but was
# historically the serial number, so derive the connection from the MAC.
if mac_address := self.config_entry.data.get(CONF_MAC):
device_info["connections"] = {(CONNECTION_NETWORK_MAC, mac_address)}
return device_info
async def _async_update_data(self) -> RainbirdDeviceState:
"""Fetch data from Rain Bird device."""
@@ -25,7 +25,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["paho_mqtt", "roombapy"],
"requirements": ["roombapy==1.9.0"],
"requirements": ["roombapy==1.9.1"],
"zeroconf": [
{
"name": "irobot-*",
@@ -58,6 +58,38 @@
"user": "Add sensor"
},
"step": {
"reconfigure": {
"data": {
"index": "[%key:component::scrape::config_subentries::entity::step::user::data::index%]",
"select": "[%key:component::scrape::config_subentries::entity::step::user::data::select%]"
},
"data_description": {
"index": "[%key:component::scrape::config_subentries::entity::step::user::data_description::index%]",
"select": "[%key:component::scrape::config_subentries::entity::step::user::data_description::select%]"
},
"sections": {
"advanced": {
"data": {
"attribute": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::attribute%]",
"availability": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::availability%]",
"device_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::device_class%]",
"state_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::state_class%]",
"unit_of_measurement": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::unit_of_measurement%]",
"value_template": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data::value_template%]"
},
"data_description": {
"attribute": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::attribute%]",
"availability": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::availability%]",
"device_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::device_class%]",
"state_class": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::state_class%]",
"unit_of_measurement": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::unit_of_measurement%]",
"value_template": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::data_description::value_template%]"
},
"description": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::description%]",
"name": "[%key:component::scrape::config_subentries::entity::step::user::sections::advanced::name%]"
}
}
},
"user": {
"data": {
"index": "Index",
@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["pysesame2"],
"quality_scale": "legacy",
"requirements": ["pysesame2==1.0.1"]
"requirements": ["pysesame2==1.0.2"]
}
+1 -1
View File
@@ -13,7 +13,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"additional_account": {
"add_account": {
"data": {
"account": "[%key:component::sia::config::step::user::data::account%]",
"additional_account": "[%key:component::sia::config::step::user::data::additional_account%]",
+1
View File
@@ -29,6 +29,7 @@ VEHICLE_STATUS = "vehicle_status"
API_GEN_1 = "g1"
API_GEN_2 = "g2"
API_GEN_3 = "g3"
API_GEN_4 = "g4"
MANUFACTURER = "Subaru"
PLATFORMS = [
+3 -2
View File
@@ -24,6 +24,7 @@ from . import get_device_info
from .const import (
API_GEN_2,
API_GEN_3,
API_GEN_4,
VEHICLE_API_GEN,
VEHICLE_HAS_EV,
VEHICLE_STATUS,
@@ -153,10 +154,10 @@ def create_vehicle_sensors(
sensor_descriptions_to_add = []
sensor_descriptions_to_add.extend(SAFETY_SENSORS)
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_2, API_GEN_3]:
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_2, API_GEN_3, API_GEN_4]:
sensor_descriptions_to_add.extend(API_GEN_2_SENSORS)
if vehicle_info[VEHICLE_API_GEN] == API_GEN_3:
if vehicle_info[VEHICLE_API_GEN] in [API_GEN_3, API_GEN_4]:
sensor_descriptions_to_add.extend(API_GEN_3_SENSORS)
if vehicle_info[VEHICLE_HAS_EV]:
@@ -6,5 +6,5 @@
"iot_class": "assumed_state",
"loggers": ["tellcore"],
"quality_scale": "legacy",
"requirements": ["tellcore-net==0.4", "tellcore-py==1.1.2"]
"requirements": ["tellcore-net==0.4", "tellcore-py==1.1.3"]
}
+66 -12
View File
@@ -16,6 +16,7 @@ from homeassistant.components.light import (
ATTR_RGBW_COLOR,
ATTR_RGBWW_COLOR,
ATTR_TRANSITION,
ATTR_XY_COLOR,
DEFAULT_MAX_KELVIN,
DEFAULT_MIN_KELVIN,
DOMAIN as LIGHT_DOMAIN,
@@ -26,7 +27,14 @@ from homeassistant.components.light import (
filter_supported_color_modes,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EFFECT, CONF_HS, CONF_NAME, CONF_RGB, CONF_STATE
from homeassistant.const import (
CONF_EFFECT,
CONF_HS,
CONF_NAME,
CONF_RGB,
CONF_STATE,
CONF_XY,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import (
@@ -74,6 +82,7 @@ CONF_ON_ACTION = "turn_on"
CONF_SUPPORTS_TRANSITION = "supports_transition"
CONF_TEMPERATURE_ACTION = "set_temperature"
CONF_TEMPERATURE = "temperature"
CONF_XY_ACTION = "set_xy"
DEFAULT_MIN_MIREDS = 153
DEFAULT_MAX_MIREDS = 500
@@ -90,6 +99,7 @@ SCRIPT_FIELDS = (
CONF_RGBW_ACTION,
CONF_RGBWW_ACTION,
CONF_TEMPERATURE_ACTION,
CONF_XY_ACTION,
)
LIGHT_COMMON_SCHEMA = vol.Schema(
@@ -115,6 +125,8 @@ LIGHT_COMMON_SCHEMA = vol.Schema(
vol.Optional(CONF_SUPPORTS_TRANSITION): cv.template,
vol.Optional(CONF_TEMPERATURE_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TEMPERATURE): cv.template,
vol.Optional(CONF_XY): cv.template,
vol.Optional(CONF_XY_ACTION): cv.SCRIPT_SCHEMA,
}
)
@@ -184,10 +196,21 @@ def _string_to_list(result: str) -> list[float]:
return [float(v) for v in result.split(",")]
def hs_color_list(entity: AbstractTemplateLight) -> Callable[[Any], list[int] | None]:
"""Convert the result to a list of numbers that represent hue and saturation."""
def two_color_list(
entity: AbstractTemplateLight,
option: str,
option_one: str,
min_1: int,
max_1: int,
option_two: str,
min_2: int,
max_2: int,
) -> Callable[[Any], list[int | float] | None]:
"""Convert the result to a list of 2 numbers that represent a color."""
def convert(result: Any) -> list[int] | None:
option_range = f"({min_1}-{max_1}, {min_2}-{max_2})"
def convert(result: Any) -> list[int | float] | None:
if template_validators.check_result_for_none(result):
return None
@@ -200,15 +223,15 @@ def hs_color_list(entity: AbstractTemplateLight) -> Callable[[Any], list[int] |
and len(result) == 2
and all(isinstance(value, (int, float)) for value in result)
):
hue, saturation = result
if not (0 <= hue <= 360) or not (0 <= saturation <= 100):
one, two = result
if not (min_1 <= one <= max_1) or not (min_2 <= two <= max_2):
template_validators.log_validation_result_error(
entity,
CONF_HS,
option,
result,
(
"expected a hue value between 0 and 360 and "
"a saturation value between 0 and 100: (0-360, 0-100)"
f"expected {option_one} value between {min_1} and {max_1} and "
f"{option_two} value between {min_2} and {max_2}: {option_range}"
),
)
return None
@@ -217,9 +240,9 @@ def hs_color_list(entity: AbstractTemplateLight) -> Callable[[Any], list[int] |
template_validators.log_validation_result_error(
entity,
CONF_HS,
option,
result,
"expected a list of numbers: (0-360, 0-100)",
f"expected a list of numbers: {option_range}",
)
return None
@@ -299,7 +322,7 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
self.setup_template(
CONF_HS,
"_attr_hs_color",
hs_color_list(self),
two_color_list(self, CONF_HS, "a hue", 0, 360, "a saturation", 0, 100),
self._update_color("_attr_hs_color", ColorMode.HS),
render_complex=True,
)
@@ -318,6 +341,15 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
render_complex=True,
)
# Setup XY Color
self.setup_template(
CONF_XY,
"_attr_xy_color",
two_color_list(self, CONF_XY, "an x", 0, 1, "a y", 0, 1),
self._update_color("_attr_xy_color", ColorMode.XY),
render_complex=True,
)
# Setup Effect templates
self.setup_template(
CONF_EFFECT_LIST,
@@ -371,6 +403,7 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
(CONF_RGB_ACTION, ColorMode.RGB),
(CONF_RGBW_ACTION, ColorMode.RGBW),
(CONF_RGBWW_ACTION, ColorMode.RGBWW),
(CONF_XY_ACTION, ColorMode.XY),
):
if (action_config := config.get(action_id)) is not None:
self.add_script(action_id, action_config, name, DOMAIN)
@@ -482,6 +515,15 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
)
optimistic_set = True
if CONF_XY not in self._templates and ATTR_XY_COLOR in kwargs:
self._set_optimistic_color(
"xy color",
"_attr_xy_color",
kwargs[ATTR_XY_COLOR],
ColorMode.XY,
)
optimistic_set = True
if optimistic_set and not self._attr_assumed_state:
# If we are optmistically setting color or level but the state template
# has not rendered, optimisically set the state to 'on'.
@@ -507,6 +549,7 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
(CONF_RGB, "_attr_rgb_color"),
(CONF_RGBW, "_attr_rgbw_color"),
(CONF_RGBWW, "_attr_rgbww_color"),
(CONF_XY, "_attr_xy_color"),
):
if attribute == attr:
continue
@@ -619,6 +662,17 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
return (script, common_params)
if (
ATTR_XY_COLOR in kwargs
and (script := CONF_XY_ACTION) in self._action_scripts
):
xy_value = kwargs[ATTR_XY_COLOR]
common_params["xy"] = xy_value
common_params["x"] = float(xy_value[0])
common_params["y"] = float(xy_value[1])
return (script, common_params)
if (
ATTR_BRIGHTNESS in kwargs
and (script := CONF_LEVEL_ACTION) in self._action_scripts
+1 -1
View File
@@ -137,7 +137,7 @@ class TimeRemainingTrigger(Trigger):
state = self._hass.states.get(entity_id)
schedule_for_state(entity_id, state, state.context if state else None)
unsub = async_track_target_selector_state_change_event(
unsub = await async_track_target_selector_state_change_event(
self._hass,
self._target,
state_change_listener,
+1 -1
View File
@@ -153,7 +153,7 @@ class ItemTriggerBase(Trigger, abc.ABC):
functools.partial(self._handle_item_change, run_action=run_action),
self._handle_entities_updated,
)
return listener.async_setup()
return await listener.async_setup()
@callback
@abc.abstractmethod
@@ -12,6 +12,12 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",

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