forked from home-assistant/core
Compare commits
266 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d321edaded | |||
| a4d12694da | |||
| 2278e3f06f | |||
| 0144a0bb1f | |||
| 7cc8f91bf9 | |||
| d58157ca9e | |||
| f401ffb08c | |||
| 8f7b831b94 | |||
| 9ed6b591a5 | |||
| 98ea067285 | |||
| 7e507dd378 | |||
| 8e87223c40 | |||
| 0cce4d1b81 | |||
| 46dcc91510 | |||
| b1a2af9fd3 | |||
| 5d58cdd98e | |||
| a8aebbce9a | |||
| f1244c182a | |||
| 560eeac457 | |||
| d33080d79e | |||
| 25f02c5b38 | |||
| cb01af9f92 | |||
| 9a6ebb0848 | |||
| fd30dd0aee | |||
| 4a5e261709 | |||
| 2842f55460 | |||
| 7573a74cb0 | |||
| 636b484d9d | |||
| a979f884f9 | |||
| 990ea78dec | |||
| ee6db3bd23 | |||
| ae5606aa2f | |||
| 7f9f106729 | |||
| 44c63ce6f1 | |||
| cbf7ca6a9a | |||
| eb892df65a | |||
| 24b5886d88 | |||
| d5e902a170 | |||
| d907e4c10b | |||
| c4be3c4de2 | |||
| 626591f832 | |||
| 2bd3196183 | |||
| fd93cf375d | |||
| 6bf8b84d26 | |||
| c72fea57a1 | |||
| 17dad7d8ae | |||
| 14664719d9 | |||
| b14cd1e14b | |||
| fd38d9788d | |||
| 0b3b641328 | |||
| 6ef77f8243 | |||
| 3a27143012 | |||
| 9a6c642bdf | |||
| 38b8d0b018 | |||
| 4d3443dbf5 | |||
| 4f99e54402 | |||
| d6615e3d44 | |||
| 9c23331ead | |||
| 5fb2802bf4 | |||
| b4864e6a8a | |||
| 04c34877f4 | |||
| bdeb61fafc | |||
| 76d4257f51 | |||
| c6c7e7eae1 | |||
| 07557e27b0 | |||
| f211da60e0 | |||
| 64b74d00f7 | |||
| 96cb645644 | |||
| 9b0db3bd51 | |||
| ffdefd1e0f | |||
| 59ad0268a9 | |||
| f28851e76f | |||
| 4f5c1d544b | |||
| a8ccf1c6fc | |||
| e3f7e5706b | |||
| 7ad1e756e7 | |||
| 8868f214f3 | |||
| 3ecff19a45 | |||
| 74421db747 | |||
| 1cccfac3dc | |||
| c254548a64 | |||
| 7f8b782e95 | |||
| cd518d4a46 | |||
| c5db07e84d | |||
| d1e0225520 | |||
| d439bb68eb | |||
| 980dbf364d | |||
| 842e7ce171 | |||
| 8afec8ada9 | |||
| 7b699f7733 | |||
| d448ef9f16 | |||
| 03912a1704 | |||
| 54c20d5d5a | |||
| 2dbf24e798 | |||
| 791654a420 | |||
| 5fe07e49e4 | |||
| 0bd287788c | |||
| 40e0c0f98d | |||
| 85b608912b | |||
| 987753dd1c | |||
| 5df05fb6dd | |||
| f295ca27af | |||
| 8f75cc6a33 | |||
| 19c71f0f49 | |||
| 22c2028c00 | |||
| 39f687e3a3 | |||
| 6692b9b71f | |||
| 2f5787e7be | |||
| bbda1761bf | |||
| ecc10e9793 | |||
| 9e1e889fd7 | |||
| eefe1e6f0f | |||
| 397ed87f2d | |||
| 15830f383e | |||
| 87395efc6e | |||
| 27d79bb10a | |||
| 7427db70aa | |||
| 77d5bffa85 | |||
| ab7c7b8d89 | |||
| 93b8cc38d8 | |||
| e5f95b3aff | |||
| 613728ad3b | |||
| cb1bfe6ebe | |||
| 434179ab3f | |||
| eb53277fcc | |||
| 850ddb3667 | |||
| 5a727a4fa3 | |||
| 33fc700952 | |||
| ad493e077e | |||
| a2b2f6f20a | |||
| ee57fd413a | |||
| f5d585e0f0 | |||
| 1899388f35 | |||
| 4d833e9b1c | |||
| 6d827cd412 | |||
| ebfbea39ff | |||
| 89a40f1c48 | |||
| 664eb7af10 | |||
| 33b99b6627 | |||
| 0cf2ee0bcb | |||
| 85a86c3f11 | |||
| de4a5fa30b | |||
| 43ac550ca0 | |||
| c3c4d224b2 | |||
| 6f865beacd | |||
| de25195383 | |||
| 0139d2cabf | |||
| 17542614b5 | |||
| 885367e690 | |||
| f8c44aad25 | |||
| 2323cc2869 | |||
| 7f0249bbf7 | |||
| 7a23b778a4 | |||
| d910924032 | |||
| 0b93a8c2f2 | |||
| 5e377b89fc | |||
| dd85a1e5f0 | |||
| b96a7aebcd | |||
| 3cfcf382da | |||
| ed9fd2c643 | |||
| a007e8dc26 | |||
| b318644998 | |||
| 0434eea3ab | |||
| c19b984660 | |||
| 0d6bb8a325 | |||
| 094b969301 | |||
| ddef6fdb98 | |||
| cabf7860b3 | |||
| 0c0a2403e5 | |||
| be6c3d8bbd | |||
| c01536ee58 | |||
| a9f36a50e4 | |||
| 6d11c0395f | |||
| 66bb638dd0 | |||
| 0d72bfef70 | |||
| 6e44552d41 | |||
| 9ec02633b3 | |||
| 5d340332bf | |||
| 1e973c1d74 | |||
| 618ada64f8 | |||
| 2d6802e06a | |||
| 9687a34a70 | |||
| 5ba0ceb6c2 | |||
| d8e3e88c63 | |||
| d1d1bca29d | |||
| 80189495c5 | |||
| cad6c72cfa | |||
| 23ac22e213 | |||
| 55e664fc0d | |||
| 881ce45afa | |||
| b80195df81 | |||
| e57ce0a9df | |||
| ff66ad7705 | |||
| 33e98ebffa | |||
| 8fd9e2046e | |||
| 32c2f47ab5 | |||
| e2fc2dce84 | |||
| afa97f8ec1 | |||
| 2708c1c94c | |||
| d76ed6a3c2 | |||
| 695f69bd90 | |||
| 7da8e24e21 | |||
| 9d0fc0d513 | |||
| ca567aa7fc | |||
| 27af2d8ec6 | |||
| 59ea6f375a | |||
| 6c365c94ed | |||
| 6693fc764f | |||
| e855b6c2bc | |||
| 23a1dddc23 | |||
| bd5fef1ddb | |||
| c3ade400fb | |||
| 1889f0ef66 | |||
| 6b28af8282 | |||
| f59001d45f | |||
| a857461059 | |||
| e4cc842584 | |||
| bb52058920 | |||
| c1676570da | |||
| 4858b2171e | |||
| 192aa76cd7 | |||
| ddf611bfdf | |||
| 3164394982 | |||
| b250a03ff5 | |||
| 2dd7f035f6 | |||
| 2c08b3f30c | |||
| c3ec30ce3b | |||
| 9d4375ca76 | |||
| 3870b87db9 | |||
| ff2fd7e9ef | |||
| 719dd09eb3 | |||
| 2cf2613dbd | |||
| 181a3d142e | |||
| c20ad5fde1 | |||
| 4fcebf18dc | |||
| a6e04be076 | |||
| 330a8e197d | |||
| 4300e846e6 | |||
| 07fd1f99df | |||
| 481639bcf9 | |||
| 376008940b | |||
| b2c2db3394 | |||
| a636e38d24 | |||
| ae1294830c | |||
| d87fdf028b | |||
| 6f5d5d4cdb | |||
| 12fdd7034a | |||
| f295d72cd9 | |||
| 2605fda185 | |||
| 2189dc3e2a | |||
| 8364d8a2e3 | |||
| 96c9636086 | |||
| 7b1dfc35d1 | |||
| 2e94730491 | |||
| 11c6998bf2 | |||
| 055a024d10 | |||
| f73afd71fd | |||
| ec64194ab9 | |||
| d49a613c62 | |||
| 6fc064fa6a | |||
| b36b591ccf | |||
| d25ba79427 | |||
| df35f30321 | |||
| 1e3d06a993 | |||
| 2ee6bf7340 | |||
| 13a8e5e021 |
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v9
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/intents-package
|
||||
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
||||
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 2
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.6"
|
||||
HA_SHORT_VERSION: "2025.7"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -360,7 +360,7 @@ jobs:
|
||||
- name: Run ruff
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure
|
||||
pre-commit run --hook-stage manual ruff-check --all-files --show-diff-on-failure
|
||||
env:
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.18
|
||||
uses: github/codeql-action/init@v3.28.19
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.18
|
||||
uses: github/codeql-action/analyze@v3.28.19
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.0
|
||||
rev: v0.11.12
|
||||
hooks:
|
||||
- id: ruff
|
||||
- id: ruff-check
|
||||
args:
|
||||
- --fix
|
||||
- id: ruff-format
|
||||
@@ -30,7 +30,7 @@ repos:
|
||||
- --branch=master
|
||||
- --branch=rc
|
||||
- repo: https://github.com/adrienverge/yamllint.git
|
||||
rev: v1.35.1
|
||||
rev: v1.37.1
|
||||
hooks:
|
||||
- id: yamllint
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
|
||||
Vendored
+1
-1
@@ -45,7 +45,7 @@
|
||||
{
|
||||
"label": "Ruff",
|
||||
"type": "shell",
|
||||
"command": "pre-commit run ruff --all-files",
|
||||
"command": "pre-commit run ruff-check --all-files",
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
|
||||
@@ -171,8 +171,6 @@ FRONTEND_INTEGRATIONS = {
|
||||
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
|
||||
# The substage containing recorder should have no timeout, as it could cancel a database migration.
|
||||
# Recorder freezes "recorder" timeout during a migration, but it does not freeze other timeouts.
|
||||
# The substages preceding it should also have no timeout, until we ensure that the recorder
|
||||
# is not accidentally promoted as a dependency of any of the integrations in them.
|
||||
# If we add timeouts to the frontend substages, we should make sure they don't apply in recovery mode.
|
||||
STAGE_0_INTEGRATIONS = (
|
||||
# Load logging and http deps as soon as possible
|
||||
@@ -929,7 +927,11 @@ async def _async_set_up_integrations(
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
continue
|
||||
try:
|
||||
async with hass.timeout.async_timeout(timeout, cool_down=COOLDOWN_TIME):
|
||||
async with hass.timeout.async_timeout(
|
||||
timeout,
|
||||
cool_down=COOLDOWN_TIME,
|
||||
cancel_message=f"Bootstrap stage {name} timeout",
|
||||
):
|
||||
await _async_setup_multi_components(hass, stage_all_domains, config)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
@@ -941,7 +943,11 @@ async def _async_set_up_integrations(
|
||||
# Wrap up startup
|
||||
_LOGGER.debug("Waiting for startup to wrap up")
|
||||
try:
|
||||
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
|
||||
async with hass.timeout.async_timeout(
|
||||
WRAP_UP_TIMEOUT,
|
||||
cool_down=COOLDOWN_TIME,
|
||||
cancel_message="Bootstrap startup wrap up timeout",
|
||||
):
|
||||
await hass.async_block_till_done()
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "shelly",
|
||||
"name": "shelly",
|
||||
"integrations": ["shelly"],
|
||||
"iot_standards": ["zwave"]
|
||||
}
|
||||
@@ -14,30 +14,24 @@ from jaraco.abode.exceptions import (
|
||||
)
|
||||
from jaraco.abode.helpers.timeline import Groups as GROUPS
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_DATE,
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_TIME,
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_POLLING, DOMAIN, LOGGER
|
||||
|
||||
SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
|
||||
from .services import async_setup_services
|
||||
|
||||
ATTR_DEVICE_NAME = "device_name"
|
||||
ATTR_DEVICE_TYPE = "device_type"
|
||||
@@ -45,22 +39,12 @@ ATTR_EVENT_CODE = "event_code"
|
||||
ATTR_EVENT_NAME = "event_name"
|
||||
ATTR_EVENT_TYPE = "event_type"
|
||||
ATTR_EVENT_UTC = "event_utc"
|
||||
ATTR_SETTING = "setting"
|
||||
ATTR_USER_NAME = "user_name"
|
||||
ATTR_APP_TYPE = "app_type"
|
||||
ATTR_EVENT_BY = "event_by"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
CHANGE_SETTING_SCHEMA = vol.Schema(
|
||||
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
|
||||
)
|
||||
|
||||
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -85,7 +69,7 @@ class AbodeSystem:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Abode component."""
|
||||
setup_hass_services(hass)
|
||||
async_setup_services(hass)
|
||||
return True
|
||||
|
||||
|
||||
@@ -138,60 +122,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
return unload_ok
|
||||
|
||||
|
||||
def setup_hass_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
def change_setting(call: ServiceCall) -> None:
|
||||
"""Change an Abode system setting."""
|
||||
setting = call.data[ATTR_SETTING]
|
||||
value = call.data[ATTR_VALUE]
|
||||
|
||||
try:
|
||||
hass.data[DOMAIN].abode.set_setting(setting, value)
|
||||
except AbodeException as ex:
|
||||
LOGGER.warning(ex)
|
||||
|
||||
def capture_image(call: ServiceCall) -> None:
|
||||
"""Capture a new image."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_camera_capture_{entity_id}"
|
||||
dispatcher_send(hass, signal)
|
||||
|
||||
def trigger_automation(call: ServiceCall) -> None:
|
||||
"""Trigger an Abode automation."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_trigger_automation_{entity_id}"
|
||||
dispatcher_send(hass, signal)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SETTINGS, change_setting, schema=CHANGE_SETTING_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_CAPTURE_IMAGE, capture_image, schema=CAPTURE_IMAGE_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_TRIGGER_AUTOMATION, trigger_automation, schema=AUTOMATION_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def setup_hass_events(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant start and stop callbacks."""
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Support for the Abode Security System."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from jaraco.abode.exceptions import Exception as AbodeException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
SERVICE_SETTINGS = "change_setting"
|
||||
SERVICE_CAPTURE_IMAGE = "capture_image"
|
||||
SERVICE_TRIGGER_AUTOMATION = "trigger_automation"
|
||||
|
||||
ATTR_SETTING = "setting"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
|
||||
CHANGE_SETTING_SCHEMA = vol.Schema(
|
||||
{vol.Required(ATTR_SETTING): cv.string, vol.Required(ATTR_VALUE): cv.string}
|
||||
)
|
||||
|
||||
CAPTURE_IMAGE_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
AUTOMATION_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.entity_ids})
|
||||
|
||||
|
||||
def _change_setting(call: ServiceCall) -> None:
|
||||
"""Change an Abode system setting."""
|
||||
setting = call.data[ATTR_SETTING]
|
||||
value = call.data[ATTR_VALUE]
|
||||
|
||||
try:
|
||||
call.hass.data[DOMAIN].abode.set_setting(setting, value)
|
||||
except AbodeException as ex:
|
||||
LOGGER.warning(ex)
|
||||
|
||||
|
||||
def _capture_image(call: ServiceCall) -> None:
|
||||
"""Capture a new image."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in call.hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_camera_capture_{entity_id}"
|
||||
dispatcher_send(call.hass, signal)
|
||||
|
||||
|
||||
def _trigger_automation(call: ServiceCall) -> None:
|
||||
"""Trigger an Abode automation."""
|
||||
entity_ids = call.data[ATTR_ENTITY_ID]
|
||||
|
||||
target_entities = [
|
||||
entity_id
|
||||
for entity_id in call.hass.data[DOMAIN].entity_ids
|
||||
if entity_id in entity_ids
|
||||
]
|
||||
|
||||
for entity_id in target_entities:
|
||||
signal = f"abode_trigger_automation_{entity_id}"
|
||||
dispatcher_send(call.hass, signal)
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Home Assistant services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_SETTINGS, _change_setting, schema=CHANGE_SETTING_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_CAPTURE_IMAGE, _capture_image, schema=CAPTURE_IMAGE_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_TRIGGER_AUTOMATION,
|
||||
_trigger_automation,
|
||||
schema=AUTOMATION_SCHEMA,
|
||||
)
|
||||
@@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
entry.unique_id for entry in self._async_current_entries()
|
||||
}
|
||||
|
||||
hubs: list[aiopulse.Hub] = []
|
||||
with suppress(TimeoutError):
|
||||
async with timeout(5):
|
||||
hubs: list[aiopulse.Hub] = [
|
||||
hubs = [
|
||||
hub
|
||||
async for hub in aiopulse.Hub.discover()
|
||||
if hub.id not in already_configured
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant
|
||||
from .const import CONNECTION_TYPE, LOCAL
|
||||
from .coordinator import AdaxCloudCoordinator, AdaxConfigEntry, AdaxLocalCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE]
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AdaxConfigEntry) -> bool:
|
||||
|
||||
@@ -41,7 +41,30 @@ class AdaxCloudCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
||||
"""Fetch data from the Adax."""
|
||||
rooms = await self.adax_data_handler.get_rooms() or []
|
||||
try:
|
||||
if hasattr(self.adax_data_handler, "fetch_rooms_info"):
|
||||
rooms = await self.adax_data_handler.fetch_rooms_info() or []
|
||||
_LOGGER.debug("fetch_rooms_info returned: %s", rooms)
|
||||
else:
|
||||
_LOGGER.debug("fetch_rooms_info method not available, using get_rooms")
|
||||
rooms = []
|
||||
|
||||
if not rooms:
|
||||
_LOGGER.debug(
|
||||
"No rooms from fetch_rooms_info, trying get_rooms as fallback"
|
||||
)
|
||||
rooms = await self.adax_data_handler.get_rooms() or []
|
||||
_LOGGER.debug("get_rooms fallback returned: %s", rooms)
|
||||
|
||||
if not rooms:
|
||||
raise UpdateFailed("No rooms available from Adax API")
|
||||
|
||||
except OSError as e:
|
||||
raise UpdateFailed(f"Error communicating with API: {e}") from e
|
||||
|
||||
for room in rooms:
|
||||
room["energyWh"] = int(room.get("energyWh", 0))
|
||||
|
||||
return {r["id"]: r for r in rooms}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Support for Adax energy sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AdaxConfigEntry
|
||||
from .const import CONNECTION_TYPE, DOMAIN, LOCAL
|
||||
from .coordinator import AdaxCloudCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AdaxConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Adax energy sensors with config flow."""
|
||||
if entry.data.get(CONNECTION_TYPE) != LOCAL:
|
||||
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
|
||||
|
||||
# Create individual energy sensors for each device
|
||||
async_add_entities(
|
||||
AdaxEnergySensor(cloud_coordinator, device_id)
|
||||
for device_id in cloud_coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class AdaxEnergySensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
|
||||
"""Representation of an Adax energy sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "energy"
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
|
||||
_attr_suggested_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
_attr_suggested_display_precision = 3
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AdaxCloudCoordinator,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
"""Initialize the energy sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._device_id = device_id
|
||||
room = coordinator.data[device_id]
|
||||
|
||||
self._attr_unique_id = f"{room['homeId']}_{device_id}_energy"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
name=room["name"],
|
||||
manufacturer="Adax",
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available and "energyWh" in self.coordinator.data[self._device_id]
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
"""Return the native value of the sensor."""
|
||||
return int(self.coordinator.data[self._device_id]["energyWh"])
|
||||
@@ -15,7 +15,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
)
|
||||
|
||||
from . import AgentDVRConfigEntry
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN as AGENT_DOMAIN
|
||||
from .const import ATTRIBUTION, CAMERA_SCAN_INTERVAL_SECS, DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=CAMERA_SCAN_INTERVAL_SECS)
|
||||
|
||||
@@ -82,7 +82,7 @@ class AgentCamera(MjpegCamera):
|
||||
still_image_url=f"{device.client._server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", # noqa: SLF001
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(AGENT_DOMAIN, self.unique_id)},
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
manufacturer="Agent",
|
||||
model="Camera",
|
||||
name=f"{device.client.name} {device.name}",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairq"],
|
||||
"requirements": ["aioairq==0.4.4"]
|
||||
"requirements": ["aioairq==0.4.6"]
|
||||
}
|
||||
|
||||
@@ -5,23 +5,22 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
from airthings import Airthings
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_SECRET, DOMAIN
|
||||
from .const import CONF_SECRET
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
type AirthingsDataCoordinatorType = DataUpdateCoordinator[dict[str, AirthingsDevice]]
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataCoordinatorType]
|
||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
||||
@@ -32,21 +31,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
||||
async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
async def _update_method() -> dict[str, AirthingsDevice]:
|
||||
"""Get the latest data from Airthings."""
|
||||
try:
|
||||
return await airthings.update_devices() # type: ignore[no-any-return]
|
||||
except AirthingsError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_method=_update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""The Airthings integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(minutes=6)
|
||||
|
||||
|
||||
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
|
||||
"""Coordinator for Airthings data updates."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_method=self._update_method,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.airthings = airthings
|
||||
|
||||
async def _update_method(self) -> dict[str, AirthingsDevice]:
|
||||
"""Get the latest data from Airthings."""
|
||||
try:
|
||||
return await self.airthings.update_devices() # type: ignore[no-any-return]
|
||||
except AirthingsError as err:
|
||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||
@@ -19,6 +19,7 @@ from homeassistant.const import (
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
EntityCategory,
|
||||
UnitOfPressure,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -27,8 +28,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AirthingsConfigEntry, AirthingsDataCoordinatorType
|
||||
from . import AirthingsConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirthingsDataUpdateCoordinator
|
||||
|
||||
SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"radonShortTermAvg": SensorEntityDescription(
|
||||
@@ -54,6 +56,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
native_unit_of_measurement=UnitOfPressure.MBAR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"sla": SensorEntityDescription(
|
||||
key="sla",
|
||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"battery": SensorEntityDescription(
|
||||
key="battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
@@ -140,7 +148,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class AirthingsHeaterEnergySensor(
|
||||
CoordinatorEntity[AirthingsDataCoordinatorType], SensorEntity
|
||||
CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Representation of a Airthings Sensor device."""
|
||||
|
||||
@@ -149,7 +157,7 @@ class AirthingsHeaterEnergySensor(
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirthingsDataCoordinatorType,
|
||||
coordinator: AirthingsDataUpdateCoordinator,
|
||||
airthings_device: AirthingsDevice,
|
||||
entity_description: SensorEntityDescription,
|
||||
) -> None:
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["airtouch5py"],
|
||||
"requirements": ["airtouch5py==0.2.11"]
|
||||
"requirements": ["airtouch5py==0.3.0"]
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
): CountrySelector(),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.positive_int,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Diagnostics support for Amazon Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AmazonConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
devices: list[dict[str, dict[str, Any]]] = [
|
||||
build_device_data(device) for device in coordinator.data.values()
|
||||
]
|
||||
|
||||
return {
|
||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
"device_info": {
|
||||
"last_update success": coordinator.last_update_success,
|
||||
"last_exception": repr(coordinator.last_exception),
|
||||
"devices": devices,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: AmazonConfigEntry, device_entry: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
assert device_entry.serial_number
|
||||
|
||||
return build_device_data(coordinator.data[device_entry.serial_number])
|
||||
|
||||
|
||||
def build_device_data(device: AmazonDevice) -> dict[str, Any]:
|
||||
"""Build device data for diagnostics."""
|
||||
return {
|
||||
"account name": device.account_name,
|
||||
"capabilities": device.capabilities,
|
||||
"device family": device.device_family,
|
||||
"device type": device.device_type,
|
||||
"device cluster members": device.device_cluster_members,
|
||||
"online": device.online,
|
||||
"serial number": device.serial_number,
|
||||
"software version": device.software_version,
|
||||
"do not disturb": device.do_not_disturb,
|
||||
"response style": device.response_style,
|
||||
"bluetooth state": device.bluetooth_state,
|
||||
}
|
||||
@@ -25,15 +25,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._serial_num = serial_num
|
||||
model_details = coordinator.api.get_model_details(self.device)
|
||||
model = model_details["model"] if model_details else None
|
||||
model_details = coordinator.api.get_model_details(self.device) or {}
|
||||
model = model_details.get("model")
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_num)},
|
||||
name=self.device.account_name,
|
||||
model=model,
|
||||
model_id=self.device.device_type,
|
||||
manufacturer="Amazon",
|
||||
hw_version=model_details["hw_version"] if model_details else None,
|
||||
manufacturer=model_details.get("manufacturer", "Amazon"),
|
||||
hw_version=model_details.get("hw_version"),
|
||||
sw_version=(
|
||||
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
|
||||
),
|
||||
@@ -50,4 +50,8 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self._serial_num in self.coordinator.data
|
||||
return (
|
||||
super().available
|
||||
and self._serial_num in self.coordinator.data
|
||||
and self.device.online
|
||||
)
|
||||
|
||||
@@ -4,30 +4,119 @@
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{ "macaddress": "007147*" },
|
||||
{ "macaddress": "00FC8B*" },
|
||||
{ "macaddress": "0812A5*" },
|
||||
{ "macaddress": "086AE5*" },
|
||||
{ "macaddress": "08849D*" },
|
||||
{ "macaddress": "089115*" },
|
||||
{ "macaddress": "08A6BC*" },
|
||||
{ "macaddress": "08C224*" },
|
||||
{ "macaddress": "0CDC91*" },
|
||||
{ "macaddress": "0CEE99*" },
|
||||
{ "macaddress": "1009F9*" },
|
||||
{ "macaddress": "109693*" },
|
||||
{ "macaddress": "10BF67*" },
|
||||
{ "macaddress": "10CE02*" },
|
||||
{ "macaddress": "140AC5*" },
|
||||
{ "macaddress": "149138*" },
|
||||
{ "macaddress": "1848BE*" },
|
||||
{ "macaddress": "1C12B0*" },
|
||||
{ "macaddress": "1C4D66*" },
|
||||
{ "macaddress": "1C93C4*" },
|
||||
{ "macaddress": "1CFE2B*" },
|
||||
{ "macaddress": "244CE3*" },
|
||||
{ "macaddress": "24CE33*" },
|
||||
{ "macaddress": "2873F6*" },
|
||||
{ "macaddress": "2C71FF*" },
|
||||
{ "macaddress": "34AFB3*" },
|
||||
{ "macaddress": "34D270*" },
|
||||
{ "macaddress": "38F73D*" },
|
||||
{ "macaddress": "3C5CC4*" },
|
||||
{ "macaddress": "3CE441*" },
|
||||
{ "macaddress": "440049*" },
|
||||
{ "macaddress": "40A2DB*" },
|
||||
{ "macaddress": "40A9CF*" },
|
||||
{ "macaddress": "40B4CD*" },
|
||||
{ "macaddress": "443D54*" },
|
||||
{ "macaddress": "44650D*" },
|
||||
{ "macaddress": "485F2D*" },
|
||||
{ "macaddress": "48785E*" },
|
||||
{ "macaddress": "48B423*" },
|
||||
{ "macaddress": "4C1744*" },
|
||||
{ "macaddress": "4CEFC0*" },
|
||||
{ "macaddress": "5007C3*" },
|
||||
{ "macaddress": "50D45C*" },
|
||||
{ "macaddress": "50DCE7*" },
|
||||
{ "macaddress": "50F5DA*" },
|
||||
{ "macaddress": "5C415A*" },
|
||||
{ "macaddress": "6837E9*" },
|
||||
{ "macaddress": "6854FD*" },
|
||||
{ "macaddress": "689A87*" },
|
||||
{ "macaddress": "68B691*" },
|
||||
{ "macaddress": "68DBF5*" },
|
||||
{ "macaddress": "68F63B*" },
|
||||
{ "macaddress": "6C0C9A*" },
|
||||
{ "macaddress": "6C5697*" },
|
||||
{ "macaddress": "7458F3*" },
|
||||
{ "macaddress": "74C246*" },
|
||||
{ "macaddress": "74D637*" },
|
||||
{ "macaddress": "74E20C*" },
|
||||
{ "macaddress": "74ECB2*" },
|
||||
{ "macaddress": "786C84*" },
|
||||
{ "macaddress": "78A03F*" },
|
||||
{ "macaddress": "7C6166*" },
|
||||
{ "macaddress": "7C6305*" },
|
||||
{ "macaddress": "7CD566*" },
|
||||
{ "macaddress": "8871E5*" },
|
||||
{ "macaddress": "901195*" },
|
||||
{ "macaddress": "90235B*" },
|
||||
{ "macaddress": "90A822*" },
|
||||
{ "macaddress": "90F82E*" },
|
||||
{ "macaddress": "943A91*" },
|
||||
{ "macaddress": "98226E*" },
|
||||
{ "macaddress": "98CCF3*" },
|
||||
{ "macaddress": "9CC8E9*" },
|
||||
{ "macaddress": "A002DC*" },
|
||||
{ "macaddress": "A0D2B1*" },
|
||||
{ "macaddress": "A40801*" },
|
||||
{ "macaddress": "A8E621*" },
|
||||
{ "macaddress": "AC416A*" },
|
||||
{ "macaddress": "AC63BE*" },
|
||||
{ "macaddress": "ACCCFC*" },
|
||||
{ "macaddress": "B0739C*" },
|
||||
{ "macaddress": "B0CFCB*" },
|
||||
{ "macaddress": "B0F7C4*" },
|
||||
{ "macaddress": "B85F98*" },
|
||||
{ "macaddress": "C091B9*" },
|
||||
{ "macaddress": "C095CF*" },
|
||||
{ "macaddress": "C49500*" },
|
||||
{ "macaddress": "C86C3D*" },
|
||||
{ "macaddress": "CC9EA2*" },
|
||||
{ "macaddress": "CCF735*" },
|
||||
{ "macaddress": "DC54D7*" },
|
||||
{ "macaddress": "D8BE65*" },
|
||||
{ "macaddress": "EC2BEB*" }
|
||||
{ "macaddress": "D8FBD6*" },
|
||||
{ "macaddress": "DC91BF*" },
|
||||
{ "macaddress": "DCA0D0*" },
|
||||
{ "macaddress": "E0F728*" },
|
||||
{ "macaddress": "EC2BEB*" },
|
||||
{ "macaddress": "EC8AC4*" },
|
||||
{ "macaddress": "ECA138*" },
|
||||
{ "macaddress": "F02F9E*" },
|
||||
{ "macaddress": "F0272D*" },
|
||||
{ "macaddress": "F0F0A4*" },
|
||||
{ "macaddress": "F4032A*" },
|
||||
{ "macaddress": "F854B8*" },
|
||||
{ "macaddress": "FC492D*" },
|
||||
{ "macaddress": "FC65DE*" },
|
||||
{ "macaddress": "FCA183*" },
|
||||
{ "macaddress": "FCE9D8*" }
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==2.1.1"]
|
||||
"requirements": ["aioamazondevices==3.0.6"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"data_description_country": "The country of your Amazon account.",
|
||||
"data_description_username": "The email address of your Amazon account.",
|
||||
"data_description_password": "The password of your Amazon account.",
|
||||
"data_description_code": "The one-time password sent to your email address."
|
||||
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{username}",
|
||||
|
||||
@@ -16,10 +16,7 @@ from amcrest import AmcrestError, ApiWrapper, LoginError
|
||||
import httpx
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.auth.permissions.const import POLICY_CONTROL
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_AUTHENTICATION,
|
||||
CONF_BINARY_SENSORS,
|
||||
CONF_HOST,
|
||||
@@ -30,21 +27,17 @@ from homeassistant.const import (
|
||||
CONF_SENSORS,
|
||||
CONF_SWITCHES,
|
||||
CONF_USERNAME,
|
||||
ENTITY_MATCH_ALL,
|
||||
ENTITY_MATCH_NONE,
|
||||
HTTP_BASIC_AUTHENTICATION,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .binary_sensor import BINARY_SENSOR_KEYS, BINARY_SENSORS, check_binary_sensors
|
||||
from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST
|
||||
from .camera import STREAM_SOURCE_LIST
|
||||
from .const import (
|
||||
CAMERAS,
|
||||
COMM_RETRIES,
|
||||
@@ -58,6 +51,7 @@ from .const import (
|
||||
)
|
||||
from .helpers import service_signal
|
||||
from .sensor import SENSOR_KEYS
|
||||
from .services import async_setup_services
|
||||
from .switch import SWITCH_KEYS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -455,47 +449,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if not hass.data[DATA_AMCREST][DEVICES]:
|
||||
return False
|
||||
|
||||
def have_permission(user: User | None, entity_id: str) -> bool:
|
||||
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
|
||||
|
||||
async def async_extract_from_service(call: ServiceCall) -> list[str]:
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
if user is None:
|
||||
raise UnknownUser(context=call.context)
|
||||
else:
|
||||
user = None
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
|
||||
# Return all entity_ids user has permission to control.
|
||||
return [
|
||||
entity_id
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
|
||||
if have_permission(user, entity_id)
|
||||
]
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
|
||||
return []
|
||||
|
||||
call_ids = await async_extract_entity_ids(hass, call)
|
||||
entity_ids = []
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
|
||||
if entity_id not in call_ids:
|
||||
continue
|
||||
if not have_permission(user, entity_id):
|
||||
raise Unauthorized(
|
||||
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
|
||||
)
|
||||
entity_ids.append(entity_id)
|
||||
return entity_ids
|
||||
|
||||
async def async_service_handler(call: ServiceCall) -> None:
|
||||
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
|
||||
for entity_id in await async_extract_from_service(call):
|
||||
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
|
||||
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Support for Amcrest IP cameras."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.auth.permissions.const import POLICY_CONTROL
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import Unauthorized, UnknownUser
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.service import async_extract_entity_ids
|
||||
|
||||
from .camera import CAMERA_SERVICES
|
||||
from .const import CAMERAS, DATA_AMCREST, DOMAIN
|
||||
from .helpers import service_signal
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the Amcrest IP Camera services."""
|
||||
|
||||
def have_permission(user: User | None, entity_id: str) -> bool:
|
||||
return not user or user.permissions.check_entity(entity_id, POLICY_CONTROL)
|
||||
|
||||
async def async_extract_from_service(call: ServiceCall) -> list[str]:
|
||||
if call.context.user_id:
|
||||
user = await hass.auth.async_get_user(call.context.user_id)
|
||||
if user is None:
|
||||
raise UnknownUser(context=call.context)
|
||||
else:
|
||||
user = None
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_ALL:
|
||||
# Return all entity_ids user has permission to control.
|
||||
return [
|
||||
entity_id
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]
|
||||
if have_permission(user, entity_id)
|
||||
]
|
||||
|
||||
if call.data.get(ATTR_ENTITY_ID) == ENTITY_MATCH_NONE:
|
||||
return []
|
||||
|
||||
call_ids = await async_extract_entity_ids(hass, call)
|
||||
entity_ids = []
|
||||
for entity_id in hass.data[DATA_AMCREST][CAMERAS]:
|
||||
if entity_id not in call_ids:
|
||||
continue
|
||||
if not have_permission(user, entity_id):
|
||||
raise Unauthorized(
|
||||
context=call.context, entity_id=entity_id, permission=POLICY_CONTROL
|
||||
)
|
||||
entity_ids.append(entity_id)
|
||||
return entity_ids
|
||||
|
||||
async def async_service_handler(call: ServiceCall) -> None:
|
||||
args = [call.data[arg] for arg in CAMERA_SERVICES[call.service][2]]
|
||||
for entity_id in await async_extract_from_service(call):
|
||||
async_dispatcher_send(hass, service_signal(call.service, entity_id), *args)
|
||||
|
||||
for service, params in CAMERA_SERVICES.items():
|
||||
hass.services.async_register(DOMAIN, service, async_service_handler, params[0])
|
||||
@@ -24,7 +24,7 @@ from homeassistant.components.recorder import (
|
||||
get_instance as get_recorder_instance,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.const import ATTR_DOMAIN, __version__ as HA_VERSION
|
||||
from homeassistant.const import ATTR_DOMAIN, BASE_PLATFORMS, __version__ as HA_VERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -225,7 +225,8 @@ class Analytics:
|
||||
LOGGER.error(err)
|
||||
return
|
||||
|
||||
configuration_set = set(yaml_configuration)
|
||||
configuration_set = _domains_from_yaml_config(yaml_configuration)
|
||||
|
||||
er_platforms = {
|
||||
entity.platform
|
||||
for entity in ent_reg.entities.values()
|
||||
@@ -370,3 +371,13 @@ class Analytics:
|
||||
for entry in entries
|
||||
if entry.source != SOURCE_IGNORE and entry.disabled_by is None
|
||||
)
|
||||
|
||||
|
||||
def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
||||
"""Extract domains from the YAML configuration."""
|
||||
domains = set(yaml_configuration)
|
||||
for platforms in conf_util.extract_platform_integrations(
|
||||
yaml_configuration, BASE_PLATFORMS
|
||||
).values():
|
||||
domains.update(platforms)
|
||||
return domains
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyaprilaire"],
|
||||
"requirements": ["pyaprilaire==0.9.0"]
|
||||
"requirements": ["pyaprilaire==0.9.1"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ from homeassistant.components.water_heater import (
|
||||
STATE_ECO,
|
||||
STATE_PERFORMANCE,
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, STATE_OFF, Platform, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -32,6 +33,7 @@ class AtagWaterHeater(AtagEntity, WaterHeaterEntity):
|
||||
"""Representation of an ATAG water heater."""
|
||||
|
||||
_attr_operation_list = OPERATION_LIST
|
||||
_attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
@property
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from ..const import ATTR_MANUFACTURER, DOMAIN as AXIS_DOMAIN
|
||||
from ..const import ATTR_MANUFACTURER, DOMAIN
|
||||
from .config import AxisConfig
|
||||
from .entity_loader import AxisEntityLoader
|
||||
from .event_source import AxisEventSource
|
||||
@@ -79,7 +79,7 @@ class AxisHub:
|
||||
config_entry_id=self.config.entry.entry_id,
|
||||
configuration_url=self.api.config.url,
|
||||
connections={(CONNECTION_NETWORK_MAC, self.unique_id)},
|
||||
identifiers={(AXIS_DOMAIN, self.unique_id)},
|
||||
identifiers={(DOMAIN, self.unique_id)},
|
||||
manufacturer=ATTR_MANUFACTURER,
|
||||
model=f"{self.config.model} {self.product_type}",
|
||||
name=self.config.name,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, discovery_flow
|
||||
from homeassistant.helpers.backup import DATA_BACKUP
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
@@ -45,6 +45,7 @@ from .manager import (
|
||||
WrittenBackup,
|
||||
)
|
||||
from .models import AddonInfo, AgentBackup, BackupNotFound, Folder
|
||||
from .services import async_setup_services
|
||||
from .util import suggested_filename, suggested_filename_from_name_date
|
||||
from .websocket import async_register_websocket_handlers
|
||||
|
||||
@@ -111,29 +112,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async_register_websocket_handlers(hass, with_hassio)
|
||||
|
||||
async def async_handle_create_service(call: ServiceCall) -> None:
|
||||
"""Service handler for creating backups."""
|
||||
agent_id = list(backup_manager.local_backup_agents)[0]
|
||||
await backup_manager.async_create_backup(
|
||||
agent_ids=[agent_id],
|
||||
include_addons=None,
|
||||
include_all_addons=False,
|
||||
include_database=True,
|
||||
include_folders=None,
|
||||
include_homeassistant=True,
|
||||
name=None,
|
||||
password=None,
|
||||
)
|
||||
|
||||
async def async_handle_create_automatic_service(call: ServiceCall) -> None:
|
||||
"""Service handler for creating automatic backups."""
|
||||
await backup_manager.async_create_automatic_backup()
|
||||
|
||||
if not with_hassio:
|
||||
hass.services.async_register(DOMAIN, "create", async_handle_create_service)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "create_automatic", async_handle_create_automatic_service
|
||||
)
|
||||
async_setup_services(hass)
|
||||
|
||||
async_register_http_views(hass)
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
"""The Backup integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
|
||||
from .const import DATA_MANAGER, DOMAIN
|
||||
|
||||
|
||||
async def _async_handle_create_service(call: ServiceCall) -> None:
|
||||
"""Service handler for creating backups."""
|
||||
backup_manager = call.hass.data[DATA_MANAGER]
|
||||
agent_id = list(backup_manager.local_backup_agents)[0]
|
||||
await backup_manager.async_create_backup(
|
||||
agent_ids=[agent_id],
|
||||
include_addons=None,
|
||||
include_all_addons=False,
|
||||
include_database=True,
|
||||
include_folders=None,
|
||||
include_homeassistant=True,
|
||||
name=None,
|
||||
password=None,
|
||||
)
|
||||
|
||||
|
||||
async def _async_handle_create_automatic_service(call: ServiceCall) -> None:
|
||||
"""Service handler for creating automatic backups."""
|
||||
await call.hass.data[DATA_MANAGER].async_create_automatic_backup()
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services."""
|
||||
if not is_hassio(hass):
|
||||
hass.services.async_register(DOMAIN, "create", _async_handle_create_service)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "create_automatic", _async_handle_create_automatic_service
|
||||
)
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.48.2"
|
||||
"habluetooth==3.49.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
|
||||
|
||||
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
|
||||
"""Initialise a Bosch Alarm control panel entity."""
|
||||
super().__init__(panel, area_id, unique_id, False, False, True)
|
||||
super().__init__(panel, area_id, unique_id, True, False, True)
|
||||
self._attr_unique_id = self._area_unique_id
|
||||
|
||||
@property
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"requirements": ["python-bsblan==1.2.1"]
|
||||
"requirements": ["python-bsblan==2.1.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/camera",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["PyTurboJPEG==1.7.5"]
|
||||
"requirements": ["PyTurboJPEG==1.8.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/compensation",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["numpy==2.2.2"]
|
||||
"requirements": ["numpy==2.3.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT, CONF_ID
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import CONF_GESTURE, DOMAIN as DECONZ_DOMAIN
|
||||
from .const import CONF_GESTURE, DOMAIN
|
||||
from .deconz_event import CONF_DECONZ_ALARM_EVENT, CONF_DECONZ_EVENT
|
||||
from .device_trigger import (
|
||||
CONF_BOTH_BUTTONS,
|
||||
@@ -200,6 +200,6 @@ def async_describe_events(
|
||||
}
|
||||
|
||||
async_describe_event(
|
||||
DECONZ_DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
|
||||
DOMAIN, CONF_DECONZ_ALARM_EVENT, async_describe_deconz_alarm_event
|
||||
)
|
||||
async_describe_event(DECONZ_DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
|
||||
async_describe_event(DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The decora component."""
|
||||
|
||||
DOMAIN = "decora"
|
||||
|
||||
@@ -21,7 +21,11 @@ from homeassistant.components.light import (
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -90,6 +94,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up an Decora switch."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Leviton Decora",
|
||||
},
|
||||
)
|
||||
|
||||
lights = []
|
||||
for address, device_config in config[CONF_DEVICES].items():
|
||||
device = {}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The dlib_face_detect component."""
|
||||
|
||||
DOMAIN = "dlib_face_detect"
|
||||
|
||||
@@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
|
||||
ImageProcessingFaceEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
|
||||
|
||||
|
||||
@@ -25,6 +32,20 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Dlib Face Detect",
|
||||
},
|
||||
)
|
||||
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||
add_entities(
|
||||
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
"""The dlib_face_identify component."""
|
||||
|
||||
CONF_FACES = "faces"
|
||||
DOMAIN = "dlib_face_identify"
|
||||
|
||||
@@ -15,14 +15,20 @@ from homeassistant.components.image_processing import (
|
||||
ImageProcessingFaceEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_FACES, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FACES = "faces"
|
||||
|
||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
@@ -39,6 +45,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Dlib Face Identify",
|
||||
},
|
||||
)
|
||||
|
||||
confidence: float = config[CONF_CONFIDENCE]
|
||||
faces: dict[str, str] = config[CONF_FACES]
|
||||
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
@@ -62,16 +62,16 @@ async def async_validate_hostname(
|
||||
"""Validate hostname."""
|
||||
|
||||
async def async_check(
|
||||
hostname: str, resolver: str, qtype: str, port: int = 53
|
||||
hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53
|
||||
) -> bool:
|
||||
"""Return if able to resolve hostname."""
|
||||
result = False
|
||||
result: bool = False
|
||||
with contextlib.suppress(DNSError):
|
||||
result = bool(
|
||||
await aiodns.DNSResolver( # type: ignore[call-overload]
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
).query(hostname, qtype)
|
||||
_resolver = aiodns.DNSResolver(
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
)
|
||||
result = bool(await _resolver.query(hostname, qtype))
|
||||
|
||||
return result
|
||||
|
||||
result: dict[str, bool] = {}
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
from ipaddress import IPv4Address, IPv6Address
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
@@ -34,7 +35,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
|
||||
def sort_ips(ips: list, querytype: str) -> list:
|
||||
def sort_ips(ips: list, querytype: Literal["A", "AAAA"]) -> list:
|
||||
"""Join IPs into a single string."""
|
||||
|
||||
if querytype == "AAAA":
|
||||
@@ -89,7 +90,7 @@ class WanIpSensor(SensorEntity):
|
||||
self.hostname = hostname
|
||||
self.resolver = aiodns.DNSResolver(tcp_port=port, udp_port=port)
|
||||
self.resolver.nameservers = [resolver]
|
||||
self.querytype = "AAAA" if ipv6 else "A"
|
||||
self.querytype: Literal["A", "AAAA"] = "AAAA" if ipv6 else "A"
|
||||
self._retries = DEFAULT_RETRIES
|
||||
self._attr_extra_state_attributes = {
|
||||
"resolver": resolver,
|
||||
@@ -106,7 +107,7 @@ class WanIpSensor(SensorEntity):
|
||||
async def async_update(self) -> None:
|
||||
"""Get the current DNS IP address for hostname."""
|
||||
try:
|
||||
response = await self.resolver.query(self.hostname, self.querytype) # type: ignore[call-overload]
|
||||
response = await self.resolver.query(self.hostname, self.querytype)
|
||||
except DNSError as err:
|
||||
_LOGGER.warning("Exception while resolving host: %s", err)
|
||||
response = None
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import _LOGGER, CONF_DOWNLOAD_DIR
|
||||
from .services import register_services
|
||||
from .services import async_setup_services
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -25,6 +25,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
return False
|
||||
|
||||
register_services(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
@@ -141,7 +141,7 @@ def download_file(service: ServiceCall) -> None:
|
||||
threading.Thread(target=do_download).start()
|
||||
|
||||
|
||||
def register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register the services for the downloader component."""
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
"""The eddystone_temperature component."""
|
||||
|
||||
DOMAIN = "eddystone_temperature"
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
@@ -23,17 +23,18 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_BT_DEVICE_ID = "bt_device_id"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
|
||||
BEACON_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -58,6 +59,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Validate configuration, create devices and start monitoring thread."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Eddystone",
|
||||
},
|
||||
)
|
||||
|
||||
bt_device_id: int = config[CONF_BT_DEVICE_ID]
|
||||
|
||||
beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]
|
||||
|
||||
@@ -8,7 +8,7 @@ import re
|
||||
from typing import Any
|
||||
|
||||
from elkm1_lib.elements import Element
|
||||
from elkm1_lib.elk import Elk, Panel
|
||||
from elkm1_lib.elk import Elk
|
||||
from elkm1_lib.util import parse_url
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -26,12 +26,11 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.network import is_ip_address
|
||||
|
||||
from .const import (
|
||||
@@ -62,6 +61,7 @@ from .discovery import (
|
||||
async_update_entry_from_discovery,
|
||||
)
|
||||
from .models import ELKM1Data
|
||||
from .services import async_setup_services
|
||||
|
||||
type ElkM1ConfigEntry = ConfigEntry[ELKM1Data]
|
||||
|
||||
@@ -79,19 +79,6 @@ PLATFORMS = [
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
SPEAK_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("number"): vol.All(vol.Coerce(int), vol.Range(min=0, max=999)),
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SET_TIME_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def hostname_from_url(url: str) -> str:
|
||||
"""Return the hostname from a url."""
|
||||
@@ -179,7 +166,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
|
||||
"""Set up the Elk M1 platform."""
|
||||
_create_elk_services(hass)
|
||||
async_setup_services(hass)
|
||||
|
||||
async def _async_discovery(*_: Any) -> None:
|
||||
async_trigger_discovery(
|
||||
@@ -326,17 +313,6 @@ def _included(ranges: list[tuple[int, int]], set_to: bool, values: list[bool]) -
|
||||
values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
|
||||
|
||||
|
||||
def _find_elk_by_prefix(hass: HomeAssistant, prefix: str) -> Elk | None:
|
||||
"""Search all config entries for a given prefix."""
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if not entry.runtime_data:
|
||||
continue
|
||||
elk_data: ELKM1Data = entry.runtime_data
|
||||
if elk_data.prefix == prefix:
|
||||
return elk_data.elk
|
||||
return None
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ElkM1ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -390,39 +366,3 @@ async def async_wait_for_elk_to_sync(
|
||||
_LOGGER.debug("Received %s event", name)
|
||||
|
||||
return success
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_elk_panel(hass: HomeAssistant, service: ServiceCall) -> Panel:
|
||||
"""Get the ElkM1 panel from a service call."""
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk.panel
|
||||
|
||||
|
||||
def _create_elk_services(hass: HomeAssistant) -> None:
|
||||
"""Create ElkM1 services."""
|
||||
|
||||
@callback
|
||||
def _speak_word_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(hass, service).speak_word(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _speak_phrase_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(hass, service).speak_phrase(service.data["number"])
|
||||
|
||||
@callback
|
||||
def _set_time_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(hass, service).set_time(dt_util.now())
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_phrase", _speak_phrase_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "set_time", _set_time_service, SET_TIME_SERVICE_SCHEMA
|
||||
)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from elkm1_lib.elk import Elk, Panel
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .models import ELKM1Data
|
||||
|
||||
SPEAK_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("number"): vol.All(vol.Coerce(int), vol.Range(min=0, max=999)),
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
SET_TIME_SERVICE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional("prefix", default=""): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _find_elk_by_prefix(hass: HomeAssistant, prefix: str) -> Elk | None:
|
||||
"""Search all config entries for a given prefix."""
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if not entry.runtime_data:
|
||||
continue
|
||||
elk_data: ELKM1Data = entry.runtime_data
|
||||
if elk_data.prefix == prefix:
|
||||
return elk_data.elk
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_elk_panel(service: ServiceCall) -> Panel:
|
||||
"""Get the ElkM1 panel from a service call."""
|
||||
prefix = service.data["prefix"]
|
||||
elk = _find_elk_by_prefix(service.hass, prefix)
|
||||
if elk is None:
|
||||
raise HomeAssistantError(f"No ElkM1 with prefix '{prefix}' found")
|
||||
return elk.panel
|
||||
|
||||
|
||||
@callback
|
||||
def _speak_word_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(service).speak_word(service.data["number"])
|
||||
|
||||
|
||||
@callback
|
||||
def _speak_phrase_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(service).speak_phrase(service.data["number"])
|
||||
|
||||
|
||||
@callback
|
||||
def _set_time_service(service: ServiceCall) -> None:
|
||||
_async_get_elk_panel(service).set_time(dt_util.now())
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Create ElkM1 services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_word", _speak_word_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "speak_phrase", _speak_phrase_service, SPEAK_SERVICE_SCHEMA
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN, "set_time", _set_time_service, SET_TIME_SERVICE_SCHEMA
|
||||
)
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import httpx
|
||||
from pyenphase import Envoy
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -10,14 +9,9 @@ from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
OPTION_DISABLE_KEEP_ALIVE,
|
||||
OPTION_DISABLE_KEEP_ALIVE_DEFAULT_VALUE,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator
|
||||
|
||||
|
||||
@@ -25,19 +19,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: EnphaseConfigEntry) -> b
|
||||
"""Set up Enphase Envoy from a config entry."""
|
||||
|
||||
host = entry.data[CONF_HOST]
|
||||
options = entry.options
|
||||
envoy = (
|
||||
Envoy(
|
||||
host,
|
||||
httpx.AsyncClient(
|
||||
verify=False, limits=httpx.Limits(max_keepalive_connections=0)
|
||||
),
|
||||
)
|
||||
if options.get(
|
||||
OPTION_DISABLE_KEEP_ALIVE, OPTION_DISABLE_KEEP_ALIVE_DEFAULT_VALUE
|
||||
)
|
||||
else Envoy(host, get_async_client(hass, verify_ssl=False))
|
||||
)
|
||||
session = async_create_clientsession(hass, verify_ssl=False)
|
||||
envoy = Envoy(host, session)
|
||||
coordinator = EnphaseUpdateCoordinator(hass, envoy, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
@@ -63,7 +63,7 @@ async def validate_input(
|
||||
description_placeholders: dict[str, str],
|
||||
) -> Envoy:
|
||||
"""Validate the user input allows us to connect."""
|
||||
envoy = Envoy(host, get_async_client(hass, verify_ssl=False))
|
||||
envoy = Envoy(host, async_get_clientsession(hass, verify_ssl=False))
|
||||
try:
|
||||
await envoy.setup()
|
||||
await envoy.authenticate(username=username, password=password)
|
||||
|
||||
@@ -6,6 +6,7 @@ import copy
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohttp import ClientResponse
|
||||
from attr import asdict
|
||||
from pyenphase.envoy import Envoy
|
||||
from pyenphase.exceptions import EnvoyError
|
||||
@@ -69,14 +70,14 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
|
||||
|
||||
for end_point in end_points:
|
||||
try:
|
||||
response = await envoy.request(end_point)
|
||||
fixture_data[end_point] = response.text.replace("\n", "").replace(
|
||||
serial, CLEAN_TEXT
|
||||
response: ClientResponse = await envoy.request(end_point)
|
||||
fixture_data[end_point] = (
|
||||
(await response.text()).replace("\n", "").replace(serial, CLEAN_TEXT)
|
||||
)
|
||||
fixture_data[f"{end_point}_log"] = json_dumps(
|
||||
{
|
||||
"headers": dict(response.headers.items()),
|
||||
"code": response.status_code,
|
||||
"code": response.status,
|
||||
}
|
||||
)
|
||||
except EnvoyError as err:
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from httpx import HTTPError
|
||||
from aiohttp import ClientError
|
||||
from pyenphase import EnvoyData
|
||||
from pyenphase.exceptions import EnvoyError
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EnphaseUpdateCoordinator
|
||||
|
||||
ACTIONERRORS = (EnvoyError, HTTPError)
|
||||
ACTIONERRORS = (EnvoyError, ClientError)
|
||||
|
||||
|
||||
class EnvoyBaseEntity(CoordinatorEntity[EnphaseUpdateCoordinator]):
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==1.26.1"],
|
||||
"requirements": ["pyenphase==2.0.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.10.2"]
|
||||
"requirements": ["env-canada==0.11.2"]
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"]
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==31.1.0",
|
||||
"aioesphomeapi==32.2.1",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.15.1"
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -71,6 +71,11 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
_attr_name = "DHW controller"
|
||||
_attr_icon = "mdi:thermometer-lines"
|
||||
_attr_operation_list = list(HA_STATE_TO_EVO)
|
||||
_attr_supported_features = (
|
||||
WaterHeaterEntityFeature.AWAY_MODE
|
||||
| WaterHeaterEntityFeature.ON_OFF
|
||||
| WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
_evo_device: evo.HotWater
|
||||
@@ -91,9 +96,6 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
self._attr_precision = (
|
||||
PRECISION_TENTHS if coordinator.client_v1 else PRECISION_WHOLE
|
||||
)
|
||||
self._attr_supported_features = (
|
||||
WaterHeaterEntityFeature.AWAY_MODE | WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str | None:
|
||||
|
||||
@@ -11,32 +11,25 @@ from propcache.api import cached_property
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONTENT_TYPE_MULTIPART,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
from homeassistant.util.system_info import is_official_image
|
||||
|
||||
DOMAIN = "ffmpeg"
|
||||
|
||||
SERVICE_START = "start"
|
||||
SERVICE_STOP = "stop"
|
||||
SERVICE_RESTART = "restart"
|
||||
|
||||
SIGNAL_FFMPEG_START = SignalType[list[str] | None]("ffmpeg.start")
|
||||
SIGNAL_FFMPEG_STOP = SignalType[list[str] | None]("ffmpeg.stop")
|
||||
SIGNAL_FFMPEG_RESTART = SignalType[list[str] | None]("ffmpeg.restart")
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SIGNAL_FFMPEG_RESTART,
|
||||
SIGNAL_FFMPEG_START,
|
||||
SIGNAL_FFMPEG_STOP,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
DATA_FFMPEG = "ffmpeg"
|
||||
|
||||
@@ -63,8 +56,6 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_FFMPEG_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the FFmpeg component."""
|
||||
@@ -74,29 +65,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
await manager.async_get_version()
|
||||
|
||||
# Register service
|
||||
async def async_service_handle(service: ServiceCall) -> None:
|
||||
"""Handle service ffmpeg process."""
|
||||
entity_ids: list[str] | None = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if service.service == SERVICE_START:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_START, entity_ids)
|
||||
elif service.service == SERVICE_STOP:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_STOP, entity_ids)
|
||||
else:
|
||||
async_dispatcher_send(hass, SIGNAL_FFMPEG_RESTART, entity_ids)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_START, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_STOP, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESTART, async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
async_setup_services(hass)
|
||||
|
||||
hass.data[DATA_FFMPEG] = manager
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Support for FFmpeg."""
|
||||
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
|
||||
DOMAIN = "ffmpeg"
|
||||
|
||||
SIGNAL_FFMPEG_START = SignalType[list[str] | None]("ffmpeg.start")
|
||||
SIGNAL_FFMPEG_STOP = SignalType[list[str] | None]("ffmpeg.stop")
|
||||
SIGNAL_FFMPEG_RESTART = SignalType[list[str] | None]("ffmpeg.restart")
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Support for FFmpeg."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
SIGNAL_FFMPEG_RESTART,
|
||||
SIGNAL_FFMPEG_START,
|
||||
SIGNAL_FFMPEG_STOP,
|
||||
)
|
||||
|
||||
SERVICE_START = "start"
|
||||
SERVICE_STOP = "stop"
|
||||
SERVICE_RESTART = "restart"
|
||||
|
||||
SERVICE_FFMPEG_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids})
|
||||
|
||||
|
||||
async def _async_service_handle(service: ServiceCall) -> None:
|
||||
"""Handle service ffmpeg process."""
|
||||
entity_ids: list[str] | None = service.data.get(ATTR_ENTITY_ID)
|
||||
|
||||
if service.service == SERVICE_START:
|
||||
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_START, entity_ids)
|
||||
elif service.service == SERVICE_STOP:
|
||||
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_STOP, entity_ids)
|
||||
else:
|
||||
async_dispatcher_send(service.hass, SIGNAL_FFMPEG_RESTART, entity_ids)
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register FFmpeg services."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_START, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_STOP, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, SERVICE_RESTART, _async_service_handle, schema=SERVICE_FFMPEG_SCHEMA
|
||||
)
|
||||
@@ -84,6 +84,7 @@ async def async_setup_entry(
|
||||
name=f"Freebox {sensor_name}",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
for sensor_name in router.sensors_temperature
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250526.0"]
|
||||
"requirements": ["home-assistant-frontend==20250531.0"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["go2rtc-client==0.1.3b0"],
|
||||
"requirements": ["go2rtc-client==0.2.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==9.2.5"]
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Google Mail platform."""
|
||||
"""Set up the Google Mail integration."""
|
||||
hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -52,8 +54,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoogleMailConfigEntry) -
|
||||
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
|
||||
)
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -7,17 +7,26 @@ from google_photos_library_api.api import GooglePhotosLibraryApi
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import api
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GooglePhotosConfigEntry, GooglePhotosUpdateCoordinator
|
||||
from .services import async_register_services
|
||||
from .services import async_setup_services
|
||||
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
]
|
||||
__all__ = ["DOMAIN"]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Google Photos integration."""
|
||||
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -48,8 +57,6 @@ async def async_setup_entry(
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
async_register_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ def _read_file_contents(
|
||||
return results
|
||||
|
||||
|
||||
def async_register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register Google Photos services."""
|
||||
|
||||
async def async_handle_upload(call: ServiceCall) -> ServiceResponse:
|
||||
@@ -152,11 +152,10 @@ def async_register_services(hass: HomeAssistant) -> None:
|
||||
}
|
||||
return None
|
||||
|
||||
if not hass.services.has_service(DOMAIN, UPLOAD_SERVICE):
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
UPLOAD_SERVICE,
|
||||
async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
UPLOAD_SERVICE,
|
||||
async_handle_upload,
|
||||
schema=UPLOAD_SERVICE_SCHEMA,
|
||||
supports_response=SupportsResponse.OPTIONAL,
|
||||
)
|
||||
|
||||
@@ -2,48 +2,33 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import aiohttp
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from gspread import Client
|
||||
from gspread.exceptions import APIError
|
||||
from gspread.utils import ValueInputOption
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DEFAULT_ACCESS, DOMAIN
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
type GoogleSheetsConfigEntry = ConfigEntry[OAuth2Session]
|
||||
|
||||
DATA = "data"
|
||||
DATA_CONFIG_ENTRY = "config_entry"
|
||||
WORKSHEET = "worksheet"
|
||||
|
||||
SERVICE_APPEND_SHEET = "append_sheet"
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Activate the Google Sheets component."""
|
||||
|
||||
SHEET_SERVICE_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
||||
vol.Optional(WORKSHEET): cv.string,
|
||||
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
|
||||
},
|
||||
)
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -67,8 +52,6 @@ async def async_setup_entry(
|
||||
raise ConfigEntryAuthFailed("Required scopes are not present, reauth required")
|
||||
entry.runtime_data = session
|
||||
|
||||
await async_setup_service(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -81,55 +64,4 @@ async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: GoogleSheetsConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for service_name in hass.services.async_services_for_domain(DOMAIN):
|
||||
hass.services.async_remove(DOMAIN, service_name)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_service(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Sheets."""
|
||||
|
||||
def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
|
||||
"""Run append in the executor."""
|
||||
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
|
||||
try:
|
||||
sheet = service.open_by_key(entry.unique_id)
|
||||
except RefreshError:
|
||||
entry.async_start_reauth(hass)
|
||||
raise
|
||||
except APIError as ex:
|
||||
raise HomeAssistantError("Failed to write data") from ex
|
||||
|
||||
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
|
||||
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
|
||||
now = str(datetime.now())
|
||||
rows = []
|
||||
for d in call.data[DATA]:
|
||||
row_data = {"created": now} | d
|
||||
row = [row_data.get(column, "") for column in columns]
|
||||
for key, value in row_data.items():
|
||||
if key not in columns:
|
||||
columns.append(key)
|
||||
worksheet.update_cell(1, len(columns), key)
|
||||
row.append(value)
|
||||
rows.append(row)
|
||||
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
|
||||
|
||||
async def append_to_sheet(call: ServiceCall) -> None:
|
||||
"""Append new line of data to a Google Sheets document."""
|
||||
entry: GoogleSheetsConfigEntry | None = hass.config_entries.async_get_entry(
|
||||
call.data[DATA_CONFIG_ENTRY]
|
||||
)
|
||||
if not entry or not hasattr(entry, "runtime_data"):
|
||||
raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}")
|
||||
await entry.runtime_data.async_ensure_token_valid()
|
||||
await hass.async_add_executor_job(_append_to_sheet, call, entry)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_APPEND_SHEET,
|
||||
append_to_sheet,
|
||||
schema=SHEET_SERVICE_SCHEMA,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Support for Google Sheets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from gspread import Client
|
||||
from gspread.exceptions import APIError
|
||||
from gspread.utils import ValueInputOption
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import GoogleSheetsConfigEntry
|
||||
|
||||
DATA = "data"
|
||||
DATA_CONFIG_ENTRY = "config_entry"
|
||||
WORKSHEET = "worksheet"
|
||||
|
||||
SERVICE_APPEND_SHEET = "append_sheet"
|
||||
|
||||
SHEET_SERVICE_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
||||
vol.Optional(WORKSHEET): cv.string,
|
||||
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _append_to_sheet(call: ServiceCall, entry: GoogleSheetsConfigEntry) -> None:
|
||||
"""Run append in the executor."""
|
||||
service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) # type: ignore[no-untyped-call]
|
||||
try:
|
||||
sheet = service.open_by_key(entry.unique_id)
|
||||
except RefreshError:
|
||||
entry.async_start_reauth(call.hass)
|
||||
raise
|
||||
except APIError as ex:
|
||||
raise HomeAssistantError("Failed to write data") from ex
|
||||
|
||||
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
|
||||
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
|
||||
now = str(datetime.now())
|
||||
rows = []
|
||||
for d in call.data[DATA]:
|
||||
row_data = {"created": now} | d
|
||||
row = [row_data.get(column, "") for column in columns]
|
||||
for key, value in row_data.items():
|
||||
if key not in columns:
|
||||
columns.append(key)
|
||||
worksheet.update_cell(1, len(columns), key)
|
||||
row.append(value)
|
||||
rows.append(row)
|
||||
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
|
||||
|
||||
|
||||
async def _async_append_to_sheet(call: ServiceCall) -> None:
|
||||
"""Append new line of data to a Google Sheets document."""
|
||||
entry: GoogleSheetsConfigEntry | None = call.hass.config_entries.async_get_entry(
|
||||
call.data[DATA_CONFIG_ENTRY]
|
||||
)
|
||||
if not entry or not hasattr(entry, "runtime_data"):
|
||||
raise ValueError(f"Invalid config entry: {call.data[DATA_CONFIG_ENTRY]}")
|
||||
await entry.runtime_data.async_ensure_token_valid()
|
||||
await call.hass.async_add_executor_job(_append_to_sheet, call, entry)
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Add the services for Google Sheets."""
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_APPEND_SHEET,
|
||||
_async_append_to_sheet,
|
||||
schema=SHEET_SERVICE_SCHEMA,
|
||||
)
|
||||
@@ -50,7 +50,12 @@ from .const import (
|
||||
UNITS_IMPERIAL,
|
||||
UNITS_METRIC,
|
||||
)
|
||||
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
|
||||
from .helpers import (
|
||||
InvalidApiKeyException,
|
||||
PermissionDeniedException,
|
||||
UnknownException,
|
||||
validate_config_entry,
|
||||
)
|
||||
|
||||
RECONFIGURE_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -188,6 +193,8 @@ async def validate_input(
|
||||
user_input[CONF_ORIGIN],
|
||||
user_input[CONF_DESTINATION],
|
||||
)
|
||||
except PermissionDeniedException:
|
||||
return {"base": "permission_denied"}
|
||||
except InvalidApiKeyException:
|
||||
return {"base": "invalid_auth"}
|
||||
except TimeoutError:
|
||||
|
||||
@@ -7,6 +7,7 @@ from google.api_core.exceptions import (
|
||||
Forbidden,
|
||||
GatewayTimeout,
|
||||
GoogleAPIError,
|
||||
PermissionDenied,
|
||||
Unauthorized,
|
||||
)
|
||||
from google.maps.routing_v2 import (
|
||||
@@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
|
||||
from google.type import latlng_pb2
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.location import find_coordinates
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
|
||||
try:
|
||||
formatted_coordinates = coordinates.split(",")
|
||||
vol.Schema(cv.gps(formatted_coordinates))
|
||||
except (AttributeError, vol.ExactSequenceInvalid):
|
||||
except (AttributeError, vol.Invalid):
|
||||
return Waypoint(address=location)
|
||||
return Waypoint(
|
||||
location=Location(
|
||||
@@ -67,6 +76,9 @@ async def validate_config_entry(
|
||||
await client.compute_routes(
|
||||
request, metadata=[("x-goog-fieldmask", field_mask)]
|
||||
)
|
||||
except PermissionDenied as permission_error:
|
||||
_LOGGER.error("Permission denied: %s", permission_error.message)
|
||||
raise PermissionDeniedException from permission_error
|
||||
except (Unauthorized, Forbidden) as unauthorized_error:
|
||||
_LOGGER.error("Request denied: %s", unauthorized_error.message)
|
||||
raise InvalidApiKeyException from unauthorized_error
|
||||
@@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
|
||||
|
||||
class UnknownException(Exception):
|
||||
"""Unknown API Error."""
|
||||
|
||||
|
||||
class PermissionDeniedException(Exception):
|
||||
"""Permission Denied Error."""
|
||||
|
||||
|
||||
def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Create an issue for the Routes API being disabled."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"routes_api_disabled_{entry.entry_id}",
|
||||
learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="routes_api_disabled",
|
||||
translation_placeholders={
|
||||
"entry_title": entry.title,
|
||||
"enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api",
|
||||
"api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Delete the issue for the Routes API being disabled."""
|
||||
async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}")
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from google.api_core.client_options import ClientOptions
|
||||
from google.api_core.exceptions import GoogleAPIError
|
||||
from google.api_core.exceptions import GoogleAPIError, PermissionDenied
|
||||
from google.maps.routing_v2 import (
|
||||
ComputeRoutesRequest,
|
||||
Route,
|
||||
@@ -58,7 +58,11 @@ from .const import (
|
||||
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
|
||||
UNITS_TO_GOOGLE_SDK_ENUM,
|
||||
)
|
||||
from .helpers import convert_to_waypoint
|
||||
from .helpers import (
|
||||
convert_to_waypoint,
|
||||
create_routes_api_disabled_issue,
|
||||
delete_routes_api_disabled_issue,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
|
||||
response = await self._client.compute_routes(
|
||||
request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
|
||||
)
|
||||
_LOGGER.debug("Received response: %s", response)
|
||||
if response is not None and len(response.routes) > 0:
|
||||
self._route = response.routes[0]
|
||||
delete_routes_api_disabled_issue(self.hass, self._config_entry)
|
||||
except PermissionDenied:
|
||||
_LOGGER.error("Routes API is disabled for this API key")
|
||||
create_routes_api_disabled_issue(self.hass, self._config_entry)
|
||||
self._route = None
|
||||
except GoogleAPIError as ex:
|
||||
_LOGGER.error("Error getting travel time: %s", ex)
|
||||
self._route = None
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"permission_denied": "The Routes API is not enabled for this API key. Please see the setup instructions for detailed information.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
|
||||
@@ -100,5 +101,11 @@
|
||||
"fewer_transfers": "Fewer transfers"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"routes_api_disabled": {
|
||||
"title": "The Routes API must be enabled",
|
||||
"description": "Your Google Travel Time integration `{entry_title}` uses an API key which does not have the Routes API enabled.\n\n Please follow the instructions to [enable the API for your project]({enable_api_url}) and make sure your [API key restrictions]({api_key_restrictions_url}) allow access to the Routes API.\n\n After enabling the API this issue will be resolved automatically."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ from homeassistant.helpers.issue_registry import (
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
|
||||
|
||||
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN as GROUP_DOMAIN
|
||||
from .const import CONF_IGNORE_NON_NUMERIC, DOMAIN
|
||||
from .entity import GroupEntity
|
||||
|
||||
DEFAULT_NAME = "Sensor Group"
|
||||
@@ -509,7 +509,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
return state_classes[0]
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
GROUP_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{self.entity_id}_state_classes_not_matching",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -566,7 +566,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
return device_classes[0]
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
GROUP_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{self.entity_id}_device_classes_not_matching",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -654,7 +654,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
if device_class:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
GROUP_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{self.entity_id}_uoms_not_matching_device_class",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
@@ -670,7 +670,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
else:
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
GROUP_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{self.entity_id}_uoms_not_matching_no_device_class",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The gstreamer component."""
|
||||
|
||||
DOMAIN = "gstreamer"
|
||||
|
||||
@@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PIPELINE = "pipeline"
|
||||
|
||||
DOMAIN = "gstreamer"
|
||||
|
||||
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
|
||||
@@ -48,6 +50,20 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Gstreamer platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "GStreamer",
|
||||
},
|
||||
)
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
pipeline = config.get(CONF_PIPELINE)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The hddtemp component."""
|
||||
|
||||
DOMAIN = "hddtemp"
|
||||
|
||||
@@ -22,11 +22,14 @@ from homeassistant.const import (
|
||||
CONF_PORT,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DEVICE = "device"
|
||||
@@ -56,6 +59,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the HDDTemp sensor."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "hddtemp",
|
||||
},
|
||||
)
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
@@ -73,7 +73,9 @@ async def async_setup_entry(
|
||||
class HiveWaterHeater(HiveEntity, WaterHeaterEntity):
|
||||
"""Hive Water Heater Device."""
|
||||
|
||||
_attr_supported_features = WaterHeaterEntityFeature.OPERATION_MODE
|
||||
_attr_supported_features = (
|
||||
WaterHeaterEntityFeature.ON_OFF | WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_operation_list = SUPPORT_WATER_HEATER
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.73", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.74", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
"macaddress": "C8D778*"
|
||||
},
|
||||
{
|
||||
"hostname": "(bosch|siemens)-*",
|
||||
"hostname": "(balay|bosch|neff|siemens)-*",
|
||||
"macaddress": "68A40E*"
|
||||
},
|
||||
{
|
||||
"hostname": "siemens-*",
|
||||
"hostname": "(siemens|neff)-*",
|
||||
"macaddress": "38B4D3*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"requirements": ["aiohomeconnect==0.17.0"],
|
||||
"requirements": ["aiohomeconnect==0.17.1"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
"title": "The {integration_title} YAML configuration is being removed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
},
|
||||
"deprecated_system_packages_config_flow_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue."
|
||||
},
|
||||
"deprecated_system_packages_yaml_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
|
||||
@@ -27,6 +27,7 @@ PLATFORMS = [
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SIREN,
|
||||
Platform.SWITCH,
|
||||
Platform.VALVE,
|
||||
]
|
||||
|
||||
@@ -83,3 +83,54 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=AUTH_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the reconfigure flow."""
|
||||
errors: dict[str, str] = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input:
|
||||
self.homee = Homee(
|
||||
user_input[CONF_HOST],
|
||||
reconfigure_entry.data[CONF_USERNAME],
|
||||
reconfigure_entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
try:
|
||||
await self.homee.get_access_token()
|
||||
except HomeeConnectionFailedException:
|
||||
errors["base"] = "cannot_connect"
|
||||
except HomeeAuthenticationFailedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self.hass.loop.create_task(self.homee.run())
|
||||
await self.homee.wait_until_connected()
|
||||
self.homee.disconnect()
|
||||
await self.homee.wait_until_disconnected()
|
||||
|
||||
await self.async_set_unique_id(self.homee.settings.uid)
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_hub")
|
||||
|
||||
_LOGGER.debug("Updated homee entry with ID %s", self.homee.settings.uid)
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(), data_updates=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_HOST, default=reconfigure_entry.data[CONF_HOST]
|
||||
): str
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"name": reconfigure_entry.runtime_data.settings.uid
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Diagnostics for homee integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from . import DOMAIN, HomeeConfigEntry
|
||||
|
||||
TO_REDACT = [CONF_PASSWORD, CONF_USERNAME, "latitude", "longitude", "wlan_ssid"]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: HomeeConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
return {
|
||||
"entry_data": async_redact_data(entry.data, TO_REDACT),
|
||||
"settings": async_redact_data(entry.runtime_data.settings.raw_data, TO_REDACT),
|
||||
"devices": [{"node": node.raw_data} for node in entry.runtime_data.nodes],
|
||||
}
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: HomeeConfigEntry, device: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
|
||||
# Extract node_id from the device identifiers
|
||||
split_uid = next(
|
||||
identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN
|
||||
).split("-")
|
||||
# Homee hub itself only has MAC as identifier and a node_id of -1
|
||||
node_id = -1 if len(split_uid) < 2 else split_uid[1]
|
||||
|
||||
node = entry.runtime_data.get_node_by_id(int(node_id))
|
||||
assert node is not None
|
||||
return {
|
||||
"homee node": node.raw_data,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
"""The homee siren platform."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyHomee.const import AttributeType
|
||||
|
||||
from homeassistant.components.siren import SirenEntity, SirenEntityFeature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_devices: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add siren entities for homee."""
|
||||
|
||||
async_add_devices(
|
||||
HomeeSiren(attribute, config_entry)
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type == AttributeType.SIREN
|
||||
)
|
||||
|
||||
|
||||
class HomeeSiren(HomeeEntity, SirenEntity):
|
||||
"""Representation of a homee siren device."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_supported_features = SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the siren."""
|
||||
return self._attribute.current_value == 1.0
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren on."""
|
||||
await self.async_set_homee_value(1)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the siren off."""
|
||||
await self.async_set_homee_value(0)
|
||||
@@ -2,7 +2,9 @@
|
||||
"config": {
|
||||
"flow_title": "homee {name} ({host})",
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"wrong_hub": "Address belongs to a different homee."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -22,6 +24,16 @@
|
||||
"username": "The username for your homee.",
|
||||
"password": "The password for your homee."
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Reconfigure homee {name}",
|
||||
"description": "Reconfigure the IP address of your homee.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address of your homee."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.14"],
|
||||
"requirements": ["aiohomekit==3.2.15"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ from .const import (
|
||||
HMIPC_NAME,
|
||||
)
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .services import async_setup_services, async_unload_services
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -63,6 +63,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
)
|
||||
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -83,7 +85,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
|
||||
if not await hap.async_setup():
|
||||
return False
|
||||
|
||||
await async_setup_services(hass)
|
||||
_async_remove_obsolete_entities(hass, entry, hap)
|
||||
|
||||
# Register on HA stop event to gracefully shutdown HomematicIP Cloud connection
|
||||
@@ -115,8 +116,6 @@ async def async_unload_entry(
|
||||
assert hap.reset_connection_listener is not None
|
||||
hap.reset_connection_listener()
|
||||
|
||||
await async_unload_services(hass)
|
||||
|
||||
return await hap.async_reset()
|
||||
|
||||
|
||||
|
||||
@@ -123,32 +123,29 @@ SCHEMA_SET_HOME_COOLING_MODE = vol.Schema(
|
||||
async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the HomematicIP Cloud services."""
|
||||
|
||||
if hass.services.async_services_for_domain(DOMAIN):
|
||||
return
|
||||
|
||||
@verify_domain_control(hass, DOMAIN)
|
||||
async def async_call_hmipc_service(service: ServiceCall) -> None:
|
||||
"""Call correct HomematicIP Cloud service."""
|
||||
service_name = service.service
|
||||
|
||||
if service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_DURATION:
|
||||
await _async_activate_eco_mode_with_duration(hass, service)
|
||||
await _async_activate_eco_mode_with_duration(service)
|
||||
elif service_name == SERVICE_ACTIVATE_ECO_MODE_WITH_PERIOD:
|
||||
await _async_activate_eco_mode_with_period(hass, service)
|
||||
await _async_activate_eco_mode_with_period(service)
|
||||
elif service_name == SERVICE_ACTIVATE_VACATION:
|
||||
await _async_activate_vacation(hass, service)
|
||||
await _async_activate_vacation(service)
|
||||
elif service_name == SERVICE_DEACTIVATE_ECO_MODE:
|
||||
await _async_deactivate_eco_mode(hass, service)
|
||||
await _async_deactivate_eco_mode(service)
|
||||
elif service_name == SERVICE_DEACTIVATE_VACATION:
|
||||
await _async_deactivate_vacation(hass, service)
|
||||
await _async_deactivate_vacation(service)
|
||||
elif service_name == SERVICE_DUMP_HAP_CONFIG:
|
||||
await _async_dump_hap_config(hass, service)
|
||||
await _async_dump_hap_config(service)
|
||||
elif service_name == SERVICE_RESET_ENERGY_COUNTER:
|
||||
await _async_reset_energy_counter(hass, service)
|
||||
await _async_reset_energy_counter(service)
|
||||
elif service_name == SERVICE_SET_ACTIVE_CLIMATE_PROFILE:
|
||||
await _set_active_climate_profile(hass, service)
|
||||
await _set_active_climate_profile(service)
|
||||
elif service_name == SERVICE_SET_HOME_COOLING_MODE:
|
||||
await _async_set_home_cooling_mode(hass, service)
|
||||
await _async_set_home_cooling_mode(service)
|
||||
|
||||
hass.services.async_register(
|
||||
domain=DOMAIN,
|
||||
@@ -217,90 +214,75 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_services(hass: HomeAssistant):
|
||||
"""Unload HomematicIP Cloud services."""
|
||||
if hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
return
|
||||
|
||||
for hmipc_service in HMIPC_SERVICES:
|
||||
hass.services.async_remove(domain=DOMAIN, service=hmipc_service)
|
||||
|
||||
|
||||
async def _async_activate_eco_mode_with_duration(
|
||||
hass: HomeAssistant, service: ServiceCall
|
||||
) -> None:
|
||||
async def _async_activate_eco_mode_with_duration(service: ServiceCall) -> None:
|
||||
"""Service to activate eco mode with duration."""
|
||||
duration = service.data[ATTR_DURATION]
|
||||
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.activate_absence_with_duration_async(duration)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.activate_absence_with_duration_async(duration)
|
||||
|
||||
|
||||
async def _async_activate_eco_mode_with_period(
|
||||
hass: HomeAssistant, service: ServiceCall
|
||||
) -> None:
|
||||
async def _async_activate_eco_mode_with_period(service: ServiceCall) -> None:
|
||||
"""Service to activate eco mode with period."""
|
||||
endtime = service.data[ATTR_ENDTIME]
|
||||
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.activate_absence_with_period_async(endtime)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.activate_absence_with_period_async(endtime)
|
||||
|
||||
|
||||
async def _async_activate_vacation(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
async def _async_activate_vacation(service: ServiceCall) -> None:
|
||||
"""Service to activate vacation."""
|
||||
endtime = service.data[ATTR_ENDTIME]
|
||||
temperature = service.data[ATTR_TEMPERATURE]
|
||||
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.activate_vacation_async(endtime, temperature)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.activate_vacation_async(endtime, temperature)
|
||||
|
||||
|
||||
async def _async_deactivate_eco_mode(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
async def _async_deactivate_eco_mode(service: ServiceCall) -> None:
|
||||
"""Service to deactivate eco mode."""
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.deactivate_absence_async()
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.deactivate_absence_async()
|
||||
|
||||
|
||||
async def _async_deactivate_vacation(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
async def _async_deactivate_vacation(service: ServiceCall) -> None:
|
||||
"""Service to deactivate vacation."""
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.deactivate_vacation_async()
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.deactivate_vacation_async()
|
||||
|
||||
|
||||
async def _set_active_climate_profile(
|
||||
hass: HomeAssistant, service: ServiceCall
|
||||
) -> None:
|
||||
async def _set_active_climate_profile(service: ServiceCall) -> None:
|
||||
"""Service to set the active climate profile."""
|
||||
entity_id_list = service.data[ATTR_ENTITY_ID]
|
||||
climate_profile_index = service.data[ATTR_CLIMATE_PROFILE_INDEX] - 1
|
||||
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if entity_id_list != "all":
|
||||
for entity_id in entity_id_list:
|
||||
group = entry.runtime_data.hmip_device_by_entity_id.get(entity_id)
|
||||
@@ -312,16 +294,16 @@ async def _set_active_climate_profile(
|
||||
await group.set_active_profile_async(climate_profile_index)
|
||||
|
||||
|
||||
async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> None:
|
||||
async def _async_dump_hap_config(service: ServiceCall) -> None:
|
||||
"""Service to dump the configuration of a Homematic IP Access Point."""
|
||||
config_path: str = (
|
||||
service.data.get(ATTR_CONFIG_OUTPUT_PATH) or hass.config.config_dir
|
||||
service.data.get(ATTR_CONFIG_OUTPUT_PATH) or service.hass.config.config_dir
|
||||
)
|
||||
config_file_prefix = service.data[ATTR_CONFIG_OUTPUT_FILE_PREFIX]
|
||||
anonymize = service.data[ATTR_ANONYMIZE]
|
||||
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
hap_sgtin = entry.unique_id
|
||||
assert hap_sgtin is not None
|
||||
|
||||
@@ -338,12 +320,12 @@ async def _async_dump_hap_config(hass: HomeAssistant, service: ServiceCall) -> N
|
||||
config_file.write_text(json_state, encoding="utf8")
|
||||
|
||||
|
||||
async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall):
|
||||
async def _async_reset_energy_counter(service: ServiceCall):
|
||||
"""Service to reset the energy counter."""
|
||||
entity_id_list = service.data[ATTR_ENTITY_ID]
|
||||
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if entity_id_list != "all":
|
||||
for entity_id in entity_id_list:
|
||||
device = entry.runtime_data.hmip_device_by_entity_id.get(entity_id)
|
||||
@@ -355,16 +337,16 @@ async def _async_reset_energy_counter(hass: HomeAssistant, service: ServiceCall)
|
||||
await device.reset_energy_counter_async()
|
||||
|
||||
|
||||
async def _async_set_home_cooling_mode(hass: HomeAssistant, service: ServiceCall):
|
||||
async def _async_set_home_cooling_mode(service: ServiceCall):
|
||||
"""Service to set the cooling mode."""
|
||||
cooling = service.data[ATTR_COOLING]
|
||||
|
||||
if hapid := service.data.get(ATTR_ACCESSPOINT_ID):
|
||||
if home := _get_home(hass, hapid):
|
||||
if home := _get_home(service.hass, hapid):
|
||||
await home.set_cooling_async(cooling)
|
||||
else:
|
||||
entry: HomematicIPConfigEntry
|
||||
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for entry in service.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
await entry.runtime_data.home.set_cooling_async(cooling)
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["homewizard_energy"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-homewizard-energy==v8.3.2"],
|
||||
"requirements": ["python-homewizard-energy==8.3.3"],
|
||||
"zeroconf": ["_hwenergy._tcp.local.", "_homewizard._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -39,14 +39,14 @@ def setup_cors(app: Application, origins: list[str]) -> None:
|
||||
cors = aiohttp_cors.setup(
|
||||
app,
|
||||
defaults={
|
||||
host: aiohttp_cors.ResourceOptions(
|
||||
host: aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
|
||||
allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*"
|
||||
)
|
||||
for host in origins
|
||||
},
|
||||
)
|
||||
|
||||
cors_added = set()
|
||||
cors_added: set[str] = set()
|
||||
|
||||
def _allow_cors(
|
||||
route: AbstractRoute | AbstractResource,
|
||||
@@ -69,13 +69,13 @@ def setup_cors(app: Application, origins: list[str]) -> None:
|
||||
if path_str in cors_added:
|
||||
return
|
||||
|
||||
cors.add(route, config)
|
||||
cors.add(route, config) # type: ignore[arg-type]
|
||||
cors_added.add(path_str)
|
||||
|
||||
app[KEY_ALLOW_ALL_CORS] = lambda route: _allow_cors(
|
||||
route,
|
||||
{
|
||||
"*": aiohttp_cors.ResourceOptions(
|
||||
"*": aiohttp_cors.ResourceOptions( # type: ignore[no-untyped-call]
|
||||
allow_headers=ALLOWED_CORS_HEADERS, allow_methods="*"
|
||||
)
|
||||
},
|
||||
|
||||
@@ -5,12 +5,23 @@ from aiohue.util import normalize_bridge_id
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.config_entries import SOURCE_IGNORE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .bridge import HueBridge, HueConfigEntry
|
||||
from .const import DOMAIN, SERVICE_HUE_ACTIVATE_SCENE
|
||||
from .const import DOMAIN
|
||||
from .migration import check_migration
|
||||
from .services import async_register_services
|
||||
from .services import async_setup_services
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Hue integration."""
|
||||
|
||||
async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
|
||||
@@ -23,9 +34,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
|
||||
if not await bridge.async_initialize_bridge():
|
||||
return False
|
||||
|
||||
# register Hue domain services
|
||||
async_register_services(hass)
|
||||
|
||||
api = bridge.api
|
||||
|
||||
# For backwards compat
|
||||
@@ -106,7 +114,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: HueConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_success = await entry.runtime_data.async_reset()
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
hass.services.async_remove(DOMAIN, SERVICE_HUE_ACTIVATE_SCENE)
|
||||
return unload_success
|
||||
return await entry.runtime_data.async_reset()
|
||||
|
||||
@@ -25,7 +25,7 @@ from .const import (
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def async_register_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register services for Hue integration."""
|
||||
|
||||
async def hue_activate_scene(call: ServiceCall, skip_reload=True) -> None:
|
||||
@@ -59,21 +59,20 @@ def async_register_services(hass: HomeAssistant) -> None:
|
||||
group_name,
|
||||
)
|
||||
|
||||
if not hass.services.has_service(DOMAIN, SERVICE_HUE_ACTIVATE_SCENE):
|
||||
# Register a local handler for scene activation
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_HUE_ACTIVATE_SCENE,
|
||||
verify_domain_control(hass, DOMAIN)(hue_activate_scene),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GROUP_NAME): cv.string,
|
||||
vol.Required(ATTR_SCENE_NAME): cv.string,
|
||||
vol.Optional(ATTR_TRANSITION): cv.positive_int,
|
||||
vol.Optional(ATTR_DYNAMIC): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
# Register a local handler for scene activation
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_HUE_ACTIVATE_SCENE,
|
||||
verify_domain_control(hass, DOMAIN)(hue_activate_scene),
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_GROUP_NAME): cv.string,
|
||||
vol.Required(ATTR_SCENE_NAME): cv.string,
|
||||
vol.Optional(ATTR_TRANSITION): cv.positive_int,
|
||||
vol.Optional(ATTR_DYNAMIC): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def hue_activate_scene_v1(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2025.3.0"]
|
||||
"requirements": ["pydrawise==2025.6.0"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user