mirror of
https://github.com/home-assistant/core.git
synced 2026-05-02 20:37:41 +00:00
Compare commits
98 Commits
python-3.1
...
edenhaus/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b58f2642a | ||
|
|
bdca86b975 | ||
|
|
3ab08f1b45 | ||
|
|
331fafe9e1 | ||
|
|
9f432df02f | ||
|
|
49c3a73102 | ||
|
|
793b84034b | ||
|
|
6fbff94af8 | ||
|
|
764075f917 | ||
|
|
09817aff8e | ||
|
|
44e14136d7 | ||
|
|
a1802f4072 | ||
|
|
88525955ce | ||
|
|
2d87280ea8 | ||
|
|
92897b92bd | ||
|
|
a24e3bc703 | ||
|
|
3ed0d8a1d5 | ||
|
|
fe577eaa1f | ||
|
|
62232a8bff | ||
|
|
269ec1e5ac | ||
|
|
9c7ac4d608 | ||
|
|
d888d17ef6 | ||
|
|
3725e498ff | ||
|
|
dba17323a7 | ||
|
|
ee88b6ac0d | ||
|
|
a7a2387a2e | ||
|
|
9442790fba | ||
|
|
da1889d312 | ||
|
|
f79f386044 | ||
|
|
4b28928702 | ||
|
|
859ce55c96 | ||
|
|
9a9f19cb9e | ||
|
|
d8b1bfb268 | ||
|
|
f5363db26f | ||
|
|
3be1aa5441 | ||
|
|
7dbffb7375 | ||
|
|
07c4025d47 | ||
|
|
3e3e425aa5 | ||
|
|
162a4fc385 | ||
|
|
ef6fd92079 | ||
|
|
4ad71a070a | ||
|
|
f33ad12f5e | ||
|
|
da7fbb0dd6 | ||
|
|
81137345a3 | ||
|
|
d3e77d4195 | ||
|
|
ce977e90a5 | ||
|
|
2871b87344 | ||
|
|
d82ce1e22d | ||
|
|
b8bb2e0090 | ||
|
|
1b81cfe3ca | ||
|
|
0a3f0d90c3 | ||
|
|
84d566a02c | ||
|
|
0e0d54e4b6 | ||
|
|
5b05061def | ||
|
|
e0bf76769a | ||
|
|
63868bc169 | ||
|
|
b8b7169371 | ||
|
|
1cc778954f | ||
|
|
3ba3ecdef3 | ||
|
|
5c57fc6e14 | ||
|
|
2da440043a | ||
|
|
4f34725e53 | ||
|
|
d03bec2f44 | ||
|
|
57c37fc10c | ||
|
|
fd98594143 | ||
|
|
894547abed | ||
|
|
b48060674c | ||
|
|
6f2aa7852a | ||
|
|
9d53645468 | ||
|
|
a3f1c067f7 | ||
|
|
cef97973d0 | ||
|
|
7bb297a3fc | ||
|
|
7e2b8e1a48 | ||
|
|
013c5e7f7c | ||
|
|
7cb1d5b8ab | ||
|
|
57d9e8ea6f | ||
|
|
32743fcf8d | ||
|
|
f4637db26d | ||
|
|
b4bfe6b80b | ||
|
|
278f25ec6e | ||
|
|
39d3bc3e53 | ||
|
|
bb41a2df9f | ||
|
|
284242b90e | ||
|
|
a95c216983 | ||
|
|
d41a3ae0cd | ||
|
|
0dfbe3ef84 | ||
|
|
71fc725d75 | ||
|
|
d41c9aee52 | ||
|
|
8091f511b8 | ||
|
|
a7baedc22b | ||
|
|
05bfb3a52e | ||
|
|
2a5b95ba4d | ||
|
|
3dd972cc7a | ||
|
|
acd9dd218a | ||
|
|
6552cf8f7a | ||
|
|
e4e4785225 | ||
|
|
d531ce8d1d | ||
|
|
0224928655 |
@@ -15,12 +15,14 @@ description: Everything you need to know to build, test and review Home Assistan
|
||||
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
|
||||
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
|
||||
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
|
||||
- "potato" is a forbidden word for an integration and should never be used.
|
||||
|
||||
The following platforms have extra guidelines:
|
||||
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
|
||||
- **Repairs**: [`platform-repairs.md`](platform-repairs.md) for user-actionable repair issues
|
||||
|
||||
## Entity platforms
|
||||
|
||||
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
||||
|
||||
## Integration Quality Scale
|
||||
|
||||
|
||||
4
.github/copilot-instructions.md
vendored
4
.github/copilot-instructions.md
vendored
@@ -21,7 +21,9 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
## Python Syntax Notes
|
||||
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses. Never flag this as an issue since Home Assistant officially supports Python 3.14.
|
||||
- Home Assistant officially supports Python 3.14 as its minimum version. Do not flag syntax or features that require Python 3.14 as issues, and do not suggest workarounds for older Python versions.
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses. Never flag this as an issue.
|
||||
- Python 3.14 evaluates annotations lazily (PEP 649). Forward references in annotations do not need to be quoted — annotations can reference names defined later in the module without quoting them or using `from __future__ import annotations`. Do not flag unquoted forward references in annotations as issues.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -18,12 +18,14 @@ excludeAgent: "cloud-agent"
|
||||
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
|
||||
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
|
||||
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
|
||||
- "potato" is a forbidden word for an integration and should never be used.
|
||||
|
||||
The following platforms have extra guidelines:
|
||||
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
|
||||
- **Repairs**: [`platform-repairs.md`](platform-repairs.md) for user-actionable repair issues
|
||||
|
||||
## Entity platforms
|
||||
|
||||
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
|
||||
|
||||
## Integration Quality Scale
|
||||
|
||||
|
||||
5
.github/renovate.json
vendored
5
.github/renovate.json
vendored
@@ -6,7 +6,7 @@
|
||||
"pep621",
|
||||
"pip_requirements",
|
||||
"pre-commit",
|
||||
"regex",
|
||||
"custom.regex",
|
||||
"homeassistant-manifest"
|
||||
],
|
||||
|
||||
@@ -27,8 +27,9 @@
|
||||
]
|
||||
},
|
||||
|
||||
"regexManagers": [
|
||||
"customManagers": [
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "Update ruff required-version in pyproject.toml",
|
||||
"managerFilePatterns": ["/^pyproject\\.toml$/"],
|
||||
"matchStrings": ["required-version = \">=(?<currentValue>[\\d.]+)\""],
|
||||
|
||||
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -14,7 +14,7 @@ env:
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
# Base image version from https://github.com/home-assistant/docker
|
||||
BASE_IMAGE_VERSION: "2026.02.0"
|
||||
BASE_IMAGE_VERSION: "2026.04.0"
|
||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||
|
||||
permissions: {}
|
||||
|
||||
72
.github/workflows/ci.yaml
vendored
72
.github/workflows/ci.yaml
vendored
@@ -38,9 +38,8 @@ on:
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 3
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2026.5"
|
||||
HA_SHORT_VERSION: "2026.6"
|
||||
ADDITIONAL_PYTHON_VERSIONS: "[]"
|
||||
# 10.3 is the oldest supported version
|
||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||
@@ -358,32 +357,34 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Generate partial uv restore key
|
||||
id: generate-uv-key
|
||||
run: |
|
||||
uv_version=$(cat requirements.txt | grep uv | cut -d '=' -f 3)
|
||||
echo "version=${uv_version}" >> $GITHUB_OUTPUT
|
||||
echo "key=uv-${UV_CACHE_VERSION}-${uv_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Generate partial uv restore key
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
id: generate-uv-key
|
||||
env:
|
||||
RUNNER_OS: ${{ runner.os }}
|
||||
RUNNER_ARCH: ${{ runner.arch }}
|
||||
PYTHON_VERSION: ${{ steps.python.outputs.python-version }}
|
||||
HASH_FILES: ${{ hashFiles('requirements.txt', 'requirements_all.txt', 'requirements_test.txt') }}
|
||||
run: |
|
||||
partial_key="${RUNNER_OS}-${RUNNER_ARCH}-${PYTHON_VERSION}-uv-"
|
||||
echo "partial_key=${partial_key}" >> $GITHUB_OUTPUT
|
||||
echo "full_key=${partial_key}${HASH_FILES}" >> $GITHUB_OUTPUT
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
id: cache-uv
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
steps.generate-uv-key.outputs.key }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-uv-${{
|
||||
env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{
|
||||
env.HA_SHORT_VERSION }}-
|
||||
key: ${{ steps.generate-uv-key.outputs.full_key }}
|
||||
restore-keys: ${{ steps.generate-uv-key.outputs.partial_key }}
|
||||
- name: Check if apt cache exists
|
||||
id: cache-apt-check
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
@@ -398,6 +399,7 @@ jobs:
|
||||
if: |
|
||||
steps.cache-venv.outputs.cache-hit != 'true'
|
||||
|| steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
id: install-os-deps
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
APT_CACHE_HIT: ${{ steps.cache-apt-check.outputs.cache-hit }}
|
||||
@@ -431,7 +433,10 @@ jobs:
|
||||
sudo chmod -R 755 ${APT_CACHE_BASE}
|
||||
fi
|
||||
- name: Save apt cache
|
||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
if: |
|
||||
always()
|
||||
&& steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
&& steps.install-os-deps.outcome == 'success'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: |
|
||||
@@ -441,6 +446,7 @@ jobs:
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
id: create-venv
|
||||
run: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
@@ -448,8 +454,7 @@ jobs:
|
||||
pip install "$(grep '^uv' < requirements.txt)"
|
||||
uv pip install -U "pip>=25.2"
|
||||
uv pip install -r requirements.txt
|
||||
python -m script.gen_requirements_all ci
|
||||
uv pip install -r requirements_all_pytest.txt -r requirements_test.txt
|
||||
uv pip install -r requirements_all.txt -r requirements_test.txt
|
||||
uv pip install -e . --config-settings editable_mode=compat
|
||||
- name: Dump pip freeze
|
||||
run: |
|
||||
@@ -465,12 +470,33 @@ jobs:
|
||||
overwrite: true
|
||||
- name: Remove pip_freeze
|
||||
run: rm pip_freeze.txt
|
||||
- name: Remove generated requirements_all
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: rm requirements_all_pytest.txt requirements_all_wheels_*.txt
|
||||
- name: Check dirty
|
||||
run: |
|
||||
./script/check_dirty
|
||||
- name: Prune uv cache
|
||||
if: |
|
||||
steps.cache-hit.outputs.cache-hit != 'true'
|
||||
&& (
|
||||
success()
|
||||
|| (always() && steps.create-venv.outcome == 'success'))
|
||||
id: prune-uv-cache
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
uv cache prune --ci
|
||||
- name: Save uv wheel cache
|
||||
if: steps.prune-uv-cache.outcome == 'success'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: ${{ steps.generate-uv-key.outputs.full_key }}
|
||||
- name: Save base Python virtual environment
|
||||
if: always() && steps.create-venv.outcome == 'success'
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
|
||||
hassfest:
|
||||
name: Check hassfest
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.10
|
||||
rev: v0.15.12
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.14.3
|
||||
3.14.4
|
||||
|
||||
@@ -12,7 +12,9 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
## Python Syntax Notes
|
||||
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses. Never flag this as an issue since Home Assistant officially supports Python 3.14.
|
||||
- Home Assistant officially supports Python 3.14 as its minimum version. Do not flag syntax or features that require Python 3.14 as issues, and do not suggest workarounds for older Python versions.
|
||||
- Python 3.14 explicitly allows `except TypeA, TypeB:` without parentheses. Never flag this as an issue.
|
||||
- Python 3.14 evaluates annotations lazily (PEP 649). Forward references in annotations do not need to be quoted — annotations can reference names defined later in the module without quoting them or using `from __future__ import annotations`. Do not flag unquoted forward references in annotations as issues.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1241,6 +1241,8 @@ CLAUDE.md @home-assistant/core
|
||||
/homeassistant/components/ollama/ @synesthesiam
|
||||
/tests/components/ollama/ @synesthesiam
|
||||
/homeassistant/components/ombi/ @larssont
|
||||
/homeassistant/components/omie/ @luuuis
|
||||
/tests/components/omie/ @luuuis
|
||||
/homeassistant/components/onboarding/ @home-assistant/core
|
||||
/tests/components/onboarding/ @home-assistant/core
|
||||
/homeassistant/components/ondilo_ico/ @JeromeHXP
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -13,6 +14,9 @@ from .models import PermissionLookup
|
||||
from .types import PolicyType
|
||||
from .util import test_all
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..models import User
|
||||
|
||||
POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA})
|
||||
|
||||
__all__ = [
|
||||
@@ -22,10 +26,21 @@ __all__ = [
|
||||
"PermissionLookup",
|
||||
"PolicyPermissions",
|
||||
"PolicyType",
|
||||
"filter_entity_ids_by_permission",
|
||||
"merge_policies",
|
||||
]
|
||||
|
||||
|
||||
def filter_entity_ids_by_permission(
|
||||
user: User, entity_ids: Iterable[str], key: str
|
||||
) -> list[str]:
|
||||
"""Filter entity IDs to those the user can access for the given policy key."""
|
||||
if user.is_admin or user.permissions.access_all_entities(key):
|
||||
return list(entity_ids)
|
||||
check_entity = user.permissions.check_entity
|
||||
return [entity_id for entity_id in entity_ids if check_entity(entity_id, key)]
|
||||
|
||||
|
||||
class AbstractPermissions:
|
||||
"""Default permissions class."""
|
||||
|
||||
|
||||
@@ -143,4 +143,4 @@ class AcaiaRestoreSensor(AcaiaEntity, RestoreSensor):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available or self._restored_data is not None
|
||||
return super().available or self.native_value is not None
|
||||
|
||||
@@ -147,7 +147,7 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""Return the target temperature."""
|
||||
return self._status.user_aircon_settings.temperature_setpoint_cool_c
|
||||
return self._status.user_aircon_settings.current_setpoint
|
||||
|
||||
@actron_air_command
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
@@ -239,7 +239,7 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the target temperature."""
|
||||
return self._zone.temperature_setpoint_cool_c
|
||||
return self._zone.current_setpoint
|
||||
|
||||
@actron_air_command
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
|
||||
@@ -4,11 +4,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
|
||||
# --- Unit lists for multi-unit pollutants ---
|
||||
|
||||
|
||||
@@ -237,21 +237,6 @@
|
||||
"name": "Volatile organic compounds value"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Air Quality",
|
||||
"triggers": {
|
||||
"co2_changed": {
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for: &trigger_for
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["airos==0.6.4"]
|
||||
"requirements": ["airos==0.6.5"]
|
||||
}
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
|
||||
.condition_common_for: &condition_common_for
|
||||
target: *condition_common_target
|
||||
|
||||
@@ -160,21 +160,6 @@
|
||||
"message": "Arming requires a code but none was given for {entity_id}."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"alarm_arm_away": {
|
||||
"description": "Arms an alarm in the away mode.",
|
||||
|
||||
@@ -7,12 +7,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -11,6 +11,7 @@ from .services import async_setup_services
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.NOTIFY,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
|
||||
55
homeassistant/components/alexa_devices/button.py
Normal file
55
homeassistant/components/alexa_devices/button.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Support for buttons."""
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
from .entity import AmazonServiceEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up button entities for Alexa Devices."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
known_routines: set[str] = set()
|
||||
|
||||
def _check_routines() -> None:
|
||||
current_routines = set(coordinator.api.routines)
|
||||
new_routines = current_routines - known_routines
|
||||
if new_routines:
|
||||
known_routines.update(new_routines)
|
||||
async_add_entities(
|
||||
AmazonRoutineButton(coordinator, routine) for routine in new_routines
|
||||
)
|
||||
|
||||
_check_routines()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_routines))
|
||||
|
||||
|
||||
class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
|
||||
"""Button entity for Alexa routine."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: AmazonDevicesCoordinator, routine: str) -> None:
|
||||
"""Initialize the routine button entity."""
|
||||
self._coordinator = coordinator
|
||||
self._routine = routine
|
||||
super().__init__(
|
||||
coordinator,
|
||||
EntityDescription(key=slugify(routine), name=routine),
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle button press action."""
|
||||
await self._coordinator.api.call_routine(self._routine)
|
||||
@@ -12,12 +12,13 @@ from aioamazondevices.structures import AmazonDevice
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
@@ -64,6 +65,13 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
for identifier_domain, identifier in device.identifiers
|
||||
if identifier_domain == DOMAIN
|
||||
}
|
||||
self.previous_routines: set[str] = {
|
||||
routine.unique_id
|
||||
for routine in er.async_entries_for_config_entry(
|
||||
er.async_get(hass), entry.entry_id
|
||||
)
|
||||
if routine.domain == Platform.BUTTON
|
||||
}
|
||||
|
||||
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
||||
"""Update device data."""
|
||||
@@ -92,8 +100,13 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
current_devices = set(data.keys())
|
||||
if stale_devices := self.previous_devices - current_devices:
|
||||
await self._async_remove_device_stale(stale_devices)
|
||||
|
||||
self.previous_devices = current_devices
|
||||
|
||||
current_routines = {slugify(routine) for routine in self.api.routines}
|
||||
if stale_routines := self.previous_routines - current_routines:
|
||||
await self._async_remove_routine_stale(stale_routines)
|
||||
self.previous_routines = current_routines
|
||||
|
||||
return data
|
||||
|
||||
async def _async_remove_device_stale(
|
||||
@@ -116,3 +129,23 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
device_id=device.id,
|
||||
remove_config_entry_id=self.config_entry.entry_id,
|
||||
)
|
||||
|
||||
async def _async_remove_routine_stale(
|
||||
self,
|
||||
stale_routines: set[str],
|
||||
) -> None:
|
||||
"""Remove stale routine."""
|
||||
entity_registry = er.async_get(self.hass)
|
||||
|
||||
for routine in stale_routines:
|
||||
_LOGGER.debug(
|
||||
"Detected change in routines: routine %s removed",
|
||||
routine,
|
||||
)
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.BUTTON,
|
||||
DOMAIN,
|
||||
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}",
|
||||
)
|
||||
if entity_id:
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
from aioamazondevices.structures import AmazonDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AmazonDevicesCoordinator
|
||||
@@ -50,3 +51,32 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
and self._serial_num in self.coordinator.data
|
||||
and self.device.online
|
||||
)
|
||||
|
||||
|
||||
class AmazonServiceEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Defines Alexa Devices entity for service device."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the service entity."""
|
||||
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, service_device_id(coordinator))},
|
||||
manufacturer="Amazon",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = (
|
||||
f"{slugify(coordinator.config_entry.unique_id)}-{description.key}"
|
||||
)
|
||||
|
||||
|
||||
def service_device_id(coordinator: AmazonDevicesCoordinator) -> str:
|
||||
"""Return service device id."""
|
||||
return slugify(f"{coordinator.config_entry.unique_id}_service_device")
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -72,19 +72,6 @@
|
||||
"id": "Answer ID",
|
||||
"sentences": "Sentences"
|
||||
}
|
||||
},
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -7,12 +7,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -194,6 +194,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"switch",
|
||||
"temperature",
|
||||
"text",
|
||||
"timer",
|
||||
"todo",
|
||||
"update",
|
||||
"vacuum",
|
||||
@@ -901,6 +902,10 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
|
||||
"""Remove listeners when removing automation from Home Assistant."""
|
||||
await super().async_will_remove_from_hass()
|
||||
await self._async_disable()
|
||||
if self.registry_entry and self.registry_entry.entity_id != self.entity_id:
|
||||
# Entity ID change, do not unload the script or conditions as they will
|
||||
# be reused.
|
||||
return
|
||||
self.action_script.async_unload()
|
||||
if self._condition is not None:
|
||||
self._condition.async_unload()
|
||||
|
||||
@@ -18,4 +18,10 @@ DEFAULT_STREAM_PROFILE = "No stream profile"
|
||||
DEFAULT_TRIGGER_TIME = 0
|
||||
DEFAULT_VIDEO_SOURCE = "No video source"
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.LIGHT, Platform.SWITCH]
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CAMERA,
|
||||
Platform.EVENT,
|
||||
Platform.LIGHT,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
62
homeassistant/components/axis/event.py
Normal file
62
homeassistant/components/axis/event.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Support for Axis event entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from axis.models.event import Event, EventTopic
|
||||
|
||||
from homeassistant.components.event import (
|
||||
DoorbellEventType,
|
||||
EventDeviceClass,
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import AxisConfigEntry
|
||||
from .entity import AxisEventDescription, AxisEventEntity
|
||||
|
||||
DOORBELL_CONFIG = ("I8116-E", "0")
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AxisEventPlatformDescription(AxisEventDescription, EventEntityDescription):
|
||||
"""Axis event entity description."""
|
||||
|
||||
|
||||
ENTITY_DESCRIPTIONS = (
|
||||
AxisEventPlatformDescription(
|
||||
key="Doorbell",
|
||||
device_class=EventDeviceClass.DOORBELL,
|
||||
event_types=[DoorbellEventType.RING],
|
||||
event_topic=EventTopic.PORT_INPUT,
|
||||
name_fn=lambda _hub, _event: "Doorbell",
|
||||
supported_fn=lambda hub, event: (hub.config.model, event.id) == DOORBELL_CONFIG,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AxisConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up an Axis event platform."""
|
||||
config_entry.runtime_data.entity_loader.register_platform(
|
||||
async_add_entities, AxisEvent, ENTITY_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class AxisEvent(AxisEventEntity, EventEntity):
|
||||
"""Representation of an Axis event entity."""
|
||||
|
||||
entity_description: AxisEventPlatformDescription
|
||||
|
||||
@callback
|
||||
def async_event_callback(self, event: Event) -> None:
|
||||
"""Handle Axis event updates."""
|
||||
if event.is_tripped:
|
||||
self._trigger_event(DoorbellEventType.RING)
|
||||
self.async_write_ha_state()
|
||||
@@ -30,19 +30,33 @@ BATTERY_PERCENTAGE_DOMAIN_SPECS = {
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_low": make_entity_state_condition(
|
||||
BATTERY_DOMAIN_SPECS, STATE_ON, support_duration=True
|
||||
BATTERY_DOMAIN_SPECS,
|
||||
STATE_ON,
|
||||
support_duration=True,
|
||||
primary_entities_only=False,
|
||||
),
|
||||
"is_not_low": make_entity_state_condition(
|
||||
BATTERY_DOMAIN_SPECS, STATE_OFF, support_duration=True
|
||||
BATTERY_DOMAIN_SPECS,
|
||||
STATE_OFF,
|
||||
support_duration=True,
|
||||
primary_entities_only=False,
|
||||
),
|
||||
"is_charging": make_entity_state_condition(
|
||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON, support_duration=True
|
||||
BATTERY_CHARGING_DOMAIN_SPECS,
|
||||
STATE_ON,
|
||||
support_duration=True,
|
||||
primary_entities_only=False,
|
||||
),
|
||||
"is_not_charging": make_entity_state_condition(
|
||||
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF, support_duration=True
|
||||
BATTERY_CHARGING_DOMAIN_SPECS,
|
||||
STATE_OFF,
|
||||
support_duration=True,
|
||||
primary_entities_only=False,
|
||||
),
|
||||
"is_level": make_entity_numerical_condition(
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE
|
||||
BATTERY_PERCENTAGE_DOMAIN_SPECS,
|
||||
PERCENTAGE,
|
||||
primary_entities_only=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: battery
|
||||
primary_entities_only: false
|
||||
fields:
|
||||
behavior: &condition_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
for: &condition_for
|
||||
required: true
|
||||
default: 00:00:00
|
||||
@@ -42,6 +40,7 @@ is_charging:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: battery_charging
|
||||
primary_entities_only: false
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
for: *condition_for
|
||||
@@ -51,6 +50,7 @@ is_not_charging:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: battery_charging
|
||||
primary_entities_only: false
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
for: *condition_for
|
||||
@@ -60,6 +60,7 @@ is_level:
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: battery
|
||||
primary_entities_only: false
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
threshold:
|
||||
|
||||
@@ -69,21 +69,6 @@
|
||||
"name": "Battery is not low"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Battery",
|
||||
"triggers": {
|
||||
"level_changed": {
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for: &trigger_for
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -33,11 +33,13 @@ from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN
|
||||
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
|
||||
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_USER,
|
||||
ConfigEntry,
|
||||
ConfigFlowResult,
|
||||
ConfigSubentry,
|
||||
ConfigSubentryData,
|
||||
ConfigSubentryFlow,
|
||||
FlowType,
|
||||
SubentryFlowContext,
|
||||
SubentryFlowResult,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
@@ -62,7 +64,6 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
||||
|
||||
from .binary_sensor import above_greater_than_below, no_overlapping
|
||||
from .const import (
|
||||
CONF_OBSERVATIONS,
|
||||
CONF_P_GIVEN_F,
|
||||
CONF_P_GIVEN_T,
|
||||
CONF_PRIOR,
|
||||
@@ -373,26 +374,6 @@ def _validate_observation_subentry(
|
||||
return user_input
|
||||
|
||||
|
||||
async def _validate_subentry_from_config_entry(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
# Standard behavior is to merge the result with the options.
|
||||
# In this case, we want to add a subentry so we update the options directly.
|
||||
observations: list[dict[str, Any]] = handler.options.setdefault(
|
||||
CONF_OBSERVATIONS, []
|
||||
)
|
||||
|
||||
if handler.parent_handler.cur_step is not None:
|
||||
user_input[CONF_PLATFORM] = handler.parent_handler.cur_step["step_id"]
|
||||
user_input = _validate_observation_subentry(
|
||||
user_input[CONF_PLATFORM],
|
||||
user_input,
|
||||
other_subentries=handler.options[CONF_OBSERVATIONS],
|
||||
)
|
||||
observations.append(user_input)
|
||||
return {}
|
||||
|
||||
|
||||
async def _get_description_placeholders(
|
||||
handler: SchemaCommonFlowHandler,
|
||||
) -> dict[str, str]:
|
||||
@@ -420,48 +401,12 @@ async def _get_description_placeholders(
|
||||
}
|
||||
|
||||
|
||||
async def _get_observation_menu_options(handler: SchemaCommonFlowHandler) -> list[str]:
|
||||
"""Return the menu options for the observation selector."""
|
||||
options = [typ.value for typ in ObservationTypes]
|
||||
if handler.options.get(CONF_OBSERVATIONS):
|
||||
options.append("finish")
|
||||
return options
|
||||
|
||||
|
||||
CONFIG_FLOW: dict[str, SchemaFlowMenuStep | SchemaFlowFormStep] = {
|
||||
str(USER): SchemaFlowFormStep(
|
||||
CONFIG_SCHEMA,
|
||||
validate_user_input=_validate_user,
|
||||
next_step=str(OBSERVATION_SELECTOR),
|
||||
description_placeholders=_get_description_placeholders,
|
||||
),
|
||||
str(OBSERVATION_SELECTOR): SchemaFlowMenuStep(
|
||||
_get_observation_menu_options,
|
||||
),
|
||||
str(ObservationTypes.STATE): SchemaFlowFormStep(
|
||||
STATE_SUBSCHEMA,
|
||||
next_step=str(OBSERVATION_SELECTOR),
|
||||
validate_user_input=_validate_subentry_from_config_entry,
|
||||
# Prevent the name of the bayesian sensor from being used as the suggested
|
||||
# name of the observations
|
||||
suggested_values=None,
|
||||
description_placeholders=_get_description_placeholders,
|
||||
),
|
||||
str(ObservationTypes.NUMERIC_STATE): SchemaFlowFormStep(
|
||||
NUMERIC_STATE_SUBSCHEMA,
|
||||
next_step=str(OBSERVATION_SELECTOR),
|
||||
validate_user_input=_validate_subentry_from_config_entry,
|
||||
suggested_values=None,
|
||||
description_placeholders=_get_description_placeholders,
|
||||
),
|
||||
str(ObservationTypes.TEMPLATE): SchemaFlowFormStep(
|
||||
TEMPLATE_SUBSCHEMA,
|
||||
next_step=str(OBSERVATION_SELECTOR),
|
||||
validate_user_input=_validate_subentry_from_config_entry,
|
||||
suggested_values=None,
|
||||
description_placeholders=_get_description_placeholders,
|
||||
),
|
||||
"finish": SchemaFlowFormStep(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -497,27 +442,17 @@ class BayesianConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
name: str = options[CONF_NAME]
|
||||
return name
|
||||
|
||||
@callback
|
||||
def async_create_entry(
|
||||
self,
|
||||
data: Mapping[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> ConfigFlowResult:
|
||||
"""Finish config flow and create a config entry."""
|
||||
data = dict(data)
|
||||
observations = data.pop(CONF_OBSERVATIONS)
|
||||
subentries: list[ConfigSubentryData] = [
|
||||
ConfigSubentryData(
|
||||
data=observation,
|
||||
title=observation[CONF_NAME],
|
||||
subentry_type="observation",
|
||||
unique_id=None,
|
||||
)
|
||||
for observation in observations
|
||||
]
|
||||
|
||||
self.async_config_flow_finished(data)
|
||||
return super().async_create_entry(data=data, subentries=subentries, **kwargs)
|
||||
async def async_on_create_entry(self, result: ConfigFlowResult) -> ConfigFlowResult:
|
||||
"""Start subentry flow when config entry has been created."""
|
||||
subentry_result = await self.hass.config_entries.subentries.async_init(
|
||||
(result["result"].entry_id, "observation"),
|
||||
context=SubentryFlowContext(source=SOURCE_USER),
|
||||
)
|
||||
result["next_flow"] = (
|
||||
FlowType.CONFIG_SUBENTRIES_FLOW,
|
||||
subentry_result["flow_id"],
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class ObservationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["blebox_uniapi"],
|
||||
"requirements": ["blebox-uniapi==2.5.1"],
|
||||
"requirements": ["blebox-uniapi==2.5.2"],
|
||||
"zeroconf": ["_bbxsrv._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ from .api import (
|
||||
async_address_present,
|
||||
async_ble_device_from_address,
|
||||
async_clear_address_from_match_history,
|
||||
async_clear_advertisement_history,
|
||||
async_current_scanners,
|
||||
async_discovered_service_info,
|
||||
async_get_advertisement_callback,
|
||||
@@ -116,6 +117,7 @@ __all__ = [
|
||||
"async_address_present",
|
||||
"async_ble_device_from_address",
|
||||
"async_clear_address_from_match_history",
|
||||
"async_clear_advertisement_history",
|
||||
"async_current_scanners",
|
||||
"async_discovered_service_info",
|
||||
"async_get_advertisement_callback",
|
||||
|
||||
@@ -207,6 +207,19 @@ def async_clear_address_from_match_history(hass: HomeAssistant, address: str) ->
|
||||
_get_manager(hass).async_clear_address_from_match_history(address)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_clear_advertisement_history(hass: HomeAssistant, address: str) -> None:
|
||||
"""Clear cached advertisement history for a device.
|
||||
|
||||
Causes the next advertisement from this address to be treated as new
|
||||
data, bypassing the change-detection guard in the Bluetooth manager.
|
||||
Intended for devices that emit static advertisements as a wake-up
|
||||
signal, for example, devices that require an active GATT connection
|
||||
to read sensor data and whose advertisement payload never changes.
|
||||
"""
|
||||
_get_manager(hass).async_clear_advertisement_history(address)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_register_scanner(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -6,6 +6,7 @@ DOMAIN = "broadlink"
|
||||
|
||||
DOMAINS_AND_TYPES = {
|
||||
Platform.CLIMATE: {"HYS"},
|
||||
Platform.INFRARED: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"},
|
||||
Platform.LIGHT: {"LB1", "LB2"},
|
||||
Platform.RADIO_FREQUENCY: {"RM4PRO", "RMPRO"},
|
||||
Platform.REMOTE: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"},
|
||||
|
||||
69
homeassistant/components/broadlink/infrared.py
Normal file
69
homeassistant/components/broadlink/infrared.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Infrared platform for Broadlink remotes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from broadlink.exceptions import BroadlinkException
|
||||
from broadlink.remote import pulses_to_data as _bl_pulses_to_data
|
||||
|
||||
from homeassistant.components.infrared import InfraredCommand, InfraredEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import BroadlinkEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .device import BroadlinkDevice
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
def _timings_to_broadlink_packet(timings: list[int]) -> bytes:
|
||||
"""Convert signed microsecond timings to a Broadlink IR packet.
|
||||
|
||||
Positive values are pulse (high) durations; negative values are space
|
||||
(low) durations. The Broadlink library's encoder expects absolute
|
||||
durations.
|
||||
"""
|
||||
pulses = [abs(t) for t in timings]
|
||||
return _bl_pulses_to_data(pulses)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Broadlink infrared entity."""
|
||||
# Uses legacy hass.data[DOMAIN] pattern
|
||||
# pylint: disable-next=hass-use-runtime-data
|
||||
device = hass.data[DOMAIN].devices[config_entry.entry_id]
|
||||
async_add_entities([BroadlinkInfraredEntity(device)])
|
||||
|
||||
|
||||
class BroadlinkInfraredEntity(BroadlinkEntity, InfraredEntity):
|
||||
"""Broadlink infrared transmitter entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "infrared_emitter"
|
||||
|
||||
def __init__(self, device: BroadlinkDevice) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(device)
|
||||
self._attr_unique_id = f"{device.unique_id}-emitter"
|
||||
|
||||
async def async_send_command(self, command: InfraredCommand) -> None:
|
||||
"""Send an IR command via the Broadlink device."""
|
||||
packet = _timings_to_broadlink_packet(command.get_raw_timings())
|
||||
try:
|
||||
await self._device.async_request(self._device.api.send_data, packet)
|
||||
except (BroadlinkException, OSError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="send_command_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
@@ -49,6 +49,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"infrared": {
|
||||
"infrared_emitter": {
|
||||
"name": "IR emitter"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"day_of_week": {
|
||||
"name": "Day of week",
|
||||
@@ -82,6 +87,9 @@
|
||||
"frequency_not_supported": {
|
||||
"message": "Broadlink devices cannot transmit on {frequency} MHz"
|
||||
},
|
||||
"send_command_failed": {
|
||||
"message": "Failed to send IR command: {error}"
|
||||
},
|
||||
"transmit_failed": {
|
||||
"message": "Failed to transmit RF command: {error}"
|
||||
}
|
||||
|
||||
@@ -293,9 +293,8 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
|
||||
),
|
||||
BrotherSensorEntityDescription(
|
||||
key="uptime",
|
||||
translation_key="last_restart",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
device_class=SensorDeviceClass.UPTIME,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value=lambda data: data.uptime,
|
||||
),
|
||||
|
||||
@@ -151,9 +151,6 @@
|
||||
"laser_remaining_life": {
|
||||
"name": "Laser remaining lifetime"
|
||||
},
|
||||
"last_restart": {
|
||||
"name": "Last restart"
|
||||
},
|
||||
"magenta_drum_page_counter": {
|
||||
"name": "Magenta drum page counter",
|
||||
"unit_of_measurement": "[%key:component::brother::entity::sensor::page_counter::unit_of_measurement%]"
|
||||
|
||||
@@ -7,11 +7,8 @@ is_event_active:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -64,12 +64,6 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_offset_type": {
|
||||
"options": {
|
||||
"after": "After",
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
|
||||
.humidity_threshold_entity: &humidity_threshold_entity
|
||||
- domain: input_number
|
||||
|
||||
@@ -271,21 +271,6 @@
|
||||
"message": "Provided temperature {check_temp} is not valid. Accepted range is {min_temp} to {max_temp}."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"set_fan_mode": {
|
||||
"description": "Sets the fan mode of a thermostat.",
|
||||
|
||||
@@ -7,12 +7,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for: &trigger_for
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -18,7 +18,12 @@ from aiocomelit.const import (
|
||||
SCENARIO,
|
||||
VEDO,
|
||||
)
|
||||
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
||||
from aiocomelit.exceptions import (
|
||||
CannotAuthenticate,
|
||||
CannotConnect,
|
||||
CannotRetrieveData,
|
||||
DeviceStorageFailureError,
|
||||
)
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -112,6 +117,11 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_authenticate",
|
||||
) from err
|
||||
except DeviceStorageFailureError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_storage_failure",
|
||||
) from err
|
||||
|
||||
@abstractmethod
|
||||
async def _async_update_system_data(self) -> T:
|
||||
|
||||
@@ -121,6 +121,9 @@
|
||||
"cannot_retrieve_data": {
|
||||
"message": "Error retrieving data: {error}"
|
||||
},
|
||||
"device_storage_failure": {
|
||||
"message": "Device SD card read failure. The card may be corrupted or failing; replacement is recommended."
|
||||
},
|
||||
"humidity_while_off": {
|
||||
"message": "Cannot change humidity while off"
|
||||
},
|
||||
|
||||
@@ -5,7 +5,12 @@ from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any, Concatenate, Literal
|
||||
|
||||
from aiocomelit.api import ComelitSerialBridgeObject
|
||||
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
||||
from aiocomelit.exceptions import (
|
||||
CannotAuthenticate,
|
||||
CannotConnect,
|
||||
CannotRetrieveData,
|
||||
DeviceStorageFailureError,
|
||||
)
|
||||
from aiohttp import ClientSession, CookieJar
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -110,6 +115,12 @@ def bridge_api_call[_T: ComelitBridgeBaseEntity, **_P](
|
||||
translation_key="cannot_retrieve_data",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except DeviceStorageFailureError as err:
|
||||
self.coordinator.last_update_success = False
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_storage_failure",
|
||||
) from err
|
||||
except CannotAuthenticate:
|
||||
self.coordinator.last_update_success = False
|
||||
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||
|
||||
@@ -7,11 +7,8 @@ is_value:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
threshold:
|
||||
required: true
|
||||
selector:
|
||||
|
||||
@@ -43,21 +43,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"decrement": {
|
||||
"description": "Decrements a counter by its step size.",
|
||||
|
||||
@@ -7,12 +7,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -210,21 +210,6 @@
|
||||
"name": "Window"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"close_cover": {
|
||||
"description": "Closes a cover.",
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
from devolo_plc_api import Device
|
||||
from devolo_plc_api.exceptions.device import DeviceNotFound
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.const import (
|
||||
@@ -17,6 +18,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
from .const import (
|
||||
@@ -123,6 +125,25 @@ async def async_setup_entry(
|
||||
|
||||
entry.runtime_data.coordinators = coordinators
|
||||
|
||||
# Ensure the device exists before forwarding to platforms, so that the
|
||||
# device tracker (which looks up the device by wifi station MAC) is not
|
||||
# racing the other platforms that create the device via DeviceInfo.
|
||||
device_info = dr.DeviceInfo(
|
||||
configuration_url=URL.build(scheme="http", host=device.ip),
|
||||
identifiers={(DOMAIN, str(device.serial_number))},
|
||||
manufacturer="devolo",
|
||||
model=device.product,
|
||||
model_id=device.mt_number,
|
||||
serial_number=device.serial_number,
|
||||
sw_version=device.firmware_version,
|
||||
)
|
||||
if device.mac:
|
||||
device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, device.mac)}
|
||||
dr.async_get(hass).async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
**device_info,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, platforms(device))
|
||||
|
||||
entry.async_on_unload(
|
||||
|
||||
@@ -117,7 +117,7 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription[Any, Any]] = {
|
||||
key=LAST_RESTART,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
device_class=SensorDeviceClass.UPTIME,
|
||||
value_func=_last_restart,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -75,9 +75,6 @@
|
||||
"connected_wifi_clients": {
|
||||
"name": "Connected Wi-Fi clients"
|
||||
},
|
||||
"last_restart": {
|
||||
"name": "Last restart of the device"
|
||||
},
|
||||
"neighboring_wifi_networks": {
|
||||
"name": "Neighboring Wi-Fi networks"
|
||||
},
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -31,21 +31,6 @@
|
||||
"name": "Door is open"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Door",
|
||||
"triggers": {
|
||||
"closed": {
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -6,4 +6,4 @@ from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "duco"
|
||||
PLATFORMS = [Platform.FAN, Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==18.1.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==18.2.0"]
|
||||
}
|
||||
|
||||
@@ -715,6 +715,9 @@ class EnergyPowerSensor(SensorEntity):
|
||||
self._attr_native_value = None
|
||||
return
|
||||
|
||||
self._attr_native_unit_of_measurement = source_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT
|
||||
)
|
||||
self._attr_native_value = value * -1
|
||||
|
||||
elif self._is_combined:
|
||||
@@ -763,13 +766,11 @@ class EnergyPowerSensor(SensorEntity):
|
||||
# Check first sensor
|
||||
if source_entry := entity_reg.async_get(self._source_sensors[0]):
|
||||
device_id = source_entry.device_id
|
||||
# For combined mode, always use Watts because we may have different source units; for inverted mode, copy source unit
|
||||
# Combined mode always emits Watts because we convert
|
||||
# heterogeneous source units internally. For inverted mode the
|
||||
# unit is copied from the source state in _update_state.
|
||||
if self._is_combined:
|
||||
self._attr_native_unit_of_measurement = UnitOfPower.WATT
|
||||
else:
|
||||
self._attr_native_unit_of_measurement = (
|
||||
source_entry.unit_of_measurement
|
||||
)
|
||||
# Get source name from registry
|
||||
source_name = source_entry.name or source_entry.original_name
|
||||
# Assign power sensor to same device as source sensor(s)
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -93,24 +93,11 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"direction": {
|
||||
"options": {
|
||||
"forward": "Forward",
|
||||
"reverse": "Reverse"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -7,12 +7,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -695,7 +695,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
_LOGGER.debug("Device tracker cleanup triggered")
|
||||
device_hosts = {self.mac: Device(True, "", "", "", "", None)}
|
||||
if self.device_discovery_enabled:
|
||||
device_hosts = await self._async_update_hosts_info()
|
||||
device_hosts.update(await self._async_update_hosts_info())
|
||||
entity_reg: er.EntityRegistry = er.async_get(self.hass)
|
||||
config_entry = self.config_entry
|
||||
|
||||
|
||||
@@ -294,7 +294,6 @@ CONNECTION_SENSOR_TYPES: tuple[FritzConnectionSensorEntityDescription, ...] = (
|
||||
DEVICE_SENSOR_TYPES: tuple[FritzDeviceSensorEntityDescription, ...] = (
|
||||
FritzDeviceSensorEntityDescription(
|
||||
key="device_uptime",
|
||||
translation_key="device_uptime",
|
||||
device_class=SensorDeviceClass.UPTIME,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_device_uptime_state,
|
||||
|
||||
@@ -120,9 +120,6 @@
|
||||
"cpu_temperature": {
|
||||
"name": "CPU temperature"
|
||||
},
|
||||
"device_uptime": {
|
||||
"name": "Last restart"
|
||||
},
|
||||
"external_ip": {
|
||||
"name": "External IP"
|
||||
},
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260325.8"]
|
||||
"requirements": ["home-assistant-frontend==20260429.0"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CLIMATE,
|
||||
Platform.NUMBER,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
|
||||
|
||||
21
homeassistant/components/fumis/diagnostics.py
Normal file
21
homeassistant/components/fumis/diagnostics.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Diagnostics support for Fumis."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import FumisConfigEntry
|
||||
|
||||
TO_REDACT_UNIT = {"id", "ip"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: FumisConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
data = await entry.runtime_data.client.raw_status()
|
||||
data["unit"] = async_redact_data(data["unit"], TO_REDACT_UNIT)
|
||||
return data
|
||||
@@ -5,13 +5,64 @@
|
||||
"default": "mdi:clock-sync"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"fan_speed": {
|
||||
"default": "mdi:fan"
|
||||
},
|
||||
"power_level": {
|
||||
"default": "mdi:fire"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"alert": {
|
||||
"default": "mdi:alert",
|
||||
"state": {
|
||||
"airflow_malfunction": "mdi:fan-off",
|
||||
"door_open": "mdi:door-open",
|
||||
"flue_gas_warning": "mdi:thermometer-alert",
|
||||
"low_battery": "mdi:battery-alert",
|
||||
"low_fuel": "mdi:gauge-empty",
|
||||
"none": "mdi:check-circle",
|
||||
"service_due": "mdi:wrench-clock",
|
||||
"speed_sensor_failure": "mdi:fan-alert"
|
||||
}
|
||||
},
|
||||
"combustion_chamber_temperature": {
|
||||
"default": "mdi:thermometer-high"
|
||||
},
|
||||
"detailed_stove_status": {
|
||||
"default": "mdi:fireplace"
|
||||
},
|
||||
"error": {
|
||||
"default": "mdi:alert-circle",
|
||||
"state": {
|
||||
"chimney_alarm": "mdi:broom",
|
||||
"chimney_dirty": "mdi:broom",
|
||||
"door_alarm": "mdi:door-open",
|
||||
"fire_error": "mdi:fire-alert",
|
||||
"flue_gas_overtemp": "mdi:thermometer-high",
|
||||
"fuel_ignition_timeout": "mdi:fire-off",
|
||||
"gas_alarm": "mdi:alert-circle",
|
||||
"general_error": "mdi:alert-circle",
|
||||
"grate_error": "mdi:alert-circle",
|
||||
"ignition_failed": "mdi:fire-alert",
|
||||
"mfdoor_alarm": "mdi:door-open",
|
||||
"no_pellet_alarm": "mdi:gauge-empty",
|
||||
"none": "mdi:check-circle",
|
||||
"ntc1_alarm": "mdi:thermometer-alert",
|
||||
"ntc2_alarm": "mdi:thermometer-alert",
|
||||
"ntc3_alarm": "mdi:thermometer-alert",
|
||||
"pressure_alarm": "mdi:gauge-empty",
|
||||
"pressure_sensor_off": "mdi:gauge-empty",
|
||||
"safety_switch": "mdi:shield-alert",
|
||||
"sensor_t01_t02": "mdi:thermometer-alert",
|
||||
"sensor_t01_t03": "mdi:thermometer-alert",
|
||||
"sensor_t02": "mdi:thermometer-alert",
|
||||
"sensor_t03_t05": "mdi:thermometer-alert",
|
||||
"sensor_t04": "mdi:thermometer-alert",
|
||||
"tc1_alarm": "mdi:thermometer-alert"
|
||||
}
|
||||
},
|
||||
"fan_1_speed": {
|
||||
"default": "mdi:fan"
|
||||
},
|
||||
@@ -48,6 +99,14 @@
|
||||
"wifi_signal_strength": {
|
||||
"default": "mdi:wifi"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"eco_mode": {
|
||||
"default": "mdi:leaf"
|
||||
},
|
||||
"timer": {
|
||||
"default": "mdi:timer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["fumis"],
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["fumis==0.4.0"]
|
||||
}
|
||||
|
||||
97
homeassistant/components/fumis/number.py
Normal file
97
homeassistant/components/fumis/number.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""Support for Fumis number entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from fumis import Fumis, FumisInfo
|
||||
|
||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import FumisConfigEntry, FumisDataUpdateCoordinator
|
||||
from .entity import FumisEntity
|
||||
from .helpers import fumis_exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FumisNumberEntityDescription(NumberEntityDescription):
|
||||
"""Describes a Fumis number entity."""
|
||||
|
||||
has_fn: Callable[[FumisInfo], bool] = lambda _: True
|
||||
value_fn: Callable[[FumisInfo], float | None]
|
||||
set_fn: Callable[[Fumis, float], Awaitable[Any]]
|
||||
|
||||
|
||||
NUMBERS: tuple[FumisNumberEntityDescription, ...] = (
|
||||
FumisNumberEntityDescription(
|
||||
key="fan_speed",
|
||||
translation_key="fan_speed",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
native_min_value=0,
|
||||
native_max_value=5,
|
||||
native_step=1,
|
||||
has_fn=lambda data: len(data.controller.fans) > 0,
|
||||
value_fn=lambda data: (
|
||||
data.controller.fans[0].speed if data.controller.fans else None
|
||||
),
|
||||
set_fn=lambda client, value: client.set_fan_speed(int(value)),
|
||||
),
|
||||
FumisNumberEntityDescription(
|
||||
key="power_level",
|
||||
translation_key="power_level",
|
||||
native_min_value=1,
|
||||
native_max_value=5,
|
||||
native_step=1,
|
||||
value_fn=lambda data: data.controller.power.set_power,
|
||||
set_fn=lambda client, value: client.set_power(int(value)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: FumisConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Fumis number entities based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
FumisNumberEntity(coordinator=coordinator, description=description)
|
||||
for description in NUMBERS
|
||||
if description.has_fn(coordinator.data)
|
||||
)
|
||||
|
||||
|
||||
class FumisNumberEntity(FumisEntity, NumberEntity):
|
||||
"""Defines a Fumis number entity."""
|
||||
|
||||
entity_description: FumisNumberEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: FumisDataUpdateCoordinator,
|
||||
description: FumisNumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Fumis number entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the current value."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
@fumis_exception_handler
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set a new value."""
|
||||
await self.entity_description.set_fn(self.coordinator.client, value)
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -41,7 +41,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
|
||||
@@ -5,8 +5,9 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from fumis import FumisInfo, StoveState, StoveStatus
|
||||
from fumis import FumisInfo, StoveAlert, StoveError, StoveState, StoveStatus
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -34,15 +35,52 @@ from .entity import FumisEntity
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
def _code_to_state(code: StoveAlert | StoveError | None) -> str | None:
|
||||
"""Convert a stove alert or error code to a sensor state value.
|
||||
|
||||
Returns "none" when there is no active alert/error, None when the code
|
||||
is unknown, or the enum member name in lowercase for known codes.
|
||||
"""
|
||||
if code is None:
|
||||
return "none"
|
||||
if code.name == "UNKNOWN":
|
||||
return None
|
||||
return code.name.lower()
|
||||
|
||||
|
||||
def _code_to_attr(code: StoveAlert | StoveError | None) -> dict[str, str | None]:
|
||||
"""Convert a stove alert or error code to extra state attributes."""
|
||||
if code is None or code.name == "UNKNOWN":
|
||||
return {"code": None}
|
||||
return {"code": code.value}
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FumisSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes a Fumis sensor entity."""
|
||||
|
||||
attr_fn: Callable[[FumisInfo], dict[str, Any]] | None = None
|
||||
has_fn: Callable[[FumisInfo], bool] = lambda _: True
|
||||
value_fn: Callable[[FumisInfo], datetime | float | int | str | None]
|
||||
|
||||
|
||||
SENSORS: tuple[FumisSensorEntityDescription, ...] = (
|
||||
FumisSensorEntityDescription(
|
||||
key="alert",
|
||||
translation_key="alert",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[
|
||||
"none",
|
||||
*(
|
||||
alert.name.lower()
|
||||
for alert in StoveAlert
|
||||
if alert != StoveAlert.UNKNOWN
|
||||
),
|
||||
],
|
||||
value_fn=lambda data: _code_to_state(data.controller.stove_alert),
|
||||
attr_fn=lambda data: _code_to_attr(data.controller.stove_alert),
|
||||
),
|
||||
FumisSensorEntityDescription(
|
||||
key="combustion_chamber_temperature",
|
||||
translation_key="combustion_chamber_temperature",
|
||||
@@ -69,6 +107,22 @@ SENSORS: tuple[FumisSensorEntityDescription, ...] = (
|
||||
else data.controller.stove_status.name.lower()
|
||||
),
|
||||
),
|
||||
FumisSensorEntityDescription(
|
||||
key="error",
|
||||
translation_key="error",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
options=[
|
||||
"none",
|
||||
*(
|
||||
error.name.lower()
|
||||
for error in StoveError
|
||||
if error != StoveError.UNKNOWN
|
||||
),
|
||||
],
|
||||
value_fn=lambda data: _code_to_state(data.controller.stove_error),
|
||||
attr_fn=lambda data: _code_to_attr(data.controller.stove_error),
|
||||
),
|
||||
FumisSensorEntityDescription(
|
||||
key="fan_1_speed",
|
||||
translation_key="fan_1_speed",
|
||||
@@ -267,6 +321,13 @@ class FumisSensorEntity(FumisEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return additional state attributes."""
|
||||
if self.entity_description.attr_fn is None:
|
||||
return None
|
||||
return self.entity_description.attr_fn(self.coordinator.data)
|
||||
|
||||
@property
|
||||
def native_value(self) -> datetime | float | int | str | None:
|
||||
"""Return the sensor value."""
|
||||
|
||||
@@ -58,7 +58,28 @@
|
||||
"name": "Sync clock"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"fan_speed": {
|
||||
"name": "Fan speed"
|
||||
},
|
||||
"power_level": {
|
||||
"name": "Power level"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"alert": {
|
||||
"name": "Alert",
|
||||
"state": {
|
||||
"airflow_malfunction": "Airflow sensor malfunction",
|
||||
"door_open": "Door open",
|
||||
"flue_gas_warning": "Flue gas temperature warning",
|
||||
"low_battery": "Low battery",
|
||||
"low_fuel": "Low fuel level",
|
||||
"none": "No alert",
|
||||
"service_due": "Service due",
|
||||
"speed_sensor_failure": "Speed sensor failure"
|
||||
}
|
||||
},
|
||||
"combustion_chamber_temperature": {
|
||||
"name": "Combustion chamber"
|
||||
},
|
||||
@@ -81,6 +102,36 @@
|
||||
"wood_start": "Wood start"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"name": "Error",
|
||||
"state": {
|
||||
"chimney_alarm": "Chimney alarm",
|
||||
"chimney_dirty": "Chimney or burning pot dirty",
|
||||
"door_alarm": "Door alarm",
|
||||
"fire_error": "Fire error",
|
||||
"flue_gas_overtemp": "Flue gas overtemperature",
|
||||
"fuel_ignition_timeout": "Fuel ignition timeout",
|
||||
"gas_alarm": "Gas alarm",
|
||||
"general_error": "General error",
|
||||
"grate_error": "Grate error",
|
||||
"ignition_failed": "Ignition failed",
|
||||
"mfdoor_alarm": "MFDoor alarm",
|
||||
"no_pellet_alarm": "No pellet alarm",
|
||||
"none": "No error",
|
||||
"ntc1_alarm": "NTC1 alarm",
|
||||
"ntc2_alarm": "NTC2 alarm",
|
||||
"ntc3_alarm": "NTC3 alarm",
|
||||
"pressure_alarm": "Pressure alarm",
|
||||
"pressure_sensor_off": "Pressure sensor off",
|
||||
"safety_switch": "Safety switch tripped",
|
||||
"sensor_t01_t02": "Sensor T01/T02 malfunction",
|
||||
"sensor_t01_t03": "Sensor T01/T03 malfunction",
|
||||
"sensor_t02": "Sensor T02 malfunction",
|
||||
"sensor_t03_t05": "Sensor T03/T05 malfunction",
|
||||
"sensor_t04": "Sensor T04 malfunction",
|
||||
"tc1_alarm": "TC1 alarm"
|
||||
}
|
||||
},
|
||||
"fan_1_speed": {
|
||||
"name": "Fan 1 speed"
|
||||
},
|
||||
@@ -137,6 +188,14 @@
|
||||
"wifi_signal_strength": {
|
||||
"name": "Wi-Fi signal strength"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"eco_mode": {
|
||||
"name": "Eco mode"
|
||||
},
|
||||
"timer": {
|
||||
"name": "Timer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
100
homeassistant/components/fumis/switch.py
Normal file
100
homeassistant/components/fumis/switch.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""Support for Fumis switch entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from fumis import Fumis, FumisInfo
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import FumisConfigEntry, FumisDataUpdateCoordinator
|
||||
from .entity import FumisEntity
|
||||
from .helpers import fumis_exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FumisSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Describes a Fumis switch entity."""
|
||||
|
||||
has_fn: Callable[[FumisInfo], bool] = lambda _: True
|
||||
is_on_fn: Callable[[FumisInfo], bool]
|
||||
turn_on_fn: Callable[[Fumis], Awaitable[Any]]
|
||||
turn_off_fn: Callable[[Fumis], Awaitable[Any]]
|
||||
|
||||
|
||||
SWITCHES: tuple[FumisSwitchEntityDescription, ...] = (
|
||||
FumisSwitchEntityDescription(
|
||||
key="eco_mode",
|
||||
translation_key="eco_mode",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
has_fn=lambda data: data.controller.eco_mode is not None,
|
||||
is_on_fn=lambda data: (
|
||||
data.controller.eco_mode.enabled if data.controller.eco_mode else False
|
||||
),
|
||||
turn_on_fn=lambda client: client.set_eco_mode(enabled=True),
|
||||
turn_off_fn=lambda client: client.set_eco_mode(enabled=False),
|
||||
),
|
||||
FumisSwitchEntityDescription(
|
||||
key="timer",
|
||||
translation_key="timer",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
is_on_fn=lambda data: data.controller.timer_enable,
|
||||
turn_on_fn=lambda client: client.set_timer(enabled=True),
|
||||
turn_off_fn=lambda client: client.set_timer(enabled=False),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: FumisConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Fumis switch entities based on a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
FumisSwitchEntity(coordinator=coordinator, description=description)
|
||||
for description in SWITCHES
|
||||
if description.has_fn(coordinator.data)
|
||||
)
|
||||
|
||||
|
||||
class FumisSwitchEntity(FumisEntity, SwitchEntity):
|
||||
"""Defines a Fumis switch entity."""
|
||||
|
||||
entity_description: FumisSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: FumisDataUpdateCoordinator,
|
||||
description: FumisSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Fumis switch entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the switch."""
|
||||
return self.entity_description.is_on_fn(self.coordinator.data)
|
||||
|
||||
@fumis_exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the switch."""
|
||||
await self.entity_description.turn_on_fn(self.coordinator.client)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@fumis_exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the switch."""
|
||||
await self.entity_description.turn_off_fn(self.coordinator.client)
|
||||
await self.coordinator.async_request_refresh()
|
||||
@@ -3,11 +3,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -31,21 +31,6 @@
|
||||
"name": "Garage door is open"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Garage door",
|
||||
"triggers": {
|
||||
"closed": {
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -87,17 +87,6 @@ DESCRIPTIONS = (
|
||||
char=Valve.remaining_open_time,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
),
|
||||
GardenaBluetoothNumberEntityDescription(
|
||||
key=AquaContourWatering.remaining_watering_time.unique_id,
|
||||
translation_key="remaining_watering_time",
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_min_value=0.0,
|
||||
native_max_value=24 * 60 * 60,
|
||||
native_step=60.0,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
char=AquaContourWatering.remaining_watering_time,
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
),
|
||||
GardenaBluetoothNumberEntityDescription(
|
||||
key=DeviceConfiguration.rain_pause.unique_id,
|
||||
translation_key="rain_pause",
|
||||
|
||||
@@ -8,6 +8,7 @@ from datetime import UTC, datetime, timedelta
|
||||
|
||||
from gardena_bluetooth.const import (
|
||||
AquaContourBattery,
|
||||
AquaContourWatering,
|
||||
Battery,
|
||||
EventHistory,
|
||||
FlowStatistics,
|
||||
@@ -218,7 +219,22 @@ async def async_setup_entry(
|
||||
if description.char.unique_id in coordinator.characteristics
|
||||
]
|
||||
if Valve.remaining_open_time.unique_id in coordinator.characteristics:
|
||||
entities.append(GardenaBluetoothRemainSensor(coordinator))
|
||||
entities.append(
|
||||
GardenaBluetoothRemainSensor(
|
||||
coordinator, Valve.remaining_open_time, "remaining_open_timestamp"
|
||||
)
|
||||
)
|
||||
if (
|
||||
AquaContourWatering.remaining_watering_time.unique_id
|
||||
in coordinator.characteristics
|
||||
):
|
||||
entities.append(
|
||||
GardenaBluetoothRemainSensor(
|
||||
coordinator,
|
||||
AquaContourWatering.remaining_watering_time,
|
||||
"remaining_watering_timestamp",
|
||||
)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -245,18 +261,21 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity):
|
||||
|
||||
_attr_device_class = SensorDeviceClass.TIMESTAMP
|
||||
_attr_native_value: datetime | None = None
|
||||
_attr_translation_key = "remaining_open_timestamp"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: GardenaBluetoothCoordinator,
|
||||
char: Characteristic[int],
|
||||
key: str,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, {Valve.remaining_open_time.uuid})
|
||||
self._attr_unique_id = f"{coordinator.address}-remaining_open_timestamp"
|
||||
super().__init__(coordinator, {char.uuid})
|
||||
self._attr_unique_id = f"{coordinator.address}-{key}"
|
||||
self._attr_translation_key = key
|
||||
self._char = char
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
value = self.coordinator.get_cached(Valve.remaining_open_time)
|
||||
value = self.coordinator.get_cached(self._char)
|
||||
if not value:
|
||||
self._attr_native_value = None
|
||||
super()._handle_coordinator_update()
|
||||
@@ -271,8 +290,7 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity):
|
||||
error = time - self._attr_native_value
|
||||
if abs(error.total_seconds()) > 10:
|
||||
self._attr_native_value = time
|
||||
super()._handle_coordinator_update()
|
||||
return
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -134,6 +134,9 @@
|
||||
"remaining_open_timestamp": {
|
||||
"name": "Valve closing"
|
||||
},
|
||||
"remaining_watering_timestamp": {
|
||||
"name": "Watering finished"
|
||||
},
|
||||
"sensor_battery_level": {
|
||||
"name": "Sensor battery"
|
||||
},
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -31,21 +31,6 @@
|
||||
"name": "Gate is open"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Gate",
|
||||
"triggers": {
|
||||
"closed": {
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for:
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
@@ -9,12 +9,13 @@ from aiogithubapi import GitHubAPI
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import (
|
||||
SERVER_SOFTWARE,
|
||||
async_get_clientsession,
|
||||
)
|
||||
|
||||
from .const import CONF_REPOSITORIES, CONF_REPOSITORY, SUBENTRY_TYPE_REPOSITORY
|
||||
from .const import CONF_REPOSITORIES, CONF_REPOSITORY, DOMAIN, SUBENTRY_TYPE_REPOSITORY
|
||||
from .coordinator import GithubConfigEntry, GitHubDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
@@ -68,6 +69,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> b
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
if entry.minor_version == 1:
|
||||
dev_reg = dr.async_get(hass)
|
||||
# In minor version 2 we migrated repositories from entry options to
|
||||
# subentries, so we need to convert the list from
|
||||
# entry.options[CONF_REPOSITORIES] into individual subentries.
|
||||
@@ -78,8 +80,13 @@ async def async_migrate_entry(hass: HomeAssistant, entry: GithubConfigEntry) ->
|
||||
title=repository,
|
||||
unique_id=repository,
|
||||
)
|
||||
|
||||
hass.config_entries.async_add_subentry(entry, subentry)
|
||||
|
||||
if device := dev_reg.async_get_device({(DOMAIN, repository)}):
|
||||
dev_reg.async_update_device(
|
||||
device.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
add_config_subentry_id=subentry.subentry_id,
|
||||
add_config_entry_id=entry.entry_id,
|
||||
)
|
||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||
return True
|
||||
|
||||
@@ -9,8 +9,10 @@ from typing import cast
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.permissions import filter_entity_ids_by_permission
|
||||
from homeassistant.auth.permissions.const import POLICY_READ
|
||||
from homeassistant.components import frontend
|
||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
||||
from homeassistant.components.http import KEY_HASS, KEY_HASS_USER, HomeAssistantView
|
||||
from homeassistant.components.recorder import get_instance, history
|
||||
from homeassistant.components.recorder.util import session_scope
|
||||
from homeassistant.const import CONF_EXCLUDE, CONF_INCLUDE
|
||||
@@ -83,6 +85,12 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
"Invalid filter_entity_id", HTTPStatus.BAD_REQUEST
|
||||
)
|
||||
|
||||
entity_ids = filter_entity_ids_by_permission(
|
||||
request[KEY_HASS_USER], entity_ids, POLICY_READ
|
||||
)
|
||||
if not entity_ids:
|
||||
return self.json([])
|
||||
|
||||
now = dt_util.utcnow()
|
||||
if datetime_:
|
||||
start_time = dt_util.as_utc(datetime_)
|
||||
|
||||
@@ -11,6 +11,8 @@ from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.permissions import filter_entity_ids_by_permission
|
||||
from homeassistant.auth.permissions.const import POLICY_READ
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.recorder import get_instance, history
|
||||
from homeassistant.components.websocket_api import ActiveConnection, messages
|
||||
@@ -138,6 +140,13 @@ async def ws_get_history_during_period(
|
||||
connection.send_error(msg["id"], "invalid_entity_ids", "Invalid entity_ids")
|
||||
return
|
||||
|
||||
entity_ids = filter_entity_ids_by_permission(
|
||||
connection.user, entity_ids, POLICY_READ
|
||||
)
|
||||
if not entity_ids:
|
||||
connection.send_result(msg["id"], {})
|
||||
return
|
||||
|
||||
include_start_time_state = msg["include_start_time_state"]
|
||||
no_attributes = msg["no_attributes"]
|
||||
|
||||
@@ -444,6 +453,13 @@ async def ws_stream(
|
||||
connection.send_error(msg["id"], "invalid_entity_ids", "Invalid entity_ids")
|
||||
return
|
||||
|
||||
entity_ids = filter_entity_ids_by_permission(
|
||||
connection.user, entity_ids, POLICY_READ
|
||||
)
|
||||
if not entity_ids:
|
||||
_async_send_empty_response(connection, msg_id, start_time, end_time)
|
||||
return
|
||||
|
||||
include_start_time_state = msg["include_start_time_state"]
|
||||
significant_changes_only = msg["significant_changes_only"]
|
||||
no_attributes = msg["no_attributes"]
|
||||
|
||||
@@ -637,16 +637,19 @@ class HomeConnectApplianceCoordinator(DataUpdateCoordinator[HomeConnectAppliance
|
||||
options.update(await self.get_options_definitions(resolved_program_key))
|
||||
|
||||
for option in options.values():
|
||||
option_value = option.constraints.default if option.constraints else None
|
||||
if option_value is not None:
|
||||
option_event_key = EventKey(option.key)
|
||||
option_event_key = EventKey(option.key)
|
||||
if (
|
||||
option_event_key not in events
|
||||
and option.constraints
|
||||
and (option_default_value := option.constraints.default) is not None
|
||||
):
|
||||
events[option_event_key] = Event(
|
||||
option_event_key,
|
||||
option.key.value,
|
||||
0,
|
||||
"",
|
||||
"",
|
||||
option_value,
|
||||
option_default_value,
|
||||
option.name,
|
||||
unit=option.unit,
|
||||
)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.8.0"]
|
||||
"requirements": ["homematicip==2.9.0"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["huawei_lte_api.Session"],
|
||||
"requirements": ["huawei-lte-api==1.11.0", "url-normalize==2.2.1"],
|
||||
"requirements": ["huawei-lte-api==1.11.0", "url-normalize==3.0.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: condition
|
||||
|
||||
.condition_common_for: &condition_common_for
|
||||
target: *condition_humidifier_target
|
||||
|
||||
@@ -162,21 +162,6 @@
|
||||
"message": "Provided humidity {humidity} is not valid. Accepted range is {min_humidity} to {max_humidity}."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"set_humidity": {
|
||||
"description": "Sets the target humidity of a humidifier.",
|
||||
|
||||
@@ -7,12 +7,8 @@
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
automation_behavior:
|
||||
mode: trigger
|
||||
for: &trigger_for
|
||||
required: true
|
||||
default: 00:00:00
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user