forked from home-assistant/core
Compare commits
1 Commits
llm-neverm
...
dev_debug_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5f7f4339f |
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
|
||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.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@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
|
||||
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
||||
47
.github/workflows/ci.yaml
vendored
47
.github/workflows/ci.yaml
vendored
@@ -5,7 +5,7 @@ run-name: "${{ github.event_name == 'workflow_dispatch' && format('CI: {0}', git
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- dev_debug_translation_issue
|
||||
- rc
|
||||
- master
|
||||
pull_request: ~
|
||||
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 11
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 9
|
||||
HA_SHORT_VERSION: "2025.1"
|
||||
HA_SHORT_VERSION: "2024.12"
|
||||
DEFAULT_PYTHON: "3.12"
|
||||
ALL_PYTHON_VERSIONS: "['3.12', '3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -198,6 +198,8 @@ jobs:
|
||||
skip_coverage="true"
|
||||
fi
|
||||
|
||||
test_groups="[4]"
|
||||
|
||||
# Output & sent to GitHub Actions
|
||||
echo "mariadb_groups: ${mariadb_groups}"
|
||||
echo "mariadb_groups=${mariadb_groups}" >> $GITHUB_OUTPUT
|
||||
@@ -658,6 +660,7 @@ jobs:
|
||||
if: |
|
||||
github.event.inputs.mypy-only != 'true'
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
&& 'true' == 'false'
|
||||
|| github.event.inputs.pylint-only == 'true'
|
||||
needs:
|
||||
- info
|
||||
@@ -704,6 +707,7 @@ jobs:
|
||||
if: |
|
||||
(github.event.inputs.mypy-only != 'true'
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
&& 'true' == 'false'
|
||||
|| github.event.inputs.pylint-only == 'true')
|
||||
&& (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true')
|
||||
needs:
|
||||
@@ -819,44 +823,10 @@ jobs:
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
- gen-requirements-all
|
||||
- hassfest
|
||||
- lint-other
|
||||
- lint-ruff
|
||||
- lint-ruff-format
|
||||
- mypy
|
||||
name: Split tests for full run
|
||||
steps:
|
||||
- name: Install additional OS dependencies
|
||||
run: |
|
||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install \
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libgammu-dev
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.3.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.1.2
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Run split_tests.py
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
|
||||
- name: Upload pytest_buckets
|
||||
uses: actions/upload-artifact@v4.4.3
|
||||
with:
|
||||
@@ -947,11 +917,10 @@ jobs:
|
||||
|
||||
echo "Test group ${{ matrix.group }}: $(sed -n "${{ matrix.group }},1p" pytest_buckets.txt)"
|
||||
python3 -b -X dev -m pytest \
|
||||
-qq \
|
||||
-x \
|
||||
--timeout=9 \
|
||||
--durations=10 \
|
||||
--numprocesses auto \
|
||||
--snapshot-details \
|
||||
--dist=loadfile \
|
||||
${cov_params[@]} \
|
||||
-o console_output_style=count \
|
||||
@@ -995,6 +964,7 @@ jobs:
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
&& needs.info.outputs.mariadb_groups != '[]'
|
||||
&& 'true' == 'false'
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
@@ -1122,6 +1092,7 @@ jobs:
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
&& needs.info.outputs.postgresql_groups != '[]'
|
||||
&& 'true' == 'false'
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -136,4 +136,4 @@ tmp_cache
|
||||
.ropeproject
|
||||
|
||||
# Will be created from script/split_tests.py
|
||||
pytest_buckets.txt
|
||||
# pytest_buckets.txt
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.8.1
|
||||
rev: v0.8.0
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
@@ -41,7 +41,6 @@ homeassistant.util.unit_system
|
||||
# --- Add components below this line ---
|
||||
homeassistant.components
|
||||
homeassistant.components.abode.*
|
||||
homeassistant.components.acaia.*
|
||||
homeassistant.components.accuweather.*
|
||||
homeassistant.components.acer_projector.*
|
||||
homeassistant.components.acmeda.*
|
||||
@@ -406,7 +405,6 @@ homeassistant.components.ruuvitag_ble.*
|
||||
homeassistant.components.samsungtv.*
|
||||
homeassistant.components.scene.*
|
||||
homeassistant.components.schedule.*
|
||||
homeassistant.components.schlage.*
|
||||
homeassistant.components.scrape.*
|
||||
homeassistant.components.script.*
|
||||
homeassistant.components.search.*
|
||||
|
||||
16
.vscode/tasks.json
vendored
16
.vscode/tasks.json
vendored
@@ -87,22 +87,6 @@
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Update syrupy snapshots",
|
||||
"detail": "Update syrupy snapshots for a given integration.",
|
||||
"type": "shell",
|
||||
"command": "python3 -m pytest ./tests/components/${input:integrationName} --snapshot-update",
|
||||
"dependsOn": ["Compile English translations"],
|
||||
"group": {
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "new"
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Generate Requirements",
|
||||
"type": "shell",
|
||||
|
||||
@@ -25,5 +25,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioacaia"],
|
||||
"requirements": ["aioacaia==0.1.10"]
|
||||
"requirements": ["aioacaia==0.1.9"]
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: todo
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
No explicit event subscriptions.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device is expected to be offline most of the time, but needs to connect quickly once available.
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom actions are defined.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable:
|
||||
status: done
|
||||
comment: |
|
||||
Handled by coordinator.
|
||||
parallel-updates: done
|
||||
reauthentication-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
No authentication required.
|
||||
test-coverage: done
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: |
|
||||
No IP discovery.
|
||||
discovery:
|
||||
status: done
|
||||
comment: |
|
||||
Bluetooth discovery.
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device type integration.
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: |
|
||||
No noisy/non-essential entities.
|
||||
entity-translations: done
|
||||
exception-translations:
|
||||
status: exempt
|
||||
comment: |
|
||||
No custom exceptions.
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
Only parameter that could be changed (MAC = unique_id) would force a new config entry.
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
No repairs/issues.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device type integration.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: |
|
||||
Bluetooth connection.
|
||||
strict-typing: done
|
||||
@@ -37,7 +37,7 @@ STATE_KEY_POSITION = "position"
|
||||
|
||||
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_SET_POS): cv.string,
|
||||
vol.Optional(CONF_ADS_VAR_CLOSE): cv.string,
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Final, final
|
||||
|
||||
@@ -26,14 +27,26 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import make_entity_service_schema
|
||||
from homeassistant.helpers.deprecation import (
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
from homeassistant.helpers.frame import ReportBehavior, report_usage
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
from .const import ( # noqa: F401
|
||||
_DEPRECATED_FORMAT_NUMBER,
|
||||
_DEPRECATED_FORMAT_TEXT,
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_AWAY,
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_HOME,
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_NIGHT,
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_VACATION,
|
||||
_DEPRECATED_SUPPORT_ALARM_TRIGGER,
|
||||
ATTR_CHANGED_BY,
|
||||
ATTR_CODE_ARM_REQUIRED,
|
||||
DOMAIN,
|
||||
@@ -150,6 +163,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||
_alarm_control_panel_option_default_code: str | None = None
|
||||
|
||||
__alarm_legacy_state: bool = False
|
||||
__alarm_legacy_state_reported: bool = False
|
||||
|
||||
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||
"""Post initialisation processing."""
|
||||
@@ -166,7 +180,9 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||
unless already reported.
|
||||
"""
|
||||
if name == "_attr_state":
|
||||
self._report_deprecated_alarm_state_handling()
|
||||
if self.__alarm_legacy_state_reported is not True:
|
||||
self._report_deprecated_alarm_state_handling()
|
||||
self.__alarm_legacy_state_reported = True
|
||||
return super().__setattr__(name, value)
|
||||
|
||||
@callback
|
||||
@@ -178,7 +194,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||
) -> None:
|
||||
"""Start adding an entity to a platform."""
|
||||
super().add_to_platform_start(hass, platform, parallel_updates)
|
||||
if self.__alarm_legacy_state:
|
||||
if self.__alarm_legacy_state and not self.__alarm_legacy_state_reported:
|
||||
self._report_deprecated_alarm_state_handling()
|
||||
|
||||
@callback
|
||||
@@ -187,16 +203,19 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||
|
||||
Integrations should implement alarm_state instead of using state directly.
|
||||
"""
|
||||
report_usage(
|
||||
"is setting state directly."
|
||||
f" Entity {self.entity_id} ({type(self)}) should implement the 'alarm_state'"
|
||||
" property and return its state using the AlarmControlPanelState enum",
|
||||
core_integration_behavior=ReportBehavior.ERROR,
|
||||
custom_integration_behavior=ReportBehavior.LOG,
|
||||
breaks_in_ha_version="2025.11",
|
||||
integration_domain=self.platform.platform_name if self.platform else None,
|
||||
exclude_integrations={DOMAIN},
|
||||
)
|
||||
self.__alarm_legacy_state_reported = True
|
||||
if "custom_components" in type(self).__module__:
|
||||
# Do not report on core integrations as they have been fixed.
|
||||
report_issue = "report it to the custom integration author."
|
||||
_LOGGER.warning(
|
||||
"Entity %s (%s) is setting state directly"
|
||||
" which will stop working in HA Core 2025.11."
|
||||
" Entities should implement the 'alarm_state' property and"
|
||||
" return its state using the AlarmControlPanelState enum, please %s",
|
||||
self.entity_id,
|
||||
type(self),
|
||||
report_issue,
|
||||
)
|
||||
|
||||
@final
|
||||
@property
|
||||
@@ -398,3 +417,13 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
|
||||
self._alarm_control_panel_option_default_code = default_code
|
||||
return
|
||||
self._alarm_control_panel_option_default_code = None
|
||||
|
||||
|
||||
# As we import constants of the const module here, we need to add the following
|
||||
# functions to check for deprecated constants again
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
"""Provides the constants needed for component."""
|
||||
|
||||
from enum import IntFlag, StrEnum
|
||||
from functools import partial
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstantEnum,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
|
||||
DOMAIN: Final = "alarm_control_panel"
|
||||
|
||||
ATTR_CHANGED_BY: Final = "changed_by"
|
||||
@@ -31,6 +39,12 @@ class CodeFormat(StrEnum):
|
||||
NUMBER = "number"
|
||||
|
||||
|
||||
# These constants are deprecated as of Home Assistant 2022.5, can be removed in 2025.1
|
||||
# Please use the CodeFormat enum instead.
|
||||
_DEPRECATED_FORMAT_TEXT: Final = DeprecatedConstantEnum(CodeFormat.TEXT, "2025.1")
|
||||
_DEPRECATED_FORMAT_NUMBER: Final = DeprecatedConstantEnum(CodeFormat.NUMBER, "2025.1")
|
||||
|
||||
|
||||
class AlarmControlPanelEntityFeature(IntFlag):
|
||||
"""Supported features of the alarm control panel entity."""
|
||||
|
||||
@@ -42,6 +56,27 @@ class AlarmControlPanelEntityFeature(IntFlag):
|
||||
ARM_VACATION = 32
|
||||
|
||||
|
||||
# These constants are deprecated as of Home Assistant 2022.5
|
||||
# Please use the AlarmControlPanelEntityFeature enum instead.
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_HOME: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.ARM_HOME, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_AWAY: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.ARM_AWAY, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_NIGHT: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.ARM_NIGHT, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_ALARM_TRIGGER: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.TRIGGER, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_CUSTOM_BYPASS: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_ALARM_ARM_VACATION: Final = DeprecatedConstantEnum(
|
||||
AlarmControlPanelEntityFeature.ARM_VACATION, "2025.1"
|
||||
)
|
||||
|
||||
CONDITION_TRIGGERED: Final = "is_triggered"
|
||||
CONDITION_DISARMED: Final = "is_disarmed"
|
||||
CONDITION_ARMED_HOME: Final = "is_armed_home"
|
||||
@@ -49,3 +84,10 @@ CONDITION_ARMED_AWAY: Final = "is_armed_away"
|
||||
CONDITION_ARMED_NIGHT: Final = "is_armed_night"
|
||||
CONDITION_ARMED_VACATION: Final = "is_armed_vacation"
|
||||
CONDITION_ARMED_CUSTOM_BYPASS: Final = "is_armed_custom_bypass"
|
||||
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"loggers": ["adb_shell", "androidtv", "pure_python_adb"],
|
||||
"requirements": [
|
||||
"adb-shell[async]==0.4.4",
|
||||
"androidtv[async]==0.0.75",
|
||||
"androidtv[async]==0.0.73",
|
||||
"pure-python-adb[async]==0.3.0.dev0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyatv", "srptools"],
|
||||
"requirements": ["pyatv==0.16.0"],
|
||||
"requirements": ["pyatv==0.15.1"],
|
||||
"zeroconf": [
|
||||
"_mediaremotetv._tcp.local.",
|
||||
"_companion-link._tcp.local.",
|
||||
|
||||
@@ -1040,7 +1040,7 @@ class PipelineRun:
|
||||
:= await conversation.async_handle_sentence_triggers(
|
||||
self.hass, user_input
|
||||
)
|
||||
) is not None:
|
||||
):
|
||||
# Sentence trigger matched
|
||||
trigger_response = intent.IntentResponse(
|
||||
self.pipeline.conversation_language
|
||||
|
||||
@@ -6,6 +6,7 @@ from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, Protocol, cast
|
||||
|
||||
@@ -50,6 +51,12 @@ from homeassistant.core import (
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError
|
||||
from homeassistant.helpers import condition
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstant,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.entity import ToggleEntity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
@@ -79,7 +86,12 @@ from homeassistant.helpers.trace import (
|
||||
trace_get,
|
||||
trace_path,
|
||||
)
|
||||
from homeassistant.helpers.trigger import async_initialize_triggers
|
||||
from homeassistant.helpers.trigger import (
|
||||
TriggerActionType,
|
||||
TriggerData,
|
||||
TriggerInfo,
|
||||
async_initialize_triggers,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.dt import parse_datetime
|
||||
@@ -125,6 +137,20 @@ class IfAction(Protocol):
|
||||
"""AND all conditions."""
|
||||
|
||||
|
||||
# AutomationActionType, AutomationTriggerData,
|
||||
# and AutomationTriggerInfo are deprecated as of 2022.9.
|
||||
# Can be removed in 2025.1
|
||||
_DEPRECATED_AutomationActionType = DeprecatedConstant(
|
||||
TriggerActionType, "TriggerActionType", "2025.1"
|
||||
)
|
||||
_DEPRECATED_AutomationTriggerData = DeprecatedConstant(
|
||||
TriggerData, "TriggerData", "2025.1"
|
||||
)
|
||||
_DEPRECATED_AutomationTriggerInfo = DeprecatedConstant(
|
||||
TriggerInfo, "TriggerInfo", "2025.1"
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
"""Return true if specified automation entity_id is on.
|
||||
@@ -451,7 +477,6 @@ class UnavailableAutomationEntity(BaseAutomationEntity):
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Run when entity will be removed from hass."""
|
||||
await super().async_will_remove_from_hass()
|
||||
async_delete_issue(
|
||||
self.hass, DOMAIN, f"{self.entity_id}_validation_{self._validation_status}"
|
||||
@@ -1194,3 +1219,11 @@ def websocket_config(
|
||||
"config": automation.raw_config,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"""Support for Bang & Olufsen diagnostics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from . import BangOlufsenConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: BangOlufsenConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
data: dict = {
|
||||
"config_entry": config_entry.as_dict(),
|
||||
"websocket_connected": config_entry.runtime_data.client.websocket_connected,
|
||||
}
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert config_entry.unique_id
|
||||
|
||||
# Add media_player entity's state
|
||||
entity_registry = er.async_get(hass)
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
MEDIA_PLAYER_DOMAIN, DOMAIN, config_entry.unique_id
|
||||
):
|
||||
if state := hass.states.get(entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Remove context as it is not relevant
|
||||
state_dict.pop("context")
|
||||
data["media_player"] = state_dict
|
||||
|
||||
return data
|
||||
@@ -204,11 +204,13 @@ class BangOlufsenWebsocket(BangOlufsenBase):
|
||||
|
||||
def on_all_notifications_raw(self, notification: BaseWebSocketResponse) -> None:
|
||||
"""Receive all notifications."""
|
||||
debug_notification = {
|
||||
"device_id": self._device.id,
|
||||
"serial_number": int(self._unique_id),
|
||||
**notification,
|
||||
}
|
||||
|
||||
_LOGGER.debug("%s", debug_notification)
|
||||
self.hass.bus.async_fire(BANG_OLUFSEN_WEBSOCKET_EVENT, debug_notification)
|
||||
_LOGGER.debug("%s", notification)
|
||||
self.hass.bus.async_fire(
|
||||
BANG_OLUFSEN_WEBSOCKET_EVENT,
|
||||
{
|
||||
"device_id": self._device.id,
|
||||
"serial_number": int(self._unique_id),
|
||||
**notification,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import StrEnum
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Literal, final
|
||||
|
||||
@@ -15,6 +16,12 @@ from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstantEnum,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -119,7 +126,94 @@ class BinarySensorDeviceClass(StrEnum):
|
||||
|
||||
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(BinarySensorDeviceClass))
|
||||
|
||||
# DEVICE_CLASS* below are deprecated as of 2021.12
|
||||
# use the BinarySensorDeviceClass enum instead.
|
||||
DEVICE_CLASSES = [cls.value for cls in BinarySensorDeviceClass]
|
||||
_DEPRECATED_DEVICE_CLASS_BATTERY = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.BATTERY, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_BATTERY_CHARGING = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.BATTERY_CHARGING, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_CO = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.CO, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_COLD = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.COLD, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_CONNECTIVITY = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.CONNECTIVITY, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_DOOR = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.DOOR, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_GARAGE_DOOR = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.GARAGE_DOOR, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_GAS = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.GAS, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_HEAT = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.HEAT, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_LIGHT = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.LIGHT, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_LOCK = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.LOCK, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_MOISTURE = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.MOISTURE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_MOTION = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.MOTION, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_MOVING = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.MOVING, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_OCCUPANCY = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.OCCUPANCY, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_OPENING = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.OPENING, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_PLUG = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.PLUG, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_POWER = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.POWER, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_PRESENCE = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.PRESENCE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_PROBLEM = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.PROBLEM, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_RUNNING = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.RUNNING, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_SAFETY = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.SAFETY, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_SMOKE = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.SMOKE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_SOUND = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.SOUND, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_TAMPER = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.TAMPER, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_UPDATE = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.UPDATE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_VIBRATION = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.VIBRATION, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_WINDOW = DeprecatedConstantEnum(
|
||||
BinarySensorDeviceClass.WINDOW, "2025.1"
|
||||
)
|
||||
|
||||
# mypy: disallow-any-generics
|
||||
|
||||
@@ -200,3 +294,11 @@ class BinarySensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_)
|
||||
if (is_on := self.is_on) is None:
|
||||
return None
|
||||
return STATE_ON if is_on else STATE_OFF
|
||||
|
||||
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -292,6 +292,14 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
self._last_status_update = dt_util.utcnow()
|
||||
self._status = status
|
||||
|
||||
group_name = status.group_name
|
||||
if group_name != self._group_name:
|
||||
_LOGGER.debug("Group name change detected on device: %s", self.id)
|
||||
self._group_name = group_name
|
||||
|
||||
# rebuild ordered list of entity_ids that are in the group, master is first
|
||||
self._group_list = self.rebuild_bluesound_group()
|
||||
|
||||
self.async_write_ha_state()
|
||||
except PlayerUnreachableError:
|
||||
self._attr_available = False
|
||||
@@ -315,8 +323,6 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
self._sync_status = sync_status
|
||||
|
||||
self._group_list = self.rebuild_bluesound_group()
|
||||
|
||||
if sync_status.master is not None:
|
||||
self._is_master = False
|
||||
master_id = f"{sync_status.master.ip}:{sync_status.master.port}"
|
||||
@@ -613,32 +619,21 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
def rebuild_bluesound_group(self) -> list[str]:
|
||||
"""Rebuild the list of entities in speaker group."""
|
||||
if self.sync_status.master is None and self.sync_status.slaves is None:
|
||||
if self._group_name is None:
|
||||
return []
|
||||
|
||||
player_entities: list[BluesoundPlayer] = self.hass.data[DATA_BLUESOUND]
|
||||
device_group = self._group_name.split("+")
|
||||
|
||||
leader_sync_status: SyncStatus | None = None
|
||||
if self.sync_status.master is None:
|
||||
leader_sync_status = self.sync_status
|
||||
else:
|
||||
required_id = f"{self.sync_status.master.ip}:{self.sync_status.master.port}"
|
||||
for x in player_entities:
|
||||
if x.sync_status.id == required_id:
|
||||
leader_sync_status = x.sync_status
|
||||
break
|
||||
|
||||
if leader_sync_status is None or leader_sync_status.slaves is None:
|
||||
return []
|
||||
|
||||
follower_ids = [f"{x.ip}:{x.port}" for x in leader_sync_status.slaves]
|
||||
follower_names = [
|
||||
x.sync_status.name
|
||||
for x in player_entities
|
||||
if x.sync_status.id in follower_ids
|
||||
sorted_entities: list[BluesoundPlayer] = sorted(
|
||||
self.hass.data[DATA_BLUESOUND],
|
||||
key=lambda entity: entity.is_master,
|
||||
reverse=True,
|
||||
)
|
||||
return [
|
||||
entity.sync_status.name
|
||||
for entity in sorted_entities
|
||||
if entity.bluesound_device_name in device_group
|
||||
]
|
||||
follower_names.insert(0, leader_sync_status.name)
|
||||
return follower_names
|
||||
|
||||
async def async_unjoin(self) -> None:
|
||||
"""Unjoin the player from a group."""
|
||||
|
||||
@@ -27,18 +27,9 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_US
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from . import DOMAIN
|
||||
from .const import (
|
||||
CONF_ALLOWED_REGIONS,
|
||||
CONF_CAPTCHA_REGIONS,
|
||||
CONF_CAPTCHA_TOKEN,
|
||||
CONF_CAPTCHA_URL,
|
||||
CONF_GCID,
|
||||
CONF_READ_ONLY,
|
||||
CONF_REFRESH_TOKEN,
|
||||
)
|
||||
from .const import CONF_ALLOWED_REGIONS, CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -50,14 +41,7 @@ DATA_SCHEMA = vol.Schema(
|
||||
translation_key="regions",
|
||||
)
|
||||
),
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
)
|
||||
CAPTCHA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_CAPTCHA_TOKEN): str,
|
||||
},
|
||||
extra=vol.REMOVE_EXTRA,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -70,8 +54,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
get_region_from_name(data[CONF_REGION]),
|
||||
hcaptcha_token=data.get(CONF_CAPTCHA_TOKEN),
|
||||
verify=get_default_context(),
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -97,17 +79,15 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
data: dict[str, Any] = {}
|
||||
|
||||
_existing_entry_data: Mapping[str, Any] | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = self.data.pop("errors", {})
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None and not errors:
|
||||
if user_input is not None:
|
||||
unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}"
|
||||
await self.async_set_unique_id(unique_id)
|
||||
|
||||
@@ -116,35 +96,22 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
# Store user input for later use
|
||||
self.data.update(user_input)
|
||||
|
||||
# North America and Rest of World require captcha token
|
||||
if (
|
||||
self.data.get(CONF_REGION) in CONF_CAPTCHA_REGIONS
|
||||
and CONF_CAPTCHA_TOKEN not in self.data
|
||||
):
|
||||
return await self.async_step_captcha()
|
||||
|
||||
info = None
|
||||
try:
|
||||
info = await validate_input(self.hass, self.data)
|
||||
info = await validate_input(self.hass, user_input)
|
||||
entry_data = {
|
||||
**user_input,
|
||||
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
|
||||
CONF_GCID: info.get(CONF_GCID),
|
||||
}
|
||||
except MissingCaptcha:
|
||||
errors["base"] = "missing_captcha"
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
finally:
|
||||
self.data.pop(CONF_CAPTCHA_TOKEN, None)
|
||||
|
||||
if info:
|
||||
entry_data = {
|
||||
**self.data,
|
||||
CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN),
|
||||
CONF_GCID: info.get(CONF_GCID),
|
||||
}
|
||||
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data=entry_data
|
||||
@@ -161,7 +128,7 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
DATA_SCHEMA,
|
||||
self._existing_entry_data or self.data,
|
||||
self._existing_entry_data,
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
@@ -180,22 +147,6 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._existing_entry_data = self._get_reconfigure_entry().data
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_step_captcha(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show captcha form."""
|
||||
if user_input and user_input.get(CONF_CAPTCHA_TOKEN):
|
||||
self.data[CONF_CAPTCHA_TOKEN] = user_input[CONF_CAPTCHA_TOKEN].strip()
|
||||
return await self.async_step_user(self.data)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="captcha",
|
||||
data_schema=CAPTCHA_SCHEMA,
|
||||
description_placeholders={
|
||||
"captcha_url": CONF_CAPTCHA_URL.format(region=self.data[CONF_REGION])
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
|
||||
@@ -8,15 +8,10 @@ ATTR_DIRECTION = "direction"
|
||||
ATTR_VIN = "vin"
|
||||
|
||||
CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"]
|
||||
CONF_CAPTCHA_REGIONS = ["north_america", "rest_of_world"]
|
||||
CONF_READ_ONLY = "read_only"
|
||||
CONF_ACCOUNT = "account"
|
||||
CONF_REFRESH_TOKEN = "refresh_token"
|
||||
CONF_GCID = "gcid"
|
||||
CONF_CAPTCHA_TOKEN = "captcha_token"
|
||||
CONF_CAPTCHA_URL = (
|
||||
"https://bimmer-connected.readthedocs.io/en/stable/captcha/{region}.html"
|
||||
)
|
||||
|
||||
DATA_HASS_CONFIG = "hass_config"
|
||||
|
||||
|
||||
@@ -84,6 +84,11 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
|
||||
if self.account.refresh_token != old_refresh_token:
|
||||
self._update_config_entry_refresh_token(self.account.refresh_token)
|
||||
_LOGGER.debug(
|
||||
"bimmer_connected: refresh token %s > %s",
|
||||
old_refresh_token,
|
||||
self.account.refresh_token,
|
||||
)
|
||||
|
||||
def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None:
|
||||
"""Update or delete the refresh_token in the Config Entry."""
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"requirements": ["bimmer-connected[china]==0.17.0"]
|
||||
"requirements": ["bimmer-connected[china]==0.16.4"]
|
||||
}
|
||||
|
||||
@@ -7,16 +7,6 @@
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"region": "ConnectedDrive Region"
|
||||
}
|
||||
},
|
||||
"captcha": {
|
||||
"title": "Are you a robot?",
|
||||
"description": "A captcha is required for BMW login. Visit the external website to complete the challenge and submit the form. Copy the resulting token into the field below.\n\n{captcha_url}\n\nNo data will be exposed outside of your Home Assistant instance.",
|
||||
"data": {
|
||||
"captcha_token": "Captcha token"
|
||||
},
|
||||
"data_description": {
|
||||
"captcha_token": "One-time token retrieved from the captcha challenge."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -9,3 +9,4 @@ ATTR_ITEM_NAME: Final = "item"
|
||||
ATTR_NOTIFICATION_TYPE: Final = "message"
|
||||
|
||||
SERVICE_PUSH_NOTIFICATION = "send_message"
|
||||
UNIT_ITEMS = "items"
|
||||
|
||||
@@ -20,6 +20,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import BringConfigEntry
|
||||
from .const import UNIT_ITEMS
|
||||
from .coordinator import BringData, BringDataUpdateCoordinator
|
||||
from .entity import BringBaseEntity
|
||||
from .util import list_language, sum_attributes
|
||||
@@ -47,16 +48,19 @@ SENSOR_DESCRIPTIONS: tuple[BringSensorEntityDescription, ...] = (
|
||||
key=BringSensor.URGENT,
|
||||
translation_key=BringSensor.URGENT,
|
||||
value_fn=lambda lst, _: sum_attributes(lst, "urgent"),
|
||||
native_unit_of_measurement=UNIT_ITEMS,
|
||||
),
|
||||
BringSensorEntityDescription(
|
||||
key=BringSensor.CONVENIENT,
|
||||
translation_key=BringSensor.CONVENIENT,
|
||||
value_fn=lambda lst, _: sum_attributes(lst, "convenient"),
|
||||
native_unit_of_measurement=UNIT_ITEMS,
|
||||
),
|
||||
BringSensorEntityDescription(
|
||||
key=BringSensor.DISCOUNTED,
|
||||
translation_key=BringSensor.DISCOUNTED,
|
||||
value_fn=lambda lst, _: sum_attributes(lst, "discounted"),
|
||||
native_unit_of_measurement=UNIT_ITEMS,
|
||||
),
|
||||
BringSensorEntityDescription(
|
||||
key=BringSensor.LIST_LANGUAGE,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"common": {
|
||||
"shopping_list_items": "items"
|
||||
},
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
@@ -32,16 +29,13 @@
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"urgent": {
|
||||
"name": "Urgent",
|
||||
"unit_of_measurement": "[%key:component::bring::common::shopping_list_items%]"
|
||||
"name": "Urgent"
|
||||
},
|
||||
"convenient": {
|
||||
"name": "On occasion",
|
||||
"unit_of_measurement": "[%key:component::bring::common::shopping_list_items%]"
|
||||
"name": "On occasion"
|
||||
},
|
||||
"discounted": {
|
||||
"name": "Discount only",
|
||||
"unit_of_measurement": "[%key:component::bring::common::shopping_list_items%]"
|
||||
"name": "Discount only"
|
||||
},
|
||||
"list_language": {
|
||||
"name": "Region & language",
|
||||
|
||||
@@ -67,7 +67,9 @@ from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, VolDictType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import (
|
||||
from .const import ( # noqa: F401
|
||||
_DEPRECATED_STREAM_TYPE_HLS,
|
||||
_DEPRECATED_STREAM_TYPE_WEB_RTC,
|
||||
CAMERA_IMAGE_TIMEOUT,
|
||||
CAMERA_STREAM_SOURCE_TIMEOUT,
|
||||
CONF_DURATION,
|
||||
@@ -133,6 +135,16 @@ class CameraEntityFeature(IntFlag):
|
||||
STREAM = 2
|
||||
|
||||
|
||||
# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5.
|
||||
# Pleease use the CameraEntityFeature enum instead.
|
||||
_DEPRECATED_SUPPORT_ON_OFF: Final = DeprecatedConstantEnum(
|
||||
CameraEntityFeature.ON_OFF, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_STREAM: Final = DeprecatedConstantEnum(
|
||||
CameraEntityFeature.STREAM, "2025.1"
|
||||
)
|
||||
|
||||
|
||||
DEFAULT_CONTENT_TYPE: Final = "image/jpeg"
|
||||
ENTITY_IMAGE_URL: Final = "/api/camera_proxy/{0}?token={1}"
|
||||
|
||||
|
||||
@@ -3,8 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstantEnum,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -51,3 +58,17 @@ class StreamType(StrEnum):
|
||||
|
||||
HLS = "hls"
|
||||
WEB_RTC = "web_rtc"
|
||||
|
||||
|
||||
# These constants are deprecated as of Home Assistant 2022.5
|
||||
# Please use the StreamType enum instead.
|
||||
_DEPRECATED_STREAM_TYPE_HLS = DeprecatedConstantEnum(StreamType.HLS, "2025.1")
|
||||
_DEPRECATED_STREAM_TYPE_WEB_RTC = DeprecatedConstantEnum(StreamType.WEB_RTC, "2025.1")
|
||||
|
||||
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -26,6 +26,11 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||
from homeassistant.helpers.deprecation import (
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
@@ -36,6 +41,20 @@ from homeassistant.util.hass_dict import HassKey
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .const import ( # noqa: F401
|
||||
_DEPRECATED_HVAC_MODE_AUTO,
|
||||
_DEPRECATED_HVAC_MODE_COOL,
|
||||
_DEPRECATED_HVAC_MODE_DRY,
|
||||
_DEPRECATED_HVAC_MODE_FAN_ONLY,
|
||||
_DEPRECATED_HVAC_MODE_HEAT,
|
||||
_DEPRECATED_HVAC_MODE_HEAT_COOL,
|
||||
_DEPRECATED_HVAC_MODE_OFF,
|
||||
_DEPRECATED_SUPPORT_AUX_HEAT,
|
||||
_DEPRECATED_SUPPORT_FAN_MODE,
|
||||
_DEPRECATED_SUPPORT_PRESET_MODE,
|
||||
_DEPRECATED_SUPPORT_SWING_MODE,
|
||||
_DEPRECATED_SUPPORT_TARGET_HUMIDITY,
|
||||
_DEPRECATED_SUPPORT_TARGET_TEMPERATURE,
|
||||
_DEPRECATED_SUPPORT_TARGET_TEMPERATURE_RANGE,
|
||||
ATTR_AUX_HEAT,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
ATTR_CURRENT_TEMPERATURE,
|
||||
@@ -51,8 +70,6 @@ from .const import ( # noqa: F401
|
||||
ATTR_MIN_TEMP,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_PRESET_MODES,
|
||||
ATTR_SWING_HORIZONTAL_MODE,
|
||||
ATTR_SWING_HORIZONTAL_MODES,
|
||||
ATTR_SWING_MODE,
|
||||
ATTR_SWING_MODES,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
@@ -84,7 +101,6 @@ from .const import ( # noqa: F401
|
||||
SERVICE_SET_HUMIDITY,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
SERVICE_SET_SWING_HORIZONTAL_MODE,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
SWING_BOTH,
|
||||
@@ -203,12 +219,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"async_handle_set_swing_mode_service",
|
||||
[ClimateEntityFeature.SWING_MODE],
|
||||
)
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SET_SWING_HORIZONTAL_MODE,
|
||||
{vol.Required(ATTR_SWING_HORIZONTAL_MODE): cv.string},
|
||||
"async_handle_set_swing_horizontal_mode_service",
|
||||
[ClimateEntityFeature.SWING_HORIZONTAL_MODE],
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -246,8 +256,6 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
|
||||
"fan_modes",
|
||||
"swing_mode",
|
||||
"swing_modes",
|
||||
"swing_horizontal_mode",
|
||||
"swing_horizontal_modes",
|
||||
"supported_features",
|
||||
"min_temp",
|
||||
"max_temp",
|
||||
@@ -292,8 +300,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
_attr_supported_features: ClimateEntityFeature = ClimateEntityFeature(0)
|
||||
_attr_swing_mode: str | None
|
||||
_attr_swing_modes: list[str] | None
|
||||
_attr_swing_horizontal_mode: str | None
|
||||
_attr_swing_horizontal_modes: list[str] | None
|
||||
_attr_target_humidity: float | None = None
|
||||
_attr_target_temperature_high: float | None
|
||||
_attr_target_temperature_low: float | None
|
||||
@@ -507,9 +513,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if ClimateEntityFeature.SWING_MODE in supported_features:
|
||||
data[ATTR_SWING_MODES] = self.swing_modes
|
||||
|
||||
if ClimateEntityFeature.SWING_HORIZONTAL_MODE in supported_features:
|
||||
data[ATTR_SWING_HORIZONTAL_MODES] = self.swing_horizontal_modes
|
||||
|
||||
return data
|
||||
|
||||
@final
|
||||
@@ -561,9 +564,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if ClimateEntityFeature.SWING_MODE in supported_features:
|
||||
data[ATTR_SWING_MODE] = self.swing_mode
|
||||
|
||||
if ClimateEntityFeature.SWING_HORIZONTAL_MODE in supported_features:
|
||||
data[ATTR_SWING_HORIZONTAL_MODE] = self.swing_horizontal_mode
|
||||
|
||||
if ClimateEntityFeature.AUX_HEAT in supported_features:
|
||||
data[ATTR_AUX_HEAT] = STATE_ON if self.is_aux_heat else STATE_OFF
|
||||
if (
|
||||
@@ -691,27 +691,11 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""
|
||||
return self._attr_swing_modes
|
||||
|
||||
@cached_property
|
||||
def swing_horizontal_mode(self) -> str | None:
|
||||
"""Return the horizontal swing setting.
|
||||
|
||||
Requires ClimateEntityFeature.SWING_HORIZONTAL_MODE.
|
||||
"""
|
||||
return self._attr_swing_horizontal_mode
|
||||
|
||||
@cached_property
|
||||
def swing_horizontal_modes(self) -> list[str] | None:
|
||||
"""Return the list of available horizontal swing modes.
|
||||
|
||||
Requires ClimateEntityFeature.SWING_HORIZONTAL_MODE.
|
||||
"""
|
||||
return self._attr_swing_horizontal_modes
|
||||
|
||||
@final
|
||||
@callback
|
||||
def _valid_mode_or_raise(
|
||||
self,
|
||||
mode_type: Literal["preset", "horizontal_swing", "swing", "fan", "hvac"],
|
||||
mode_type: Literal["preset", "swing", "fan", "hvac"],
|
||||
mode: str | HVACMode,
|
||||
modes: list[str] | list[HVACMode] | None,
|
||||
) -> None:
|
||||
@@ -809,26 +793,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Set new target swing operation."""
|
||||
await self.hass.async_add_executor_job(self.set_swing_mode, swing_mode)
|
||||
|
||||
@final
|
||||
async def async_handle_set_swing_horizontal_mode_service(
|
||||
self, swing_horizontal_mode: str
|
||||
) -> None:
|
||||
"""Validate and set new horizontal swing mode."""
|
||||
self._valid_mode_or_raise(
|
||||
"horizontal_swing", swing_horizontal_mode, self.swing_horizontal_modes
|
||||
)
|
||||
await self.async_set_swing_horizontal_mode(swing_horizontal_mode)
|
||||
|
||||
def set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
|
||||
"""Set new target horizontal swing operation."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
|
||||
"""Set new target horizontal swing operation."""
|
||||
await self.hass.async_add_executor_job(
|
||||
self.set_swing_horizontal_mode, swing_horizontal_mode
|
||||
)
|
||||
|
||||
@final
|
||||
async def async_handle_set_preset_mode_service(self, preset_mode: str) -> None:
|
||||
"""Validate and set new preset mode."""
|
||||
@@ -1063,3 +1027,13 @@ async def async_service_temperature_set(
|
||||
kwargs[value] = temp
|
||||
|
||||
await entity.async_set_temperature(**kwargs)
|
||||
|
||||
|
||||
# As we import deprecated constants from the const module, we need to add these two functions
|
||||
# otherwise this module will be logged for using deprecated constants and not the custom component
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = ft.partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
"""Provides the constants needed for component."""
|
||||
|
||||
from enum import IntFlag, StrEnum
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstantEnum,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
|
||||
|
||||
class HVACMode(StrEnum):
|
||||
@@ -29,6 +37,15 @@ class HVACMode(StrEnum):
|
||||
FAN_ONLY = "fan_only"
|
||||
|
||||
|
||||
# These HVAC_MODE_* constants are deprecated as of Home Assistant 2022.5.
|
||||
# Please use the HVACMode enum instead.
|
||||
_DEPRECATED_HVAC_MODE_OFF = DeprecatedConstantEnum(HVACMode.OFF, "2025.1")
|
||||
_DEPRECATED_HVAC_MODE_HEAT = DeprecatedConstantEnum(HVACMode.HEAT, "2025.1")
|
||||
_DEPRECATED_HVAC_MODE_COOL = DeprecatedConstantEnum(HVACMode.COOL, "2025.1")
|
||||
_DEPRECATED_HVAC_MODE_HEAT_COOL = DeprecatedConstantEnum(HVACMode.HEAT_COOL, "2025.1")
|
||||
_DEPRECATED_HVAC_MODE_AUTO = DeprecatedConstantEnum(HVACMode.AUTO, "2025.1")
|
||||
_DEPRECATED_HVAC_MODE_DRY = DeprecatedConstantEnum(HVACMode.DRY, "2025.1")
|
||||
_DEPRECATED_HVAC_MODE_FAN_ONLY = DeprecatedConstantEnum(HVACMode.FAN_ONLY, "2025.1")
|
||||
HVAC_MODES = [cls.value for cls in HVACMode]
|
||||
|
||||
# No preset is active
|
||||
@@ -75,10 +92,6 @@ SWING_BOTH = "both"
|
||||
SWING_VERTICAL = "vertical"
|
||||
SWING_HORIZONTAL = "horizontal"
|
||||
|
||||
# Possible horizontal swing state
|
||||
SWING_HORIZONTAL_ON = "on"
|
||||
SWING_HORIZONTAL_OFF = "off"
|
||||
|
||||
|
||||
class HVACAction(StrEnum):
|
||||
"""HVAC action for climate devices."""
|
||||
@@ -93,6 +106,14 @@ class HVACAction(StrEnum):
|
||||
PREHEATING = "preheating"
|
||||
|
||||
|
||||
# These CURRENT_HVAC_* constants are deprecated as of Home Assistant 2022.5.
|
||||
# Please use the HVACAction enum instead.
|
||||
_DEPRECATED_CURRENT_HVAC_OFF = DeprecatedConstantEnum(HVACAction.OFF, "2025.1")
|
||||
_DEPRECATED_CURRENT_HVAC_HEAT = DeprecatedConstantEnum(HVACAction.HEATING, "2025.1")
|
||||
_DEPRECATED_CURRENT_HVAC_COOL = DeprecatedConstantEnum(HVACAction.COOLING, "2025.1")
|
||||
_DEPRECATED_CURRENT_HVAC_DRY = DeprecatedConstantEnum(HVACAction.DRYING, "2025.1")
|
||||
_DEPRECATED_CURRENT_HVAC_IDLE = DeprecatedConstantEnum(HVACAction.IDLE, "2025.1")
|
||||
_DEPRECATED_CURRENT_HVAC_FAN = DeprecatedConstantEnum(HVACAction.FAN, "2025.1")
|
||||
CURRENT_HVAC_ACTIONS = [cls.value for cls in HVACAction]
|
||||
|
||||
|
||||
@@ -113,8 +134,6 @@ ATTR_HVAC_MODES = "hvac_modes"
|
||||
ATTR_HVAC_MODE = "hvac_mode"
|
||||
ATTR_SWING_MODES = "swing_modes"
|
||||
ATTR_SWING_MODE = "swing_mode"
|
||||
ATTR_SWING_HORIZONTAL_MODE = "swing_horizontal_mode"
|
||||
ATTR_SWING_HORIZONTAL_MODES = "swing_horizontal_modes"
|
||||
ATTR_TARGET_TEMP_HIGH = "target_temp_high"
|
||||
ATTR_TARGET_TEMP_LOW = "target_temp_low"
|
||||
ATTR_TARGET_TEMP_STEP = "target_temp_step"
|
||||
@@ -134,7 +153,6 @@ SERVICE_SET_PRESET_MODE = "set_preset_mode"
|
||||
SERVICE_SET_HUMIDITY = "set_humidity"
|
||||
SERVICE_SET_HVAC_MODE = "set_hvac_mode"
|
||||
SERVICE_SET_SWING_MODE = "set_swing_mode"
|
||||
SERVICE_SET_SWING_HORIZONTAL_MODE = "set_swing_horizontal_mode"
|
||||
SERVICE_SET_TEMPERATURE = "set_temperature"
|
||||
|
||||
|
||||
@@ -150,4 +168,35 @@ class ClimateEntityFeature(IntFlag):
|
||||
AUX_HEAT = 64
|
||||
TURN_OFF = 128
|
||||
TURN_ON = 256
|
||||
SWING_HORIZONTAL_MODE = 512
|
||||
|
||||
|
||||
# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5.
|
||||
# Please use the ClimateEntityFeature enum instead.
|
||||
_DEPRECATED_SUPPORT_TARGET_TEMPERATURE = DeprecatedConstantEnum(
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_TARGET_TEMPERATURE_RANGE = DeprecatedConstantEnum(
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_TARGET_HUMIDITY = DeprecatedConstantEnum(
|
||||
ClimateEntityFeature.TARGET_HUMIDITY, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_FAN_MODE = DeprecatedConstantEnum(
|
||||
ClimateEntityFeature.FAN_MODE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_PRESET_MODE = DeprecatedConstantEnum(
|
||||
ClimateEntityFeature.PRESET_MODE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_SWING_MODE = DeprecatedConstantEnum(
|
||||
ClimateEntityFeature.SWING_MODE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_AUX_HEAT = DeprecatedConstantEnum(
|
||||
ClimateEntityFeature.AUX_HEAT, "2025.1"
|
||||
)
|
||||
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -51,13 +51,6 @@
|
||||
"on": "mdi:arrow-oscillating",
|
||||
"vertical": "mdi:arrow-up-down"
|
||||
}
|
||||
},
|
||||
"swing_horizontal_mode": {
|
||||
"default": "mdi:circle-medium",
|
||||
"state": {
|
||||
"off": "mdi:arrow-oscillating-off",
|
||||
"on": "mdi:arrow-expand-horizontal"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,9 +65,6 @@
|
||||
"set_swing_mode": {
|
||||
"service": "mdi:arrow-oscillating"
|
||||
},
|
||||
"set_swing_horizontal_mode": {
|
||||
"service": "mdi:arrow-expand-horizontal"
|
||||
},
|
||||
"set_temperature": {
|
||||
"service": "mdi:thermometer"
|
||||
},
|
||||
|
||||
@@ -14,7 +14,6 @@ from .const import (
|
||||
ATTR_HUMIDITY,
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_SWING_HORIZONTAL_MODE,
|
||||
ATTR_SWING_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
@@ -24,7 +23,6 @@ from .const import (
|
||||
SERVICE_SET_HUMIDITY,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
SERVICE_SET_SWING_HORIZONTAL_MODE,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
)
|
||||
@@ -78,14 +76,6 @@ async def _async_reproduce_states(
|
||||
):
|
||||
await call_service(SERVICE_SET_SWING_MODE, [ATTR_SWING_MODE])
|
||||
|
||||
if (
|
||||
ATTR_SWING_HORIZONTAL_MODE in state.attributes
|
||||
and state.attributes[ATTR_SWING_HORIZONTAL_MODE] is not None
|
||||
):
|
||||
await call_service(
|
||||
SERVICE_SET_SWING_HORIZONTAL_MODE, [ATTR_SWING_HORIZONTAL_MODE]
|
||||
)
|
||||
|
||||
if (
|
||||
ATTR_FAN_MODE in state.attributes
|
||||
and state.attributes[ATTR_FAN_MODE] is not None
|
||||
|
||||
@@ -131,20 +131,7 @@ set_swing_mode:
|
||||
fields:
|
||||
swing_mode:
|
||||
required: true
|
||||
example: "on"
|
||||
selector:
|
||||
text:
|
||||
|
||||
set_swing_horizontal_mode:
|
||||
target:
|
||||
entity:
|
||||
domain: climate
|
||||
supported_features:
|
||||
- climate.ClimateEntityFeature.SWING_HORIZONTAL_MODE
|
||||
fields:
|
||||
swing_horizontal_mode:
|
||||
required: true
|
||||
example: "on"
|
||||
example: "horizontal"
|
||||
selector:
|
||||
text:
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ from . import (
|
||||
ATTR_HUMIDITY,
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_SWING_HORIZONTAL_MODE,
|
||||
ATTR_SWING_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
@@ -35,7 +34,6 @@ SIGNIFICANT_ATTRIBUTES: set[str] = {
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_SWING_MODE,
|
||||
ATTR_SWING_HORIZONTAL_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE,
|
||||
@@ -72,7 +70,6 @@ def async_check_significant_change(
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_SWING_MODE,
|
||||
ATTR_SWING_HORIZONTAL_MODE,
|
||||
]:
|
||||
return True
|
||||
|
||||
|
||||
@@ -123,16 +123,6 @@
|
||||
"swing_modes": {
|
||||
"name": "Swing modes"
|
||||
},
|
||||
"swing_horizontal_mode": {
|
||||
"name": "Horizontal swing mode",
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]",
|
||||
"on": "[%key:common::state::on%]"
|
||||
}
|
||||
},
|
||||
"swing_horizontal_modes": {
|
||||
"name": "Horizontal swing modes"
|
||||
},
|
||||
"target_temp_high": {
|
||||
"name": "Upper target temperature"
|
||||
},
|
||||
@@ -231,16 +221,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_swing_horizontal_mode": {
|
||||
"name": "Set horizontal swing mode",
|
||||
"description": "Sets horizontal swing operation mode.",
|
||||
"fields": {
|
||||
"swing_horizontal_mode": {
|
||||
"name": "Horizontal swing mode",
|
||||
"description": "Horizontal swing operation mode."
|
||||
}
|
||||
}
|
||||
},
|
||||
"turn_on": {
|
||||
"name": "[%key:common::action::turn_on%]",
|
||||
"description": "Turns climate device on."
|
||||
@@ -284,9 +264,6 @@
|
||||
"not_valid_swing_mode": {
|
||||
"message": "Swing mode {mode} is not valid. Valid swing modes are: {modes}."
|
||||
},
|
||||
"not_valid_horizontal_swing_mode": {
|
||||
"message": "Horizontal swing mode {mode} is not valid. Valid horizontal swing modes are: {modes}."
|
||||
},
|
||||
"not_valid_fan_mode": {
|
||||
"message": "Fan mode {mode} is not valid. Valid fan modes are: {modes}."
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.0.4", "home-assistant-intents==2024.11.27"]
|
||||
"requirements": ["hassil==2.0.4", "home-assistant-intents==2024.11.13"]
|
||||
}
|
||||
|
||||
@@ -89,8 +89,36 @@ class CoverDeviceClass(StrEnum):
|
||||
|
||||
|
||||
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(CoverDeviceClass))
|
||||
DEVICE_CLASSES = [cls.value for cls in CoverDeviceClass]
|
||||
|
||||
# DEVICE_CLASS* below are deprecated as of 2021.12
|
||||
# use the CoverDeviceClass enum instead.
|
||||
DEVICE_CLASSES = [cls.value for cls in CoverDeviceClass]
|
||||
_DEPRECATED_DEVICE_CLASS_AWNING = DeprecatedConstantEnum(
|
||||
CoverDeviceClass.AWNING, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_BLIND = DeprecatedConstantEnum(
|
||||
CoverDeviceClass.BLIND, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_CURTAIN = DeprecatedConstantEnum(
|
||||
CoverDeviceClass.CURTAIN, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_DAMPER = DeprecatedConstantEnum(
|
||||
CoverDeviceClass.DAMPER, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_DOOR = DeprecatedConstantEnum(CoverDeviceClass.DOOR, "2025.1")
|
||||
_DEPRECATED_DEVICE_CLASS_GARAGE = DeprecatedConstantEnum(
|
||||
CoverDeviceClass.GARAGE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_GATE = DeprecatedConstantEnum(CoverDeviceClass.GATE, "2025.1")
|
||||
_DEPRECATED_DEVICE_CLASS_SHADE = DeprecatedConstantEnum(
|
||||
CoverDeviceClass.SHADE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_SHUTTER = DeprecatedConstantEnum(
|
||||
CoverDeviceClass.SHUTTER, "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_WINDOW = DeprecatedConstantEnum(
|
||||
CoverDeviceClass.WINDOW, "2025.1"
|
||||
)
|
||||
|
||||
# mypy: disallow-any-generics
|
||||
|
||||
@@ -108,6 +136,27 @@ class CoverEntityFeature(IntFlag):
|
||||
SET_TILT_POSITION = 128
|
||||
|
||||
|
||||
# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5.
|
||||
# Please use the CoverEntityFeature enum instead.
|
||||
_DEPRECATED_SUPPORT_OPEN = DeprecatedConstantEnum(CoverEntityFeature.OPEN, "2025.1")
|
||||
_DEPRECATED_SUPPORT_CLOSE = DeprecatedConstantEnum(CoverEntityFeature.CLOSE, "2025.1")
|
||||
_DEPRECATED_SUPPORT_SET_POSITION = DeprecatedConstantEnum(
|
||||
CoverEntityFeature.SET_POSITION, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_STOP = DeprecatedConstantEnum(CoverEntityFeature.STOP, "2025.1")
|
||||
_DEPRECATED_SUPPORT_OPEN_TILT = DeprecatedConstantEnum(
|
||||
CoverEntityFeature.OPEN_TILT, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_CLOSE_TILT = DeprecatedConstantEnum(
|
||||
CoverEntityFeature.CLOSE_TILT, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_STOP_TILT = DeprecatedConstantEnum(
|
||||
CoverEntityFeature.STOP_TILT, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_SET_TILT_POSITION = DeprecatedConstantEnum(
|
||||
CoverEntityFeature.SET_TILT_POSITION, "2025.1"
|
||||
)
|
||||
|
||||
ATTR_CURRENT_POSITION = "current_position"
|
||||
ATTR_CURRENT_TILT_POSITION = "current_tilt_position"
|
||||
ATTR_POSITION = "position"
|
||||
|
||||
@@ -43,7 +43,6 @@ async def async_setup_entry(
|
||||
target_humidity=None,
|
||||
current_humidity=None,
|
||||
swing_mode=None,
|
||||
swing_horizontal_mode=None,
|
||||
hvac_mode=HVACMode.HEAT,
|
||||
hvac_action=HVACAction.HEATING,
|
||||
target_temp_high=None,
|
||||
@@ -61,7 +60,6 @@ async def async_setup_entry(
|
||||
target_humidity=67.4,
|
||||
current_humidity=54.2,
|
||||
swing_mode="off",
|
||||
swing_horizontal_mode="auto",
|
||||
hvac_mode=HVACMode.COOL,
|
||||
hvac_action=HVACAction.COOLING,
|
||||
target_temp_high=None,
|
||||
@@ -80,7 +78,6 @@ async def async_setup_entry(
|
||||
target_humidity=None,
|
||||
current_humidity=None,
|
||||
swing_mode="auto",
|
||||
swing_horizontal_mode=None,
|
||||
hvac_mode=HVACMode.HEAT_COOL,
|
||||
hvac_action=None,
|
||||
target_temp_high=24,
|
||||
@@ -112,7 +109,6 @@ class DemoClimate(ClimateEntity):
|
||||
target_humidity: float | None,
|
||||
current_humidity: float | None,
|
||||
swing_mode: str | None,
|
||||
swing_horizontal_mode: str | None,
|
||||
hvac_mode: HVACMode,
|
||||
hvac_action: HVACAction | None,
|
||||
target_temp_high: float | None,
|
||||
@@ -133,8 +129,6 @@ class DemoClimate(ClimateEntity):
|
||||
self._attr_supported_features |= ClimateEntityFeature.TARGET_HUMIDITY
|
||||
if swing_mode is not None:
|
||||
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
|
||||
if swing_horizontal_mode is not None:
|
||||
self._attr_supported_features |= ClimateEntityFeature.SWING_HORIZONTAL_MODE
|
||||
if HVACMode.HEAT_COOL in hvac_modes or HVACMode.AUTO in hvac_modes:
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
@@ -153,11 +147,9 @@ class DemoClimate(ClimateEntity):
|
||||
self._hvac_action = hvac_action
|
||||
self._hvac_mode = hvac_mode
|
||||
self._current_swing_mode = swing_mode
|
||||
self._current_swing_horizontal_mode = swing_horizontal_mode
|
||||
self._fan_modes = ["on_low", "on_high", "auto_low", "auto_high", "off"]
|
||||
self._hvac_modes = hvac_modes
|
||||
self._swing_modes = ["auto", "1", "2", "3", "off"]
|
||||
self._swing_horizontal_modes = ["auto", "rangefull", "off"]
|
||||
self._target_temperature_high = target_temp_high
|
||||
self._target_temperature_low = target_temp_low
|
||||
self._attr_device_info = DeviceInfo(
|
||||
@@ -250,16 +242,6 @@ class DemoClimate(ClimateEntity):
|
||||
"""List of available swing modes."""
|
||||
return self._swing_modes
|
||||
|
||||
@property
|
||||
def swing_horizontal_mode(self) -> str | None:
|
||||
"""Return the swing setting."""
|
||||
return self._current_swing_horizontal_mode
|
||||
|
||||
@property
|
||||
def swing_horizontal_modes(self) -> list[str]:
|
||||
"""List of available swing modes."""
|
||||
return self._swing_horizontal_modes
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperatures."""
|
||||
if kwargs.get(ATTR_TEMPERATURE) is not None:
|
||||
@@ -284,11 +266,6 @@ class DemoClimate(ClimateEntity):
|
||||
self._current_swing_mode = swing_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None:
|
||||
"""Set new swing mode."""
|
||||
self._current_swing_horizontal_mode = swing_horizontal_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new fan mode."""
|
||||
self._current_fan_mode = fan_mode
|
||||
|
||||
@@ -19,13 +19,6 @@
|
||||
"auto": "mdi:arrow-oscillating",
|
||||
"off": "mdi:arrow-oscillating-off"
|
||||
}
|
||||
},
|
||||
"swing_horizontal_mode": {
|
||||
"state": {
|
||||
"rangefull": "mdi:pan-horizontal",
|
||||
"auto": "mdi:compare-horizontal",
|
||||
"off": "mdi:arrow-oscillating-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,13 +42,6 @@
|
||||
"auto": "Auto",
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"swing_horizontal_mode": {
|
||||
"state": {
|
||||
"rangefull": "Full range",
|
||||
"auto": "Auto",
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.deprecation import (
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
@@ -16,6 +23,10 @@ from .config_entry import ( # noqa: F401
|
||||
async_unload_entry,
|
||||
)
|
||||
from .const import ( # noqa: F401
|
||||
_DEPRECATED_SOURCE_TYPE_BLUETOOTH,
|
||||
_DEPRECATED_SOURCE_TYPE_BLUETOOTH_LE,
|
||||
_DEPRECATED_SOURCE_TYPE_GPS,
|
||||
_DEPRECATED_SOURCE_TYPE_ROUTER,
|
||||
ATTR_ATTRIBUTES,
|
||||
ATTR_BATTERY,
|
||||
ATTR_DEV_ID,
|
||||
@@ -61,3 +72,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the device tracker."""
|
||||
async_setup_legacy_integration(hass, config)
|
||||
return True
|
||||
|
||||
|
||||
# As we import deprecated constants from the const module, we need to add these two functions
|
||||
# otherwise this module will be logged for using deprecated constants and not the custom component
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -4,9 +4,16 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import StrEnum
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstantEnum,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.util.signal_type import SignalType
|
||||
|
||||
LOGGER: Final = logging.getLogger(__package__)
|
||||
@@ -27,6 +34,19 @@ class SourceType(StrEnum):
|
||||
BLUETOOTH_LE = "bluetooth_le"
|
||||
|
||||
|
||||
# SOURCE_TYPE_* below are deprecated as of 2022.9
|
||||
# use the SourceType enum instead.
|
||||
_DEPRECATED_SOURCE_TYPE_GPS: Final = DeprecatedConstantEnum(SourceType.GPS, "2025.1")
|
||||
_DEPRECATED_SOURCE_TYPE_ROUTER: Final = DeprecatedConstantEnum(
|
||||
SourceType.ROUTER, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SOURCE_TYPE_BLUETOOTH: Final = DeprecatedConstantEnum(
|
||||
SourceType.BLUETOOTH, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SOURCE_TYPE_BLUETOOTH_LE: Final = DeprecatedConstantEnum(
|
||||
SourceType.BLUETOOTH_LE, "2025.1"
|
||||
)
|
||||
|
||||
CONF_SCAN_INTERVAL: Final = "interval_seconds"
|
||||
SCAN_INTERVAL: Final = timedelta(seconds=12)
|
||||
|
||||
@@ -52,3 +72,10 @@ ATTR_IP: Final = "ip"
|
||||
CONNECTED_DEVICE_REGISTERED = SignalType[dict[str, str | None]](
|
||||
"device_tracker_connected_device_registered"
|
||||
)
|
||||
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -6,17 +6,11 @@
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]",
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]"
|
||||
},
|
||||
"data_description": {
|
||||
"ip_address": "IP address of your devolo Home Network device. This can be found in the devolo Home Network App on the device dashboard."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "Password you protected the device with."
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
|
||||
@@ -60,11 +60,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) -
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) -> None:
|
||||
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle an options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
@@ -11,7 +11,12 @@ from pydiscovergy.authentication import BasicAuth
|
||||
import pydiscovergy.error as discovergyError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.selector import (
|
||||
@@ -52,14 +57,35 @@ class DiscovergyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_existing_entry: ConfigEntry
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=CONFIG_SCHEMA,
|
||||
)
|
||||
|
||||
return await self._validate_and_save(user_input)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
return await self.async_step_user()
|
||||
self._existing_entry = self._get_reauth_entry()
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Mapping[str, Any] | None = None
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the reauth step."""
|
||||
return await self._validate_and_save(user_input, step_id="reauth_confirm")
|
||||
|
||||
async def _validate_and_save(
|
||||
self, user_input: Mapping[str, Any] | None = None, step_id: str = "user"
|
||||
) -> ConfigFlowResult:
|
||||
"""Validate user input and create config entry."""
|
||||
errors = {}
|
||||
@@ -80,17 +106,17 @@ class DiscovergyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected error occurred while getting meters")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_EMAIL].lower())
|
||||
|
||||
if self.source == SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_mismatch(reason="account_mismatch")
|
||||
return self.async_update_reload_and_abort(
|
||||
entry=self._get_reauth_entry(),
|
||||
data_updates={
|
||||
entry=self._existing_entry,
|
||||
data={
|
||||
CONF_EMAIL: user_input[CONF_EMAIL],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
|
||||
# set unique id to title which is the account email
|
||||
await self.async_set_unique_id(user_input[CONF_EMAIL].lower())
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
@@ -98,10 +124,10 @@ class DiscovergyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
step_id=step_id,
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
CONFIG_SCHEMA,
|
||||
self._get_reauth_entry().data
|
||||
self._existing_entry.data
|
||||
if self.source == SOURCE_REAUTH
|
||||
else user_input,
|
||||
),
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -15,7 +21,6 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"account_mismatch": "The inexogy account authenticated with, does not match the account needed re-authentication.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -10,31 +10,16 @@ from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONF_API_KEY,
|
||||
CONF_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_URL,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
PERCENTAGE,
|
||||
UnitOfApparentPower,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfFrequency,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@@ -56,146 +41,6 @@ from .const import (
|
||||
)
|
||||
from .coordinator import EmoncmsCoordinator
|
||||
|
||||
SENSORS: dict[str | None, SensorEntityDescription] = {
|
||||
"kWh": SensorEntityDescription(
|
||||
key="energy|kWh",
|
||||
translation_key="energy",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
"Wh": SensorEntityDescription(
|
||||
key="energy|Wh",
|
||||
translation_key="energy",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
"kW": SensorEntityDescription(
|
||||
key="power|kW",
|
||||
translation_key="power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"W": SensorEntityDescription(
|
||||
key="power|W",
|
||||
translation_key="power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"V": SensorEntityDescription(
|
||||
key="voltage",
|
||||
translation_key="voltage",
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"A": SensorEntityDescription(
|
||||
key="current",
|
||||
translation_key="current",
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"VA": SensorEntityDescription(
|
||||
key="apparent_power",
|
||||
translation_key="apparent_power",
|
||||
device_class=SensorDeviceClass.APPARENT_POWER,
|
||||
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"°C": SensorEntityDescription(
|
||||
key="temperature|celsius",
|
||||
translation_key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"°F": SensorEntityDescription(
|
||||
key="temperature|fahrenheit",
|
||||
translation_key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"K": SensorEntityDescription(
|
||||
key="temperature|kelvin",
|
||||
translation_key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.KELVIN,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"Hz": SensorEntityDescription(
|
||||
key="frequency",
|
||||
translation_key="frequency",
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"hPa": SensorEntityDescription(
|
||||
key="pressure",
|
||||
translation_key="pressure",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"dB": SensorEntityDescription(
|
||||
key="decibel",
|
||||
translation_key="decibel",
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"m³": SensorEntityDescription(
|
||||
key="volume|cubic_meter",
|
||||
translation_key="volume",
|
||||
device_class=SensorDeviceClass.VOLUME_STORAGE,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"m³/h": SensorEntityDescription(
|
||||
key="flow|cubic_meters_per_hour",
|
||||
translation_key="flow",
|
||||
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"l/m": SensorEntityDescription(
|
||||
key="flow|liters_per_minute",
|
||||
translation_key="flow",
|
||||
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"m/s": SensorEntityDescription(
|
||||
key="speed|meters_per_second",
|
||||
translation_key="speed",
|
||||
device_class=SensorDeviceClass.SPEED,
|
||||
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"µg/m³": SensorEntityDescription(
|
||||
key="concentration|microgram_per_cubic_meter",
|
||||
translation_key="concentration",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"ppm": SensorEntityDescription(
|
||||
key="concentration|microgram_parts_per_million",
|
||||
translation_key="concentration",
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"%": SensorEntityDescription(
|
||||
key="percent",
|
||||
translation_key="percent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
}
|
||||
|
||||
ATTR_FEEDID = "FeedId"
|
||||
ATTR_FEEDNAME = "FeedName"
|
||||
ATTR_LASTUPDATETIME = "LastUpdated"
|
||||
@@ -328,8 +173,6 @@ async def async_setup_entry(
|
||||
class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
|
||||
"""Implementation of an Emoncms sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EmoncmsCoordinator,
|
||||
@@ -344,15 +187,33 @@ class EmonCmsSensor(CoordinatorEntity[EmoncmsCoordinator], SensorEntity):
|
||||
elem = {}
|
||||
if self.coordinator.data:
|
||||
elem = self.coordinator.data[self.idx]
|
||||
self._attr_translation_placeholders = {
|
||||
"emoncms_details": f"{elem[FEED_TAG]} {elem[FEED_NAME]}",
|
||||
}
|
||||
self._attr_name = f"{name} {elem[FEED_NAME]}"
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
self._attr_unique_id = f"{unique_id}-{elem[FEED_ID]}"
|
||||
description = SENSORS.get(unit_of_measurement)
|
||||
if description is not None:
|
||||
self.entity_description = description
|
||||
else:
|
||||
self._attr_native_unit_of_measurement = unit_of_measurement
|
||||
if unit_of_measurement in ("kWh", "Wh"):
|
||||
self._attr_device_class = SensorDeviceClass.ENERGY
|
||||
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
elif unit_of_measurement == "W":
|
||||
self._attr_device_class = SensorDeviceClass.POWER
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
elif unit_of_measurement == "V":
|
||||
self._attr_device_class = SensorDeviceClass.VOLTAGE
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
elif unit_of_measurement == "A":
|
||||
self._attr_device_class = SensorDeviceClass.CURRENT
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
elif unit_of_measurement == "VA":
|
||||
self._attr_device_class = SensorDeviceClass.APPARENT_POWER
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
elif unit_of_measurement in ("°C", "°F", "K"):
|
||||
self._attr_device_class = SensorDeviceClass.TEMPERATURE
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
elif unit_of_measurement == "Hz":
|
||||
self._attr_device_class = SensorDeviceClass.FREQUENCY
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
elif unit_of_measurement == "hPa":
|
||||
self._attr_device_class = SensorDeviceClass.PRESSURE
|
||||
self._attr_state_class = SensorStateClass.MEASUREMENT
|
||||
self._update_attributes(elem)
|
||||
|
||||
def _update_attributes(self, elem: dict[str, Any]) -> None:
|
||||
|
||||
@@ -24,52 +24,6 @@
|
||||
"already_configured": "This server is already configured"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"energy": {
|
||||
"name": "Energy {emoncms_details}"
|
||||
},
|
||||
"power": {
|
||||
"name": "Power {emoncms_details}"
|
||||
},
|
||||
"percent": {
|
||||
"name": "Percentage {emoncms_details}"
|
||||
},
|
||||
"voltage": {
|
||||
"name": "Voltage {emoncms_details}"
|
||||
},
|
||||
"current": {
|
||||
"name": "Current {emoncms_details}"
|
||||
},
|
||||
"apparent_power": {
|
||||
"name": "Apparent power {emoncms_details}"
|
||||
},
|
||||
"temperature": {
|
||||
"name": "Temperature {emoncms_details}"
|
||||
},
|
||||
"frequency": {
|
||||
"name": "Frequency {emoncms_details}"
|
||||
},
|
||||
"pressure": {
|
||||
"name": "Pressure {emoncms_details}"
|
||||
},
|
||||
"decibel": {
|
||||
"name": "Decibel {emoncms_details}"
|
||||
},
|
||||
"volume": {
|
||||
"name": "Volume {emoncms_details}"
|
||||
},
|
||||
"flow": {
|
||||
"name": "Flow rate {emoncms_details}"
|
||||
},
|
||||
"speed": {
|
||||
"name": "Speed {emoncms_details}"
|
||||
},
|
||||
"concentration": {
|
||||
"name": "Concentration {emoncms_details}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"error": {
|
||||
"api_error": "[%key:component::emoncms::config::error::api_error%]"
|
||||
|
||||
@@ -95,7 +95,11 @@ async def async_setup_entry(
|
||||
if entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
entry_data.api_version
|
||||
):
|
||||
async_add_entities([EsphomeAssistSatellite(entry, entry_data)])
|
||||
async_add_entities(
|
||||
[
|
||||
EsphomeAssistSatellite(entry, entry_data),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class EsphomeAssistSatellite(
|
||||
@@ -194,9 +198,6 @@ class EsphomeAssistSatellite(
|
||||
self._satellite_config.max_active_wake_words = config.max_active_wake_words
|
||||
_LOGGER.debug("Received satellite configuration: %s", self._satellite_config)
|
||||
|
||||
# Inform listeners that config has been updated
|
||||
self.entry_data.async_assist_satellite_config_updated(self._satellite_config)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
@@ -253,13 +254,6 @@ class EsphomeAssistSatellite(
|
||||
# Will use media player for TTS/announcements
|
||||
self._update_tts_format()
|
||||
|
||||
# Update wake word select when config is updated
|
||||
self.async_on_remove(
|
||||
self.entry_data.async_register_assist_satellite_set_wake_word_callback(
|
||||
self.async_set_wake_word
|
||||
)
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Run when entity will be removed from hass."""
|
||||
await super().async_will_remove_from_hass()
|
||||
@@ -484,17 +478,6 @@ class EsphomeAssistSatellite(
|
||||
"""Handle announcement finished message (also sent for TTS)."""
|
||||
self.tts_response_finished()
|
||||
|
||||
@callback
|
||||
def async_set_wake_word(self, wake_word_id: str) -> None:
|
||||
"""Set active wake word and update config on satellite."""
|
||||
self._satellite_config.active_wake_words = [wake_word_id]
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self.async_set_configuration(self._satellite_config),
|
||||
"esphome_voice_assistant_set_config",
|
||||
)
|
||||
_LOGGER.debug("Setting active wake word: %s", wake_word_id)
|
||||
|
||||
def _update_tts_format(self) -> None:
|
||||
"""Update the TTS format from the first media player."""
|
||||
for supported_format in chain(*self.entry_data.media_player_formats.values()):
|
||||
|
||||
@@ -48,7 +48,6 @@ from aioesphomeapi import (
|
||||
from aioesphomeapi.model import ButtonInfo
|
||||
from bleak_esphome.backend.device import ESPHomeBluetoothDevice
|
||||
|
||||
from homeassistant.components.assist_satellite import AssistSatelliteConfiguration
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
@@ -153,12 +152,6 @@ class RuntimeEntryData:
|
||||
media_player_formats: dict[str, list[MediaPlayerSupportedFormat]] = field(
|
||||
default_factory=lambda: defaultdict(list)
|
||||
)
|
||||
assist_satellite_config_update_callbacks: list[
|
||||
Callable[[AssistSatelliteConfiguration], None]
|
||||
] = field(default_factory=list)
|
||||
assist_satellite_set_wake_word_callbacks: list[Callable[[str], None]] = field(
|
||||
default_factory=list
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
@@ -511,35 +504,3 @@ class RuntimeEntryData:
|
||||
# We use this to determine if a deep sleep device should
|
||||
# be marked as unavailable or not.
|
||||
self.expected_disconnect = True
|
||||
|
||||
@callback
|
||||
def async_register_assist_satellite_config_updated_callback(
|
||||
self,
|
||||
callback_: Callable[[AssistSatelliteConfiguration], None],
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Register to receive callbacks when the Assist satellite's configuration is updated."""
|
||||
self.assist_satellite_config_update_callbacks.append(callback_)
|
||||
return lambda: self.assist_satellite_config_update_callbacks.remove(callback_)
|
||||
|
||||
@callback
|
||||
def async_assist_satellite_config_updated(
|
||||
self, config: AssistSatelliteConfiguration
|
||||
) -> None:
|
||||
"""Notify listeners that the Assist satellite configuration has been updated."""
|
||||
for callback_ in self.assist_satellite_config_update_callbacks.copy():
|
||||
callback_(config)
|
||||
|
||||
@callback
|
||||
def async_register_assist_satellite_set_wake_word_callback(
|
||||
self,
|
||||
callback_: Callable[[str], None],
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Register to receive callbacks when the Assist satellite's wake word is set."""
|
||||
self.assist_satellite_set_wake_word_callbacks.append(callback_)
|
||||
return lambda: self.assist_satellite_set_wake_word_callbacks.remove(callback_)
|
||||
|
||||
@callback
|
||||
def async_assist_satellite_set_wake_word(self, wake_word_id: str) -> None:
|
||||
"""Notify listeners that the Assist satellite wake word has been set."""
|
||||
for callback_ in self.assist_satellite_set_wake_word_callbacks.copy():
|
||||
callback_(wake_word_id)
|
||||
|
||||
@@ -212,10 +212,6 @@ class FFmpegConvertResponse(web.StreamResponse):
|
||||
assert proc.stdout is not None
|
||||
assert proc.stderr is not None
|
||||
|
||||
stderr_task = self.hass.async_create_background_task(
|
||||
self._dump_ffmpeg_stderr(proc), "ESPHome media proxy dump stderr"
|
||||
)
|
||||
|
||||
try:
|
||||
# Pull audio chunks from ffmpeg and pass them to the HTTP client
|
||||
while (
|
||||
@@ -234,14 +230,18 @@ class FFmpegConvertResponse(web.StreamResponse):
|
||||
raise # don't log error
|
||||
except:
|
||||
_LOGGER.exception("Unexpected error during ffmpeg conversion")
|
||||
|
||||
# Process did not exit successfully
|
||||
stderr_text = ""
|
||||
while line := await proc.stderr.readline():
|
||||
stderr_text += line.decode()
|
||||
_LOGGER.error("FFmpeg output: %s", stderr_text)
|
||||
|
||||
raise
|
||||
finally:
|
||||
# Allow conversion info to be removed
|
||||
self.convert_info.is_finished = True
|
||||
|
||||
# stop dumping ffmpeg stderr task
|
||||
stderr_task.cancel()
|
||||
|
||||
# Terminate hangs, so kill is used
|
||||
if proc.returncode is None:
|
||||
proc.kill()
|
||||
@@ -250,16 +250,6 @@ class FFmpegConvertResponse(web.StreamResponse):
|
||||
if request.transport and not request.transport.is_closing():
|
||||
await writer.write_eof()
|
||||
|
||||
async def _dump_ffmpeg_stderr(
|
||||
self,
|
||||
proc: asyncio.subprocess.Process,
|
||||
) -> None:
|
||||
assert proc.stdout is not None
|
||||
assert proc.stderr is not None
|
||||
|
||||
while self.hass.is_running and (chunk := await proc.stderr.readline()):
|
||||
_LOGGER.debug("ffmpeg[%s] output: %s", proc.pid, chunk.decode().rstrip())
|
||||
|
||||
|
||||
class FFmpegProxyView(HomeAssistantView):
|
||||
"""FFmpeg web view to convert audio and stream back to client."""
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==27.0.3",
|
||||
"aioesphomeapi==27.0.2",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==1.1.0"
|
||||
],
|
||||
|
||||
@@ -8,11 +8,8 @@ from homeassistant.components.assist_pipeline.select import (
|
||||
AssistPipelineSelect,
|
||||
VadSensitivitySelect,
|
||||
)
|
||||
from homeassistant.components.assist_satellite import AssistSatelliteConfiguration
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import restore_state
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -50,7 +47,6 @@ async def async_setup_entry(
|
||||
[
|
||||
EsphomeAssistPipelineSelect(hass, entry_data),
|
||||
EsphomeVadSensitivitySelect(hass, entry_data),
|
||||
EsphomeAssistSatelliteWakeWordSelect(hass, entry_data),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -93,77 +89,3 @@ class EsphomeVadSensitivitySelect(EsphomeAssistEntity, VadSensitivitySelect):
|
||||
"""Initialize a VAD sensitivity selector."""
|
||||
EsphomeAssistEntity.__init__(self, entry_data)
|
||||
VadSensitivitySelect.__init__(self, hass, self._device_info.mac_address)
|
||||
|
||||
|
||||
class EsphomeAssistSatelliteWakeWordSelect(
|
||||
EsphomeAssistEntity, SelectEntity, restore_state.RestoreEntity
|
||||
):
|
||||
"""Wake word selector for esphome devices."""
|
||||
|
||||
entity_description = SelectEntityDescription(
|
||||
key="wake_word",
|
||||
translation_key="wake_word",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
_attr_should_poll = False
|
||||
_attr_current_option: str | None = None
|
||||
_attr_options: list[str] = []
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry_data: RuntimeEntryData) -> None:
|
||||
"""Initialize a wake word selector."""
|
||||
EsphomeAssistEntity.__init__(self, entry_data)
|
||||
|
||||
unique_id_prefix = self._device_info.mac_address
|
||||
self._attr_unique_id = f"{unique_id_prefix}-wake_word"
|
||||
|
||||
# name -> id
|
||||
self._wake_words: dict[str, str] = {}
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return bool(self._attr_options)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
# Update options when config is updated
|
||||
self.async_on_remove(
|
||||
self._entry_data.async_register_assist_satellite_config_updated_callback(
|
||||
self.async_satellite_config_updated
|
||||
)
|
||||
)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select an option."""
|
||||
if wake_word_id := self._wake_words.get(option):
|
||||
# _attr_current_option will be updated on
|
||||
# async_satellite_config_updated after the device sets the wake
|
||||
# word.
|
||||
self._entry_data.async_assist_satellite_set_wake_word(wake_word_id)
|
||||
|
||||
def async_satellite_config_updated(
|
||||
self, config: AssistSatelliteConfiguration
|
||||
) -> None:
|
||||
"""Update options with available wake words."""
|
||||
if (not config.available_wake_words) or (config.max_active_wake_words < 1):
|
||||
self._attr_current_option = None
|
||||
self._wake_words.clear()
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._wake_words = {w.wake_word: w.id for w in config.available_wake_words}
|
||||
self._attr_options = sorted(self._wake_words)
|
||||
|
||||
if config.active_wake_words:
|
||||
# Select first active wake word
|
||||
wake_word_id = config.active_wake_words[0]
|
||||
for wake_word in config.available_wake_words:
|
||||
if wake_word.id == wake_word_id:
|
||||
self._attr_current_option = wake_word.wake_word
|
||||
else:
|
||||
# Select first available wake word
|
||||
self._attr_current_option = config.available_wake_words[0].wake_word
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -84,12 +84,6 @@
|
||||
"aggressive": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::state::aggressive%]",
|
||||
"relaxed": "[%key:component::assist_pipeline::entity::select::vad_sensitivity::state::relaxed%]"
|
||||
}
|
||||
},
|
||||
"wake_word": {
|
||||
"name": "Wake word",
|
||||
"state": {
|
||||
"okay_nabu": "Okay Nabu"
|
||||
}
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
|
||||
@@ -23,6 +23,12 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstantEnum,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
@@ -55,6 +61,21 @@ class FanEntityFeature(IntFlag):
|
||||
TURN_ON = 32
|
||||
|
||||
|
||||
# These SUPPORT_* constants are deprecated as of Home Assistant 2022.5.
|
||||
# Please use the FanEntityFeature enum instead.
|
||||
_DEPRECATED_SUPPORT_SET_SPEED = DeprecatedConstantEnum(
|
||||
FanEntityFeature.SET_SPEED, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_OSCILLATE = DeprecatedConstantEnum(
|
||||
FanEntityFeature.OSCILLATE, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_DIRECTION = DeprecatedConstantEnum(
|
||||
FanEntityFeature.DIRECTION, "2025.1"
|
||||
)
|
||||
_DEPRECATED_SUPPORT_PRESET_MODE = DeprecatedConstantEnum(
|
||||
FanEntityFeature.PRESET_MODE, "2025.1"
|
||||
)
|
||||
|
||||
SERVICE_INCREASE_SPEED = "increase_speed"
|
||||
SERVICE_DECREASE_SPEED = "decrease_speed"
|
||||
SERVICE_OSCILLATE = "oscillate"
|
||||
@@ -522,3 +543,11 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
Requires FanEntityFeature.SET_SPEED.
|
||||
"""
|
||||
return self._attr_preset_modes
|
||||
|
||||
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = ft.partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = ft.partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20241127.1"]
|
||||
"requirements": ["home-assistant-frontend==20241106.2"]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GaragesAmsterdamDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
@@ -16,23 +17,24 @@ PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
type GaragesAmsterdamConfigEntry = ConfigEntry[GaragesAmsterdamDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: GaragesAmsterdamConfigEntry
|
||||
) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Garages Amsterdam from a config entry."""
|
||||
client = ODPAmsterdam(session=async_get_clientsession(hass))
|
||||
coordinator = GaragesAmsterdamDataUpdateCoordinator(hass, client)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: GaragesAmsterdamConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Garages Amsterdam config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if len(hass.config_entries.async_entries(DOMAIN)) == 1:
|
||||
hass.data.pop(DOMAIN)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -2,77 +2,48 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from odp_amsterdam import Garage
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import GaragesAmsterdamConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GaragesAmsterdamDataUpdateCoordinator
|
||||
from .entity import GaragesAmsterdamEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GaragesAmsterdamBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Class describing Garages Amsterdam binary sensor entity."""
|
||||
|
||||
is_on: Callable[[Garage], bool]
|
||||
|
||||
|
||||
BINARY_SENSORS: tuple[GaragesAmsterdamBinarySensorEntityDescription, ...] = (
|
||||
GaragesAmsterdamBinarySensorEntityDescription(
|
||||
key="state",
|
||||
translation_key="state",
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
is_on=lambda garage: garage.state != "ok",
|
||||
),
|
||||
)
|
||||
BINARY_SENSORS = {
|
||||
"state",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: GaragesAmsterdamConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Defer sensor setup to the shared sensor module."""
|
||||
coordinator = entry.runtime_data
|
||||
coordinator: GaragesAmsterdamDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
|
||||
async_add_entities(
|
||||
GaragesAmsterdamBinarySensor(
|
||||
coordinator=coordinator,
|
||||
garage_name=entry.data["garage_name"],
|
||||
description=description,
|
||||
)
|
||||
for description in BINARY_SENSORS
|
||||
GaragesAmsterdamBinarySensor(coordinator, entry.data["garage_name"], info_type)
|
||||
for info_type in BINARY_SENSORS
|
||||
)
|
||||
|
||||
|
||||
class GaragesAmsterdamBinarySensor(GaragesAmsterdamEntity, BinarySensorEntity):
|
||||
"""Binary Sensor representing garages amsterdam data."""
|
||||
|
||||
entity_description: GaragesAmsterdamBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
coordinator: GaragesAmsterdamDataUpdateCoordinator,
|
||||
garage_name: str,
|
||||
description: GaragesAmsterdamBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize garages amsterdam binary sensor."""
|
||||
super().__init__(coordinator, garage_name)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{garage_name}-{description.key}"
|
||||
_attr_device_class = BinarySensorDeviceClass.PROBLEM
|
||||
_attr_name = None
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""If the binary sensor is currently on or off."""
|
||||
return self.entity_description.is_on(self.coordinator.data[self._garage_name])
|
||||
return (
|
||||
getattr(self.coordinator.data[self._garage_name], self._info_type) != "ok"
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "garages_amsterdam"
|
||||
ATTRIBUTION = "Data provided by municipality of Amsterdam"
|
||||
ATTRIBUTION = f'{"Data provided by municipality of Amsterdam"}'
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
@@ -19,10 +19,13 @@ class GaragesAmsterdamEntity(CoordinatorEntity[GaragesAmsterdamDataUpdateCoordin
|
||||
self,
|
||||
coordinator: GaragesAmsterdamDataUpdateCoordinator,
|
||||
garage_name: str,
|
||||
info_type: str,
|
||||
) -> None:
|
||||
"""Initialize garages amsterdam entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{garage_name}-{info_type}"
|
||||
self._garage_name = garage_name
|
||||
self._info_type = info_type
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, garage_name)},
|
||||
name=garage_name,
|
||||
|
||||
@@ -2,93 +2,54 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from odp_amsterdam import Garage
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import GaragesAmsterdamConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GaragesAmsterdamDataUpdateCoordinator
|
||||
from .entity import GaragesAmsterdamEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GaragesAmsterdamSensorEntityDescription(SensorEntityDescription):
|
||||
"""Class describing Garages Amsterdam sensor entity."""
|
||||
|
||||
value_fn: Callable[[Garage], StateType]
|
||||
|
||||
|
||||
SENSORS: tuple[GaragesAmsterdamSensorEntityDescription, ...] = (
|
||||
GaragesAmsterdamSensorEntityDescription(
|
||||
key="free_space_short",
|
||||
translation_key="free_space_short",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda garage: garage.free_space_short,
|
||||
),
|
||||
GaragesAmsterdamSensorEntityDescription(
|
||||
key="free_space_long",
|
||||
translation_key="free_space_long",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda garage: garage.free_space_long,
|
||||
),
|
||||
GaragesAmsterdamSensorEntityDescription(
|
||||
key="short_capacity",
|
||||
translation_key="short_capacity",
|
||||
value_fn=lambda garage: garage.short_capacity,
|
||||
),
|
||||
GaragesAmsterdamSensorEntityDescription(
|
||||
key="long_capacity",
|
||||
translation_key="long_capacity",
|
||||
value_fn=lambda garage: garage.long_capacity,
|
||||
),
|
||||
)
|
||||
SENSORS = {
|
||||
"free_space_short",
|
||||
"free_space_long",
|
||||
"short_capacity",
|
||||
"long_capacity",
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: GaragesAmsterdamConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Defer sensor setup to the shared sensor module."""
|
||||
coordinator = entry.runtime_data
|
||||
coordinator: GaragesAmsterdamDataUpdateCoordinator = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
|
||||
async_add_entities(
|
||||
GaragesAmsterdamSensor(
|
||||
coordinator=coordinator,
|
||||
garage_name=entry.data["garage_name"],
|
||||
description=description,
|
||||
)
|
||||
for description in SENSORS
|
||||
if description.value_fn(coordinator.data[entry.data["garage_name"]]) is not None
|
||||
GaragesAmsterdamSensor(coordinator, entry.data["garage_name"], info_type)
|
||||
for info_type in SENSORS
|
||||
if getattr(coordinator.data[entry.data["garage_name"]], info_type) != ""
|
||||
)
|
||||
|
||||
|
||||
class GaragesAmsterdamSensor(GaragesAmsterdamEntity, SensorEntity):
|
||||
"""Sensor representing garages amsterdam data."""
|
||||
|
||||
entity_description: GaragesAmsterdamSensorEntityDescription
|
||||
_attr_native_unit_of_measurement = "cars"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
coordinator: GaragesAmsterdamDataUpdateCoordinator,
|
||||
garage_name: str,
|
||||
description: GaragesAmsterdamSensorEntityDescription,
|
||||
info_type: str,
|
||||
) -> None:
|
||||
"""Initialize garages amsterdam sensor."""
|
||||
super().__init__(coordinator, garage_name)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{garage_name}-{description.key}"
|
||||
super().__init__(coordinator, garage_name, info_type)
|
||||
self._attr_translation_key = info_type
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@@ -98,8 +59,6 @@ class GaragesAmsterdamSensor(GaragesAmsterdamEntity, SensorEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
def native_value(self) -> str:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data[self._garage_name]
|
||||
)
|
||||
return getattr(self.coordinator.data[self._garage_name], self._info_type)
|
||||
|
||||
@@ -3,13 +3,8 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Select a garage from the list",
|
||||
"data": {
|
||||
"garage_name": "Garage name"
|
||||
},
|
||||
"data_description": {
|
||||
"garage_name": "The name of the garage you want to monitor."
|
||||
}
|
||||
"title": "Pick a garage to monitor",
|
||||
"data": { "garage_name": "Garage name" }
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
@@ -21,25 +16,16 @@
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"free_space_short": {
|
||||
"name": "Short parking free space",
|
||||
"unit_of_measurement": "cars"
|
||||
"name": "Short parking free space"
|
||||
},
|
||||
"free_space_long": {
|
||||
"name": "Long parking free space",
|
||||
"unit_of_measurement": "cars"
|
||||
"name": "Long parking free space"
|
||||
},
|
||||
"short_capacity": {
|
||||
"name": "Short parking capacity",
|
||||
"unit_of_measurement": "cars"
|
||||
"name": "Short parking capacity"
|
||||
},
|
||||
"long_capacity": {
|
||||
"name": "Long parking capacity",
|
||||
"unit_of_measurement": "cars"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"state": {
|
||||
"name": "State"
|
||||
"name": "Long parking capacity"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class HabiticaButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes Habitica button entity."""
|
||||
|
||||
press_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
|
||||
available_fn: Callable[[HabiticaData], bool]
|
||||
available_fn: Callable[[HabiticaData], bool] | None = None
|
||||
class_needed: str | None = None
|
||||
entity_picture: str | None = None
|
||||
|
||||
@@ -343,10 +343,11 @@ class HabiticaButton(HabiticaBase, ButtonEntity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Is entity available."""
|
||||
|
||||
return super().available and self.entity_description.available_fn(
|
||||
self.coordinator.data
|
||||
)
|
||||
if not super().available:
|
||||
return False
|
||||
if self.entity_description.available_fn:
|
||||
return self.entity_description.available_fn(self.coordinator.data)
|
||||
return True
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str | None:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import StrEnum
|
||||
|
||||
@@ -61,43 +60,6 @@ class HabiticaCalendarEntity(HabiticaBase, CalendarEntity):
|
||||
"""Initialize calendar entity."""
|
||||
super().__init__(coordinator, self.entity_description)
|
||||
|
||||
@abstractmethod
|
||||
def get_events(
|
||||
self, start_date: datetime, end_date: datetime | None = None
|
||||
) -> list[CalendarEvent]:
|
||||
"""Return events."""
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the current or next upcoming event."""
|
||||
|
||||
return next(iter(self.get_events(dt_util.now())), None)
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Return calendar events within a datetime range."""
|
||||
|
||||
return self.get_events(start_date, end_date)
|
||||
|
||||
@property
|
||||
def start_of_today(self) -> datetime:
|
||||
"""Habitica daystart."""
|
||||
return dt_util.start_of_local_day(
|
||||
datetime.fromisoformat(self.coordinator.data.user["lastCron"])
|
||||
)
|
||||
|
||||
def get_recurrence_dates(
|
||||
self, recurrences: rrule, start_date: datetime, end_date: datetime | None = None
|
||||
) -> list[datetime]:
|
||||
"""Calculate recurrence dates based on start_date and end_date."""
|
||||
if end_date:
|
||||
return recurrences.between(
|
||||
start_date, end_date - timedelta(days=1), inc=True
|
||||
)
|
||||
# if no end_date is given, return only the next recurrence
|
||||
return [recurrences.after(start_date, inc=True)]
|
||||
|
||||
|
||||
class HabiticaTodosCalendarEntity(HabiticaCalendarEntity):
|
||||
"""Habitica todos calendar entity."""
|
||||
@@ -107,7 +69,7 @@ class HabiticaTodosCalendarEntity(HabiticaCalendarEntity):
|
||||
translation_key=HabiticaCalendar.TODOS,
|
||||
)
|
||||
|
||||
def get_events(
|
||||
def dated_todos(
|
||||
self, start_date: datetime, end_date: datetime | None = None
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all dated todos."""
|
||||
@@ -150,6 +112,18 @@ class HabiticaTodosCalendarEntity(HabiticaCalendarEntity):
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the current or next upcoming event."""
|
||||
|
||||
return next(iter(self.dated_todos(dt_util.now())), None)
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Return calendar events within a datetime range."""
|
||||
return self.dated_todos(start_date, end_date)
|
||||
|
||||
|
||||
class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
|
||||
"""Habitica dailies calendar entity."""
|
||||
@@ -159,6 +133,13 @@ class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
|
||||
translation_key=HabiticaCalendar.DAILIES,
|
||||
)
|
||||
|
||||
@property
|
||||
def today(self) -> datetime:
|
||||
"""Habitica daystart."""
|
||||
return dt_util.start_of_local_day(
|
||||
datetime.fromisoformat(self.coordinator.data.user["lastCron"])
|
||||
)
|
||||
|
||||
def end_date(self, recurrence: datetime, end: datetime | None = None) -> date:
|
||||
"""Calculate the end date for a yesterdaily.
|
||||
|
||||
@@ -171,20 +152,29 @@ class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
|
||||
if end:
|
||||
return recurrence.date() + timedelta(days=1)
|
||||
return (
|
||||
dt_util.start_of_local_day()
|
||||
if recurrence == self.start_of_today
|
||||
else recurrence
|
||||
dt_util.start_of_local_day() if recurrence == self.today else recurrence
|
||||
).date() + timedelta(days=1)
|
||||
|
||||
def get_events(
|
||||
def get_recurrence_dates(
|
||||
self, recurrences: rrule, start_date: datetime, end_date: datetime | None = None
|
||||
) -> list[datetime]:
|
||||
"""Calculate recurrence dates based on start_date and end_date."""
|
||||
if end_date:
|
||||
return recurrences.between(
|
||||
start_date, end_date - timedelta(days=1), inc=True
|
||||
)
|
||||
# if no end_date is given, return only the next recurrence
|
||||
return [recurrences.after(self.today, inc=True)]
|
||||
|
||||
def due_dailies(
|
||||
self, start_date: datetime, end_date: datetime | None = None
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get dailies and recurrences for a given period or the next upcoming."""
|
||||
|
||||
# we only have dailies for today and future recurrences
|
||||
if end_date and end_date < self.start_of_today:
|
||||
if end_date and end_date < self.today:
|
||||
return []
|
||||
start_date = max(start_date, self.start_of_today)
|
||||
start_date = max(start_date, self.today)
|
||||
|
||||
events = []
|
||||
for task in self.coordinator.data.tasks:
|
||||
@@ -197,12 +187,10 @@ class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
|
||||
recurrences, start_date, end_date
|
||||
)
|
||||
for recurrence in recurrence_dates:
|
||||
is_future_event = recurrence > self.start_of_today
|
||||
is_current_event = (
|
||||
recurrence <= self.start_of_today and not task["completed"]
|
||||
)
|
||||
is_future_event = recurrence > self.today
|
||||
is_current_event = recurrence <= self.today and not task["completed"]
|
||||
|
||||
if not is_future_event and not is_current_event:
|
||||
if not (is_future_event or is_current_event):
|
||||
continue
|
||||
|
||||
events.append(
|
||||
@@ -226,15 +214,20 @@ class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
return next(iter(self.get_events(self.start_of_today)), None)
|
||||
return next(iter(self.due_dailies(self.today)), None)
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Return calendar events within a datetime range."""
|
||||
|
||||
return self.due_dailies(start_date, end_date)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, bool | None] | None:
|
||||
"""Return entity specific state attributes."""
|
||||
return {
|
||||
"yesterdaily": self.event.start < self.start_of_today.date()
|
||||
if self.event
|
||||
else None
|
||||
"yesterdaily": self.event.start < self.today.date() if self.event else None
|
||||
}
|
||||
|
||||
|
||||
@@ -246,7 +239,7 @@ class HabiticaTodoRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
translation_key=HabiticaCalendar.TODO_REMINDERS,
|
||||
)
|
||||
|
||||
def get_events(
|
||||
def reminders(
|
||||
self, start_date: datetime, end_date: datetime | None = None
|
||||
) -> list[CalendarEvent]:
|
||||
"""Reminders for todos."""
|
||||
@@ -289,6 +282,18 @@ class HabiticaTodoRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
key=lambda event: event.start,
|
||||
)
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
return next(iter(self.reminders(dt_util.now())), None)
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Return calendar events within a datetime range."""
|
||||
|
||||
return self.reminders(start_date, end_date)
|
||||
|
||||
|
||||
class HabiticaDailyRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
"""Habitica daily reminders calendar entity."""
|
||||
@@ -316,31 +321,47 @@ class HabiticaDailyRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
tzinfo=dt_util.DEFAULT_TIME_ZONE,
|
||||
)
|
||||
|
||||
def get_events(
|
||||
@property
|
||||
def today(self) -> datetime:
|
||||
"""Habitica daystart."""
|
||||
return dt_util.start_of_local_day(
|
||||
datetime.fromisoformat(self.coordinator.data.user["lastCron"])
|
||||
)
|
||||
|
||||
def get_recurrence_dates(
|
||||
self, recurrences: rrule, start_date: datetime, end_date: datetime | None = None
|
||||
) -> list[datetime]:
|
||||
"""Calculate recurrence dates based on start_date and end_date."""
|
||||
if end_date:
|
||||
return recurrences.between(
|
||||
start_date, end_date - timedelta(days=1), inc=True
|
||||
)
|
||||
# if no end_date is given, return only the next recurrence
|
||||
return [recurrences.after(self.today, inc=True)]
|
||||
|
||||
def reminders(
|
||||
self, start_date: datetime, end_date: datetime | None = None
|
||||
) -> list[CalendarEvent]:
|
||||
"""Reminders for dailies."""
|
||||
|
||||
events = []
|
||||
if end_date and end_date < self.start_of_today:
|
||||
if end_date and end_date < self.today:
|
||||
return []
|
||||
start_date = max(start_date, self.start_of_today)
|
||||
start_date = max(start_date, self.today)
|
||||
|
||||
for task in self.coordinator.data.tasks:
|
||||
if not (task["type"] == HabiticaTaskType.DAILY and task["everyX"]):
|
||||
continue
|
||||
|
||||
recurrences = build_rrule(task)
|
||||
recurrences_start = self.start_of_today
|
||||
recurrences_start = self.today
|
||||
|
||||
recurrence_dates = self.get_recurrence_dates(
|
||||
recurrences, recurrences_start, end_date
|
||||
)
|
||||
for recurrence in recurrence_dates:
|
||||
is_future_event = recurrence > self.start_of_today
|
||||
is_current_event = (
|
||||
recurrence <= self.start_of_today and not task["completed"]
|
||||
)
|
||||
is_future_event = recurrence > self.today
|
||||
is_current_event = recurrence <= self.today and not task["completed"]
|
||||
|
||||
if not is_future_event and not is_current_event:
|
||||
continue
|
||||
@@ -353,6 +374,9 @@ class HabiticaDailyRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
# Event ends before date range
|
||||
continue
|
||||
|
||||
if end_date and start > end_date:
|
||||
# Event starts after date range
|
||||
continue
|
||||
events.append(
|
||||
CalendarEvent(
|
||||
start=start,
|
||||
@@ -367,3 +391,15 @@ class HabiticaDailyRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
events,
|
||||
key=lambda event: event.start,
|
||||
)
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
return next(iter(self.reminders(dt_util.now())), None)
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Return calendar events within a datetime range."""
|
||||
|
||||
return self.reminders(start_date, end_date)
|
||||
|
||||
@@ -25,6 +25,8 @@ ATTR_DATA = "data"
|
||||
MANUFACTURER = "HabitRPG, Inc."
|
||||
NAME = "Habitica"
|
||||
|
||||
UNIT_TASKS = "tasks"
|
||||
|
||||
ATTR_CONFIG_ENTRY = "config_entry"
|
||||
ATTR_SKILL = "skill"
|
||||
ATTR_TASK = "task"
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
"""Diagnostics platform for Habitica integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_API_USER
|
||||
from .types import HabiticaConfigEntry
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: HabiticaConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
habitica_data = await config_entry.runtime_data.api.user.anonymized.get()
|
||||
|
||||
return {
|
||||
"config_entry_data": {
|
||||
CONF_URL: config_entry.data[CONF_URL],
|
||||
CONF_API_USER: config_entry.data[CONF_API_USER],
|
||||
},
|
||||
"habitica_data": habitica_data,
|
||||
}
|
||||
@@ -24,7 +24,7 @@ from homeassistant.helpers.issue_registry import (
|
||||
)
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import ASSETS_URL, DOMAIN
|
||||
from .const import ASSETS_URL, DOMAIN, UNIT_TASKS
|
||||
from .entity import HabiticaBase
|
||||
from .types import HabiticaConfigEntry
|
||||
from .util import entity_used_in, get_attribute_points, get_attributes_total
|
||||
@@ -84,34 +84,40 @@ SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.HEALTH,
|
||||
translation_key=HabitipySensorEntity.HEALTH,
|
||||
native_unit_of_measurement="HP",
|
||||
suggested_display_precision=0,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("hp"),
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.HEALTH_MAX,
|
||||
translation_key=HabitipySensorEntity.HEALTH_MAX,
|
||||
native_unit_of_measurement="HP",
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("maxHealth"),
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.MANA,
|
||||
translation_key=HabitipySensorEntity.MANA,
|
||||
native_unit_of_measurement="MP",
|
||||
suggested_display_precision=0,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("mp"),
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.MANA_MAX,
|
||||
translation_key=HabitipySensorEntity.MANA_MAX,
|
||||
native_unit_of_measurement="MP",
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("maxMP"),
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.EXPERIENCE,
|
||||
translation_key=HabitipySensorEntity.EXPERIENCE,
|
||||
native_unit_of_measurement="XP",
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("exp"),
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.EXPERIENCE_MAX,
|
||||
translation_key=HabitipySensorEntity.EXPERIENCE_MAX,
|
||||
native_unit_of_measurement="XP",
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("toNextLevel"),
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
@@ -122,6 +128,7 @@ SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.GOLD,
|
||||
translation_key=HabitipySensorEntity.GOLD,
|
||||
native_unit_of_measurement="GP",
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("gp"),
|
||||
),
|
||||
@@ -137,6 +144,7 @@ SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
|
||||
translation_key=HabitipySensorEntity.GEMS,
|
||||
value_fn=lambda user, _: user.get("balance", 0) * 4,
|
||||
suggested_display_precision=0,
|
||||
native_unit_of_measurement="gems",
|
||||
entity_picture="shop_gem.png",
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
@@ -221,17 +229,20 @@ TASK_SENSOR_DESCRIPTION: tuple[HabitipyTaskSensorEntityDescription, ...] = (
|
||||
HabitipyTaskSensorEntityDescription(
|
||||
key=HabitipySensorEntity.HABITS,
|
||||
translation_key=HabitipySensorEntity.HABITS,
|
||||
native_unit_of_measurement=UNIT_TASKS,
|
||||
value_fn=lambda tasks: [r for r in tasks if r.get("type") == "habit"],
|
||||
),
|
||||
HabitipyTaskSensorEntityDescription(
|
||||
key=HabitipySensorEntity.DAILIES,
|
||||
translation_key=HabitipySensorEntity.DAILIES,
|
||||
native_unit_of_measurement=UNIT_TASKS,
|
||||
value_fn=lambda tasks: [r for r in tasks if r.get("type") == "daily"],
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
HabitipyTaskSensorEntityDescription(
|
||||
key=HabitipySensorEntity.TODOS,
|
||||
translation_key=HabitipySensorEntity.TODOS,
|
||||
native_unit_of_measurement=UNIT_TASKS,
|
||||
value_fn=lambda tasks: [
|
||||
r for r in tasks if r.get("type") == "todo" and not r.get("completed")
|
||||
],
|
||||
@@ -240,6 +251,7 @@ TASK_SENSOR_DESCRIPTION: tuple[HabitipyTaskSensorEntityDescription, ...] = (
|
||||
HabitipyTaskSensorEntityDescription(
|
||||
key=HabitipySensorEntity.REWARDS,
|
||||
translation_key=HabitipySensorEntity.REWARDS,
|
||||
native_unit_of_measurement=UNIT_TASKS,
|
||||
value_fn=lambda tasks: [r for r in tasks if r.get("type") == "reward"],
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2,11 +2,7 @@
|
||||
"common": {
|
||||
"todos": "To-Do's",
|
||||
"dailies": "Dailies",
|
||||
"config_entry_name": "Select character",
|
||||
"unit_tasks": "tasks",
|
||||
"unit_health_points": "HP",
|
||||
"unit_mana_points": "MP",
|
||||
"unit_experience_points": "XP"
|
||||
"config_entry_name": "Select character"
|
||||
},
|
||||
"config": {
|
||||
"abort": {
|
||||
@@ -139,39 +135,31 @@
|
||||
"name": "Display name"
|
||||
},
|
||||
"health": {
|
||||
"name": "Health",
|
||||
"unit_of_measurement": "[%key:component::habitica::common::unit_health_points%]"
|
||||
"name": "Health"
|
||||
},
|
||||
"health_max": {
|
||||
"name": "Max. health",
|
||||
"unit_of_measurement": "[%key:component::habitica::common::unit_health_points%]"
|
||||
"name": "Max. health"
|
||||
},
|
||||
"mana": {
|
||||
"name": "Mana",
|
||||
"unit_of_measurement": "[%key:component::habitica::common::unit_mana_points%]"
|
||||
"name": "Mana"
|
||||
},
|
||||
"mana_max": {
|
||||
"name": "Max. mana",
|
||||
"unit_of_measurement": "[%key:component::habitica::common::unit_mana_points%]"
|
||||
"name": "Max. mana"
|
||||
},
|
||||
"experience": {
|
||||
"name": "Experience",
|
||||
"unit_of_measurement": "[%key:component::habitica::common::unit_experience_points%]"
|
||||
"name": "Experience"
|
||||
},
|
||||
"experience_max": {
|
||||
"name": "Next level",
|
||||
"unit_of_measurement": "[%key:component::habitica::common::unit_experience_points%]"
|
||||
"name": "Next level"
|
||||
},
|
||||
"level": {
|
||||
"name": "Level"
|
||||
},
|
||||
"gold": {
|
||||
"name": "Gold",
|
||||
"unit_of_measurement": "GP"
|
||||
"name": "Gold"
|
||||
},
|
||||
"gems": {
|
||||
"name": "Gems",
|
||||
"unit_of_measurement": "gems"
|
||||
"name": "Gems"
|
||||
},
|
||||
"trinkets": {
|
||||
"name": "Mystic hourglasses"
|
||||
@@ -186,20 +174,16 @@
|
||||
}
|
||||
},
|
||||
"todos": {
|
||||
"name": "[%key:component::habitica::common::todos%]",
|
||||
"unit_of_measurement": "[%key:component::habitica::common::unit_tasks%]"
|
||||
"name": "[%key:component::habitica::common::todos%]"
|
||||
},
|
||||
"dailys": {
|
||||
"name": "[%key:component::habitica::common::dailies%]",
|
||||
"unit_of_measurement": "[%key:component::habitica::common::unit_tasks%]"
|
||||
"name": "[%key:component::habitica::common::dailies%]"
|
||||
},
|
||||
"habits": {
|
||||
"name": "Habits",
|
||||
"unit_of_measurement": "[%key:component::habitica::common::unit_tasks%]"
|
||||
"name": "Habits"
|
||||
},
|
||||
"rewards": {
|
||||
"name": "Rewards",
|
||||
"unit_of_measurement": "[%key:component::habitica::common::unit_tasks%]"
|
||||
"name": "Rewards"
|
||||
},
|
||||
"strength": {
|
||||
"name": "Strength",
|
||||
|
||||
@@ -174,7 +174,7 @@ def get_attribute_points(
|
||||
)
|
||||
|
||||
return {
|
||||
"level": min(floor(user["stats"]["lvl"] / 2), 50),
|
||||
"level": min(round(user["stats"]["lvl"] / 2), 50),
|
||||
"equipment": equipment,
|
||||
"class": class_bonus,
|
||||
"allocated": user["stats"][attribute],
|
||||
|
||||
@@ -22,7 +22,7 @@ import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import websocket_api
|
||||
from .const import DOMAIN
|
||||
from .helpers import entities_may_have_state_changes_after, has_states_before
|
||||
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
|
||||
|
||||
CONF_ORDER = "use_include_order"
|
||||
|
||||
@@ -107,10 +107,7 @@ class HistoryPeriodView(HomeAssistantView):
|
||||
no_attributes = "no_attributes" in request.query
|
||||
|
||||
if (
|
||||
# has_states_before will return True if there are states older than
|
||||
# end_time. If it's false, we know there are no states in the
|
||||
# database up until end_time.
|
||||
(end_time and not has_states_before(hass, end_time))
|
||||
(end_time and not has_recorder_run_after(hass, end_time))
|
||||
or not include_start_time_state
|
||||
and entity_ids
|
||||
and not entities_may_have_state_changes_after(
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections.abc import Iterable
|
||||
from datetime import datetime as dt
|
||||
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.models import process_timestamp
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
||||
@@ -25,10 +26,8 @@ def entities_may_have_state_changes_after(
|
||||
return False
|
||||
|
||||
|
||||
def has_states_before(hass: HomeAssistant, run_time: dt) -> bool:
|
||||
"""Check if the recorder has states as old or older than run_time.
|
||||
|
||||
Returns True if there may be such states.
|
||||
"""
|
||||
oldest_ts = get_instance(hass).states_manager.oldest_ts
|
||||
return oldest_ts is not None and run_time.timestamp() >= oldest_ts
|
||||
def has_recorder_run_after(hass: HomeAssistant, run_time: dt) -> bool:
|
||||
"""Check if the recorder has any runs after a specific time."""
|
||||
return run_time >= process_timestamp(
|
||||
get_instance(hass).recorder_runs_manager.first.start
|
||||
)
|
||||
|
||||
@@ -39,7 +39,7 @@ from homeassistant.util.async_ import create_eager_task
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES
|
||||
from .helpers import entities_may_have_state_changes_after, has_states_before
|
||||
from .helpers import entities_may_have_state_changes_after, has_recorder_run_after
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -142,10 +142,7 @@ async def ws_get_history_during_period(
|
||||
no_attributes = msg["no_attributes"]
|
||||
|
||||
if (
|
||||
# has_states_before will return True if there are states older than
|
||||
# end_time. If it's false, we know there are no states in the
|
||||
# database up until end_time.
|
||||
(end_time and not has_states_before(hass, end_time))
|
||||
(end_time and not has_recorder_run_after(hass, end_time))
|
||||
or not include_start_time_state
|
||||
and entity_ids
|
||||
and not entities_may_have_state_changes_after(
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, cast
|
||||
|
||||
from requests import HTTPError
|
||||
@@ -45,8 +44,6 @@ type HomeConnectConfigEntry = ConfigEntry[api.ConfigEntryAuth]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
RE_CAMEL_CASE = re.compile(r"(?<!^)(?=[A-Z])|(?=\d)(?<=\D)")
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
@@ -88,7 +85,6 @@ PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.TIME,
|
||||
@@ -340,14 +336,3 @@ def get_dict_from_home_connect_error(err: api.HomeConnectError) -> dict[str, Any
|
||||
if len(err.args) > 0 and isinstance(err.args[0], str)
|
||||
else "?",
|
||||
}
|
||||
|
||||
|
||||
def bsh_key_to_translation_key(bsh_key: str) -> str:
|
||||
"""Convert a BSH key to a translation key format.
|
||||
|
||||
This function takes a BSH key, such as `Dishcare.Dishwasher.Program.Eco50`,
|
||||
and converts it to a translation key format, such as `dishcare_dishwasher_bsh_key_eco50`.
|
||||
"""
|
||||
return "_".join(
|
||||
RE_CAMEL_CASE.sub("_", split) for split in bsh_key.split(".")
|
||||
).lower()
|
||||
|
||||
@@ -5,23 +5,10 @@ DOMAIN = "home_connect"
|
||||
OAUTH2_AUTHORIZE = "https://api.home-connect.com/security/oauth/authorize"
|
||||
OAUTH2_TOKEN = "https://api.home-connect.com/security/oauth/token"
|
||||
|
||||
APPLIANCES_WITH_PROGRAMS = (
|
||||
"CleaningRobot",
|
||||
"CoffeeMaker",
|
||||
"Dishwasher",
|
||||
"Dryer",
|
||||
"Hood",
|
||||
"Oven",
|
||||
"WarmingDrawer",
|
||||
"Washer",
|
||||
"WasherDryer",
|
||||
)
|
||||
|
||||
BSH_POWER_STATE = "BSH.Common.Setting.PowerState"
|
||||
BSH_POWER_ON = "BSH.Common.EnumType.PowerState.On"
|
||||
BSH_POWER_OFF = "BSH.Common.EnumType.PowerState.Off"
|
||||
BSH_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby"
|
||||
BSH_SELECTED_PROGRAM = "BSH.Common.Root.SelectedProgram"
|
||||
BSH_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram"
|
||||
BSH_REMOTE_CONTROL_ACTIVATION_STATE = "BSH.Common.Status.RemoteControlActive"
|
||||
BSH_REMOTE_START_ALLOWANCE_STATE = "BSH.Common.Status.RemoteControlStartAllowed"
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.components.light import (
|
||||
LightEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
@@ -150,7 +150,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
||||
self.device.appliance.set_setting, self.bsh_key, True
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="turn_on_light",
|
||||
translation_placeholders={
|
||||
@@ -169,7 +169,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
||||
self._enable_custom_color_value_key,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="select_light_custom_color",
|
||||
translation_placeholders={
|
||||
@@ -187,7 +187,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
||||
f"#{hex_val}",
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_light_color",
|
||||
translation_placeholders={
|
||||
@@ -219,7 +219,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
||||
f"#{hex_val}",
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_light_color",
|
||||
translation_placeholders={
|
||||
@@ -244,7 +244,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
||||
self.device.appliance.set_setting, self._brightness_key, brightness
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_light_brightness",
|
||||
translation_placeholders={
|
||||
@@ -263,7 +263,7 @@ class HomeConnectLight(HomeConnectEntity, LightEntity):
|
||||
self.device.appliance.set_setting, self.bsh_key, False
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="turn_off_light",
|
||||
translation_placeholders={
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.components.number import (
|
||||
NumberEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import HomeConnectConfigEntry, get_dict_from_home_connect_error
|
||||
@@ -117,7 +117,7 @@ class HomeConnectNumberEntity(HomeConnectEntity, NumberEntity):
|
||||
value,
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_setting",
|
||||
translation_placeholders={
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
"""Provides a select platform for Home Connect."""
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
|
||||
from homeconnect.api import HomeConnectError
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import (
|
||||
HomeConnectConfigEntry,
|
||||
bsh_key_to_translation_key,
|
||||
get_dict_from_home_connect_error,
|
||||
)
|
||||
from .api import HomeConnectDevice
|
||||
from .const import (
|
||||
APPLIANCES_WITH_PROGRAMS,
|
||||
ATTR_VALUE,
|
||||
BSH_ACTIVE_PROGRAM,
|
||||
BSH_SELECTED_PROGRAM,
|
||||
DOMAIN,
|
||||
)
|
||||
from .entity import HomeConnectEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRANSLATION_KEYS_PROGRAMS_MAP = {
|
||||
bsh_key_to_translation_key(program): program
|
||||
for program in (
|
||||
"ConsumerProducts.CleaningRobot.Program.Cleaning.CleanAll",
|
||||
"ConsumerProducts.CleaningRobot.Program.Cleaning.CleanMap",
|
||||
"ConsumerProducts.CleaningRobot.Program.Basic.GoHome",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.Ristretto",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.Espresso",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoDoppio",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.Coffee",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.XLCoffee",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeGrande",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.Cappuccino",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.LatteMacchiato",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeLatte",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.MilkFroth",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.WarmMilk",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.KleinerBrauner",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.GrosserBrauner",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Verlaengerter",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.VerlaengerterBraun",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.WienerMelange",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.FlatWhite",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Cortado",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.CafeCortado",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.CafeConLeche",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.CafeAuLait",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Doppio",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Kaapi",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.KoffieVerkeerd",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Galao",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Garoto",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Americano",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.RedEye",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.BlackEye",
|
||||
"ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.DeadEye",
|
||||
"ConsumerProducts.CoffeeMaker.Program.Beverage.HotWater",
|
||||
"Dishcare.Dishwasher.Program.PreRinse",
|
||||
"Dishcare.Dishwasher.Program.Auto1",
|
||||
"Dishcare.Dishwasher.Program.Auto2",
|
||||
"Dishcare.Dishwasher.Program.Auto3",
|
||||
"Dishcare.Dishwasher.Program.Eco50",
|
||||
"Dishcare.Dishwasher.Program.Quick45",
|
||||
"Dishcare.Dishwasher.Program.Intensiv70",
|
||||
"Dishcare.Dishwasher.Program.Normal65",
|
||||
"Dishcare.Dishwasher.Program.Glas40",
|
||||
"Dishcare.Dishwasher.Program.GlassCare",
|
||||
"Dishcare.Dishwasher.Program.NightWash",
|
||||
"Dishcare.Dishwasher.Program.Quick65",
|
||||
"Dishcare.Dishwasher.Program.Normal45",
|
||||
"Dishcare.Dishwasher.Program.Intensiv45",
|
||||
"Dishcare.Dishwasher.Program.AutoHalfLoad",
|
||||
"Dishcare.Dishwasher.Program.IntensivPower",
|
||||
"Dishcare.Dishwasher.Program.MagicDaily",
|
||||
"Dishcare.Dishwasher.Program.Super60",
|
||||
"Dishcare.Dishwasher.Program.Kurz60",
|
||||
"Dishcare.Dishwasher.Program.ExpressSparkle65",
|
||||
"Dishcare.Dishwasher.Program.MachineCare",
|
||||
"Dishcare.Dishwasher.Program.SteamFresh",
|
||||
"Dishcare.Dishwasher.Program.MaximumCleaning",
|
||||
"Dishcare.Dishwasher.Program.MixedLoad",
|
||||
"LaundryCare.Dryer.Program.Cotton",
|
||||
"LaundryCare.Dryer.Program.Synthetic",
|
||||
"LaundryCare.Dryer.Program.Mix",
|
||||
"LaundryCare.Dryer.Program.Blankets",
|
||||
"LaundryCare.Dryer.Program.BusinessShirts",
|
||||
"LaundryCare.Dryer.Program.DownFeathers",
|
||||
"LaundryCare.Dryer.Program.Hygiene",
|
||||
"LaundryCare.Dryer.Program.Jeans",
|
||||
"LaundryCare.Dryer.Program.Outdoor",
|
||||
"LaundryCare.Dryer.Program.SyntheticRefresh",
|
||||
"LaundryCare.Dryer.Program.Towels",
|
||||
"LaundryCare.Dryer.Program.Delicates",
|
||||
"LaundryCare.Dryer.Program.Super40",
|
||||
"LaundryCare.Dryer.Program.Shirts15",
|
||||
"LaundryCare.Dryer.Program.Pillow",
|
||||
"LaundryCare.Dryer.Program.AntiShrink",
|
||||
"LaundryCare.Dryer.Program.MyTime.MyDryingTime",
|
||||
"LaundryCare.Dryer.Program.TimeCold",
|
||||
"LaundryCare.Dryer.Program.TimeWarm",
|
||||
"LaundryCare.Dryer.Program.InBasket",
|
||||
"LaundryCare.Dryer.Program.TimeColdFix.TimeCold20",
|
||||
"LaundryCare.Dryer.Program.TimeColdFix.TimeCold30",
|
||||
"LaundryCare.Dryer.Program.TimeColdFix.TimeCold60",
|
||||
"LaundryCare.Dryer.Program.TimeWarmFix.TimeWarm30",
|
||||
"LaundryCare.Dryer.Program.TimeWarmFix.TimeWarm40",
|
||||
"LaundryCare.Dryer.Program.TimeWarmFix.TimeWarm60",
|
||||
"LaundryCare.Dryer.Program.Dessous",
|
||||
"Cooking.Common.Program.Hood.Automatic",
|
||||
"Cooking.Common.Program.Hood.Venting",
|
||||
"Cooking.Common.Program.Hood.DelayedShutOff",
|
||||
"Cooking.Oven.Program.HeatingMode.PreHeating",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAirEco",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAirGrilling",
|
||||
"Cooking.Oven.Program.HeatingMode.TopBottomHeating",
|
||||
"Cooking.Oven.Program.HeatingMode.TopBottomHeatingEco",
|
||||
"Cooking.Oven.Program.HeatingMode.BottomHeating",
|
||||
"Cooking.Oven.Program.HeatingMode.PizzaSetting",
|
||||
"Cooking.Oven.Program.HeatingMode.SlowCook",
|
||||
"Cooking.Oven.Program.HeatingMode.IntensiveHeat",
|
||||
"Cooking.Oven.Program.HeatingMode.KeepWarm",
|
||||
"Cooking.Oven.Program.HeatingMode.PreheatOvenware",
|
||||
"Cooking.Oven.Program.HeatingMode.FrozenHeatupSpecial",
|
||||
"Cooking.Oven.Program.HeatingMode.Desiccation",
|
||||
"Cooking.Oven.Program.HeatingMode.Defrost",
|
||||
"Cooking.Oven.Program.HeatingMode.Proof",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir30Steam",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir60Steam",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir80Steam",
|
||||
"Cooking.Oven.Program.HeatingMode.HotAir100Steam",
|
||||
"Cooking.Oven.Program.HeatingMode.SabbathProgramme",
|
||||
"Cooking.Oven.Program.Microwave.90Watt",
|
||||
"Cooking.Oven.Program.Microwave.180Watt",
|
||||
"Cooking.Oven.Program.Microwave.360Watt",
|
||||
"Cooking.Oven.Program.Microwave.600Watt",
|
||||
"Cooking.Oven.Program.Microwave.900Watt",
|
||||
"Cooking.Oven.Program.Microwave.1000Watt",
|
||||
"Cooking.Oven.Program.Microwave.Max",
|
||||
"Cooking.Oven.Program.HeatingMode.WarmingDrawer",
|
||||
"LaundryCare.Washer.Program.Cotton",
|
||||
"LaundryCare.Washer.Program.Cotton.CottonEco",
|
||||
"LaundryCare.Washer.Program.Cotton.Eco4060",
|
||||
"LaundryCare.Washer.Program.Cotton.Colour",
|
||||
"LaundryCare.Washer.Program.EasyCare",
|
||||
"LaundryCare.Washer.Program.Mix",
|
||||
"LaundryCare.Washer.Program.Mix.NightWash",
|
||||
"LaundryCare.Washer.Program.DelicatesSilk",
|
||||
"LaundryCare.Washer.Program.Wool",
|
||||
"LaundryCare.Washer.Program.Sensitive",
|
||||
"LaundryCare.Washer.Program.Auto30",
|
||||
"LaundryCare.Washer.Program.Auto40",
|
||||
"LaundryCare.Washer.Program.Auto60",
|
||||
"LaundryCare.Washer.Program.Chiffon",
|
||||
"LaundryCare.Washer.Program.Curtains",
|
||||
"LaundryCare.Washer.Program.DarkWash",
|
||||
"LaundryCare.Washer.Program.Dessous",
|
||||
"LaundryCare.Washer.Program.Monsoon",
|
||||
"LaundryCare.Washer.Program.Outdoor",
|
||||
"LaundryCare.Washer.Program.PlushToy",
|
||||
"LaundryCare.Washer.Program.ShirtsBlouses",
|
||||
"LaundryCare.Washer.Program.SportFitness",
|
||||
"LaundryCare.Washer.Program.Towels",
|
||||
"LaundryCare.Washer.Program.WaterProof",
|
||||
"LaundryCare.Washer.Program.PowerSpeed59",
|
||||
"LaundryCare.Washer.Program.Super153045.Super15",
|
||||
"LaundryCare.Washer.Program.Super153045.Super1530",
|
||||
"LaundryCare.Washer.Program.DownDuvet.Duvet",
|
||||
"LaundryCare.Washer.Program.Rinse.RinseSpinDrain",
|
||||
"LaundryCare.Washer.Program.DrumClean",
|
||||
"LaundryCare.WasherDryer.Program.Cotton",
|
||||
"LaundryCare.WasherDryer.Program.Cotton.Eco4060",
|
||||
"LaundryCare.WasherDryer.Program.Mix",
|
||||
"LaundryCare.WasherDryer.Program.EasyCare",
|
||||
"LaundryCare.WasherDryer.Program.WashAndDry60",
|
||||
"LaundryCare.WasherDryer.Program.WashAndDry90",
|
||||
)
|
||||
}
|
||||
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP = {
|
||||
value: key for key, value in TRANSLATION_KEYS_PROGRAMS_MAP.items()
|
||||
}
|
||||
|
||||
PROGRAM_SELECT_ENTITY_DESCRIPTIONS = (
|
||||
SelectEntityDescription(
|
||||
key=BSH_ACTIVE_PROGRAM,
|
||||
translation_key="active_program",
|
||||
),
|
||||
SelectEntityDescription(
|
||||
key=BSH_SELECTED_PROGRAM,
|
||||
translation_key="selected_program",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HomeConnectConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Home Connect select entities."""
|
||||
|
||||
def get_entities() -> list[HomeConnectProgramSelectEntity]:
|
||||
"""Get a list of entities."""
|
||||
entities: list[HomeConnectProgramSelectEntity] = []
|
||||
programs_not_found = set()
|
||||
for device in entry.runtime_data.devices:
|
||||
if device.appliance.type in APPLIANCES_WITH_PROGRAMS:
|
||||
with contextlib.suppress(HomeConnectError):
|
||||
programs = device.appliance.get_programs_available()
|
||||
if programs:
|
||||
for program in programs:
|
||||
if program not in PROGRAMS_TRANSLATION_KEYS_MAP:
|
||||
programs.remove(program)
|
||||
if program not in programs_not_found:
|
||||
_LOGGER.info(
|
||||
'The program "%s" is not part of the official Home Connect API specification',
|
||||
program,
|
||||
)
|
||||
programs_not_found.add(program)
|
||||
entities.extend(
|
||||
HomeConnectProgramSelectEntity(device, programs, desc)
|
||||
for desc in PROGRAM_SELECT_ENTITY_DESCRIPTIONS
|
||||
)
|
||||
return entities
|
||||
|
||||
async_add_entities(await hass.async_add_executor_job(get_entities), True)
|
||||
|
||||
|
||||
class HomeConnectProgramSelectEntity(HomeConnectEntity, SelectEntity):
|
||||
"""Select class for Home Connect programs."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: HomeConnectDevice,
|
||||
programs: list[str],
|
||||
desc: SelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(
|
||||
device,
|
||||
desc,
|
||||
)
|
||||
self._attr_options = [
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP[program] for program in programs
|
||||
]
|
||||
self.start_on_select = desc.key == BSH_ACTIVE_PROGRAM
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the program selection status."""
|
||||
program = self.device.appliance.status.get(self.bsh_key, {}).get(ATTR_VALUE)
|
||||
if not program:
|
||||
program_translation_key = None
|
||||
elif not (
|
||||
program_translation_key := PROGRAMS_TRANSLATION_KEYS_MAP.get(program)
|
||||
):
|
||||
_LOGGER.debug(
|
||||
'The program "%s" is not part of the official Home Connect API specification',
|
||||
program,
|
||||
)
|
||||
self._attr_current_option = program_translation_key
|
||||
_LOGGER.debug("Updated, new program: %s", self._attr_current_option)
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select new program."""
|
||||
bsh_key = TRANSLATION_KEYS_PROGRAMS_MAP[option]
|
||||
_LOGGER.debug(
|
||||
"Starting program: %s" if self.start_on_select else "Selecting program: %s",
|
||||
bsh_key,
|
||||
)
|
||||
if self.start_on_select:
|
||||
target = self.device.appliance.start_program
|
||||
else:
|
||||
target = self.device.appliance.select_program
|
||||
try:
|
||||
await self.hass.async_add_executor_job(target, bsh_key)
|
||||
except HomeConnectError as err:
|
||||
if self.start_on_select:
|
||||
translation_key = "start_program"
|
||||
else:
|
||||
translation_key = "select_program"
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
"program": bsh_key,
|
||||
},
|
||||
) from err
|
||||
self.async_entity_update()
|
||||
@@ -23,43 +23,40 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"turn_on_light": {
|
||||
"message": "Error turning on {entity_id}: {description}"
|
||||
"message": "Error while trying to turn on {entity_id}: {description}"
|
||||
},
|
||||
"turn_off_light": {
|
||||
"message": "Error turning off {entity_id}: {description}"
|
||||
"message": "Error while trying to turn off {entity_id}: {description}"
|
||||
},
|
||||
"set_light_brightness": {
|
||||
"message": "Error setting brightness of {entity_id}: {description}"
|
||||
"message": "Error while trying to set brightness of {entity_id}: {description}"
|
||||
},
|
||||
"select_light_custom_color": {
|
||||
"message": "Error selecting custom color of {entity_id}: {description}"
|
||||
"message": "Error while trying to select custom color of {entity_id}: {description}"
|
||||
},
|
||||
"set_light_color": {
|
||||
"message": "Error setting color of {entity_id}: {description}"
|
||||
"message": "Error while trying to set color of {entity_id}: {description}"
|
||||
},
|
||||
"set_setting": {
|
||||
"message": "Error assigning the value \"{value}\" to the setting \"{setting_key}\" for {entity_id}: {description}"
|
||||
"message": "Error while trying to assign the value \"{value}\" to the setting \"{setting_key}\" for {entity_id}: {description}"
|
||||
},
|
||||
"turn_on": {
|
||||
"message": "Error turning on {entity_id} ({setting_key}): {description}"
|
||||
"message": "Error while trying to turn on {entity_id} ({setting_key}): {description}"
|
||||
},
|
||||
"turn_off": {
|
||||
"message": "Error turning off {entity_id} ({setting_key}): {description}"
|
||||
},
|
||||
"select_program": {
|
||||
"message": "Error selecting program {program}: {description}"
|
||||
"message": "Error while trying to turn off {entity_id} ({setting_key}): {description}"
|
||||
},
|
||||
"start_program": {
|
||||
"message": "Error starting program {program}: {description}"
|
||||
"message": "Error while trying to start program {program}: {description}"
|
||||
},
|
||||
"stop_program": {
|
||||
"message": "Error stopping program {program}: {description}"
|
||||
"message": "Error while trying to stop program {program}: {description}"
|
||||
},
|
||||
"power_on": {
|
||||
"message": "Error turning on {appliance_name}: {description}"
|
||||
"message": "Error while trying to turn on {appliance_name}: {description}"
|
||||
},
|
||||
"power_off": {
|
||||
"message": "Error turning off {appliance_name} with value \"{value}\": {description}"
|
||||
"message": "Error while trying to turn off {appliance_name} with value \"{value}\": {description}"
|
||||
},
|
||||
"turn_off_not_supported": {
|
||||
"message": "{appliance_name} does not support turning off or entering standby mode."
|
||||
@@ -270,326 +267,6 @@
|
||||
"name": "Wine compartment 3 temperature"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"selected_program": {
|
||||
"name": "Selected program",
|
||||
"state": {
|
||||
"consumer_products_cleaning_robot_program_cleaning_clean_all": "Clean all",
|
||||
"consumer_products_cleaning_robot_program_cleaning_clean_map": "Clean map",
|
||||
"consumer_products_cleaning_robot_program_basic_go_home": "Go home",
|
||||
"consumer_products_coffee_maker_program_beverage_ristretto": "Ristretto",
|
||||
"consumer_products_coffee_maker_program_beverage_espresso": "Espresso",
|
||||
"consumer_products_coffee_maker_program_beverage_espresso_doppio": "Espresso doppio",
|
||||
"consumer_products_coffee_maker_program_beverage_coffee": "Coffee",
|
||||
"consumer_products_coffee_maker_program_beverage_x_l_coffee": "XL coffee",
|
||||
"consumer_products_coffee_maker_program_beverage_caffe_grande": "Caffe grande",
|
||||
"consumer_products_coffee_maker_program_beverage_espresso_macchiato": "Espresso macchiato",
|
||||
"consumer_products_coffee_maker_program_beverage_cappuccino": "Cappuccino",
|
||||
"consumer_products_coffee_maker_program_beverage_latte_macchiato": "Latte macchiato",
|
||||
"consumer_products_coffee_maker_program_beverage_caffe_latte": "Caffe latte",
|
||||
"consumer_products_coffee_maker_program_beverage_milk_froth": "Milk froth",
|
||||
"consumer_products_coffee_maker_program_beverage_warm_milk": "Warm milk",
|
||||
"consumer_products_coffee_maker_program_coffee_world_kleiner_brauner": "Kleiner brauner",
|
||||
"consumer_products_coffee_maker_program_coffee_world_grosser_brauner": "Grosser brauner",
|
||||
"consumer_products_coffee_maker_program_coffee_world_verlaengerter": "Verlaengerter",
|
||||
"consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun": "Verlaengerter braun",
|
||||
"consumer_products_coffee_maker_program_coffee_world_wiener_melange": "Wiener melange",
|
||||
"consumer_products_coffee_maker_program_coffee_world_flat_white": "Flat white",
|
||||
"consumer_products_coffee_maker_program_coffee_world_cortado": "Cortado",
|
||||
"consumer_products_coffee_maker_program_coffee_world_cafe_cortado": "Cafe cortado",
|
||||
"consumer_products_coffee_maker_program_coffee_world_cafe_con_leche": "Cafe con leche",
|
||||
"consumer_products_coffee_maker_program_coffee_world_cafe_au_lait": "Cafe au lait",
|
||||
"consumer_products_coffee_maker_program_coffee_world_doppio": "Doppio",
|
||||
"consumer_products_coffee_maker_program_coffee_world_kaapi": "Kaapi",
|
||||
"consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd": "Koffie verkeerd",
|
||||
"consumer_products_coffee_maker_program_coffee_world_galao": "Galao",
|
||||
"consumer_products_coffee_maker_program_coffee_world_garoto": "Garoto",
|
||||
"consumer_products_coffee_maker_program_coffee_world_americano": "Americano",
|
||||
"consumer_products_coffee_maker_program_coffee_world_red_eye": "Red eye",
|
||||
"consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye",
|
||||
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye",
|
||||
"consumer_products_coffee_maker_program_beverage_hot_water": "Hot water",
|
||||
"dishcare_dishwasher_program_pre_rinse": "Pre_rinse",
|
||||
"dishcare_dishwasher_program_auto_1": "Auto 1",
|
||||
"dishcare_dishwasher_program_auto_2": "Auto 2",
|
||||
"dishcare_dishwasher_program_auto_3": "Auto 3",
|
||||
"dishcare_dishwasher_program_eco_50": "Eco 50ºC",
|
||||
"dishcare_dishwasher_program_quick_45": "Quick 45ºC",
|
||||
"dishcare_dishwasher_program_intensiv_70": "Intensive 70ºC",
|
||||
"dishcare_dishwasher_program_normal_65": "Normal 65ºC",
|
||||
"dishcare_dishwasher_program_glas_40": "Glass 40ºC",
|
||||
"dishcare_dishwasher_program_glass_care": "Glass care",
|
||||
"dishcare_dishwasher_program_night_wash": "Night wash",
|
||||
"dishcare_dishwasher_program_quick_65": "Quick 65ºC",
|
||||
"dishcare_dishwasher_program_normal_45": "Normal 45ºC",
|
||||
"dishcare_dishwasher_program_intensiv_45": "Intensive 45ºC",
|
||||
"dishcare_dishwasher_program_auto_half_load": "Auto half load",
|
||||
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
|
||||
"dishcare_dishwasher_program_magic_daily": "Magic daily",
|
||||
"dishcare_dishwasher_program_super_60": "Super 60ºC",
|
||||
"dishcare_dishwasher_program_kurz_60": "Kurz 60ºC",
|
||||
"dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC",
|
||||
"dishcare_dishwasher_program_machine_care": "Machine care",
|
||||
"dishcare_dishwasher_program_steam_fresh": "Steam fresh",
|
||||
"dishcare_dishwasher_program_maximum_cleaning": "Maximum cleaning",
|
||||
"dishcare_dishwasher_program_mixed_load": "Mixed load",
|
||||
"laundry_care_dryer_program_cotton": "Cotton",
|
||||
"laundry_care_dryer_program_synthetic": "Synthetic",
|
||||
"laundry_care_dryer_program_mix": "Mix",
|
||||
"laundry_care_dryer_program_blankets": "Blankets",
|
||||
"laundry_care_dryer_program_business_shirts": "Business shirts",
|
||||
"laundry_care_dryer_program_down_feathers": "Down feathers",
|
||||
"laundry_care_dryer_program_hygiene": "Hygiene",
|
||||
"laundry_care_dryer_program_jeans": "Jeans",
|
||||
"laundry_care_dryer_program_outdoor": "Outdoor",
|
||||
"laundry_care_dryer_program_synthetic_refresh": "Synthetic refresh",
|
||||
"laundry_care_dryer_program_towels": "Towels",
|
||||
"laundry_care_dryer_program_delicates": "Delicates",
|
||||
"laundry_care_dryer_program_super_40": "Super 40ºC",
|
||||
"laundry_care_dryer_program_shirts_15": "Shirts 15ºC",
|
||||
"laundry_care_dryer_program_pillow": "Pillow",
|
||||
"laundry_care_dryer_program_anti_shrink": "Anti shrink",
|
||||
"laundry_care_dryer_program_my_time_my_drying_time": "My drying time",
|
||||
"laundry_care_dryer_program_time_cold": "Cold (variable time)",
|
||||
"laundry_care_dryer_program_time_warm": "Warm (variable time)",
|
||||
"laundry_care_dryer_program_in_basket": "In basket",
|
||||
"laundry_care_dryer_program_time_cold_fix_time_cold_20": "Cold (20 min)",
|
||||
"laundry_care_dryer_program_time_cold_fix_time_cold_30": "Cold (30 min)",
|
||||
"laundry_care_dryer_program_time_cold_fix_time_cold_60": "Cold (60 min)",
|
||||
"laundry_care_dryer_program_time_warm_fix_time_warm_30": "Warm (30 min)",
|
||||
"laundry_care_dryer_program_time_warm_fix_time_warm_40": "Warm (40 min)",
|
||||
"laundry_care_dryer_program_time_warm_fix_time_warm_60": "Warm (60 min)",
|
||||
"laundry_care_dryer_program_dessous": "Dessous",
|
||||
"cooking_common_program_hood_automatic": "Automatic",
|
||||
"cooking_common_program_hood_venting": "Venting",
|
||||
"cooking_common_program_hood_delayed_shut_off": "Delayed shut off",
|
||||
"cooking_oven_program_heating_mode_pre_heating": "Pre-heating",
|
||||
"cooking_oven_program_heating_mode_hot_air": "Hot air",
|
||||
"cooking_oven_program_heating_mode_hot_air_eco": "Hot air eco",
|
||||
"cooking_oven_program_heating_mode_hot_air_grilling": "Hot air grilling",
|
||||
"cooking_oven_program_heating_mode_top_bottom_heating": "Top bottom heating",
|
||||
"cooking_oven_program_heating_mode_top_bottom_heating_eco": "Top bottom heating eco",
|
||||
"cooking_oven_program_heating_mode_bottom_heating": "Bottom heating",
|
||||
"cooking_oven_program_heating_mode_pizza_setting": "Pizza setting",
|
||||
"cooking_oven_program_heating_mode_slow_cook": "Slow cook",
|
||||
"cooking_oven_program_heating_mode_intensive_heat": "Intensive heat",
|
||||
"cooking_oven_program_heating_mode_keep_warm": "Keep warm",
|
||||
"cooking_oven_program_heating_mode_preheat_ovenware": "Preheat ovenware",
|
||||
"cooking_oven_program_heating_mode_frozen_heatup_special": "Special Heat-Up for frozen products",
|
||||
"cooking_oven_program_heating_mode_desiccation": "Desiccation",
|
||||
"cooking_oven_program_heating_mode_defrost": "Defrost",
|
||||
"cooking_oven_program_heating_mode_proof": "Proof",
|
||||
"cooking_oven_program_heating_mode_hot_air_30_steam": "Hot air + 30 RH",
|
||||
"cooking_oven_program_heating_mode_hot_air_60_steam": "Hot air + 60 RH",
|
||||
"cooking_oven_program_heating_mode_hot_air_80_steam": "Hot air + 80 RH",
|
||||
"cooking_oven_program_heating_mode_hot_air_100_steam": "Hot air + 100 RH",
|
||||
"cooking_oven_program_heating_mode_sabbath_programme": "Sabbath programme",
|
||||
"cooking_oven_program_microwave_90_watt": "90 Watt",
|
||||
"cooking_oven_program_microwave_180_watt": "180 Watt",
|
||||
"cooking_oven_program_microwave_360_watt": "360 Watt",
|
||||
"cooking_oven_program_microwave_600_watt": "600 Watt",
|
||||
"cooking_oven_program_microwave_900_watt": "900 Watt",
|
||||
"cooking_oven_program_microwave_1000_watt": "1000 Watt",
|
||||
"cooking_oven_program_microwave_max": "Max",
|
||||
"cooking_oven_program_heating_mode_warming_drawer": "Warming drawer",
|
||||
"laundry_care_washer_program_cotton": "Cotton",
|
||||
"laundry_care_washer_program_cotton_cotton_eco": "Cotton eco",
|
||||
"laundry_care_washer_program_cotton_eco_4060": "Cotton eco 40/60ºC",
|
||||
"laundry_care_washer_program_cotton_colour": "Cotton color",
|
||||
"laundry_care_washer_program_easy_care": "Easy care",
|
||||
"laundry_care_washer_program_mix": "Mix",
|
||||
"laundry_care_washer_program_mix_night_wash": "Mix night wash",
|
||||
"laundry_care_washer_program_delicates_silk": "Delicates silk",
|
||||
"laundry_care_washer_program_wool": "Wool",
|
||||
"laundry_care_washer_program_sensitive": "Sensitive",
|
||||
"laundry_care_washer_program_auto_30": "Auto 30ºC",
|
||||
"laundry_care_washer_program_auto_40": "Auto 40ºC",
|
||||
"laundry_care_washer_program_auto_60": "Auto 60ºC",
|
||||
"laundry_care_washer_program_chiffon": "Chiffon",
|
||||
"laundry_care_washer_program_curtains": "Curtains",
|
||||
"laundry_care_washer_program_dark_wash": "Dark wash",
|
||||
"laundry_care_washer_program_dessous": "Dessous",
|
||||
"laundry_care_washer_program_monsoon": "Monsoon",
|
||||
"laundry_care_washer_program_outdoor": "Outdoor",
|
||||
"laundry_care_washer_program_plush_toy": "Plush toy",
|
||||
"laundry_care_washer_program_shirts_blouses": "Shirts blouses",
|
||||
"laundry_care_washer_program_sport_fitness": "Sport fitness",
|
||||
"laundry_care_washer_program_towels": "Towels",
|
||||
"laundry_care_washer_program_water_proof": "Water proof",
|
||||
"laundry_care_washer_program_power_speed_59": "Power speed <60 min",
|
||||
"laundry_care_washer_program_super_153045_super_15": "Super 15 min",
|
||||
"laundry_care_washer_program_super_153045_super_1530": "Super 15/30 min",
|
||||
"laundry_care_washer_program_down_duvet_duvet": "Down duvet",
|
||||
"laundry_care_washer_program_rinse_rinse_spin_drain": "Rinse spin drain",
|
||||
"laundry_care_washer_program_drum_clean": "Drum clean",
|
||||
"laundry_care_washer_dryer_program_cotton": "Cotton",
|
||||
"laundry_care_washer_dryer_program_cotton_eco_4060": "Cotton eco 40/60 ºC",
|
||||
"laundry_care_washer_dryer_program_mix": "Mix",
|
||||
"laundry_care_washer_dryer_program_easy_care": "Easy care",
|
||||
"laundry_care_washer_dryer_program_wash_and_dry_60": "Wash and dry (60 min)",
|
||||
"laundry_care_washer_dryer_program_wash_and_dry_90": "Wash and dry (90 min)"
|
||||
}
|
||||
},
|
||||
"active_program": {
|
||||
"name": "Active program",
|
||||
"state": {
|
||||
"consumer_products_cleaning_robot_program_cleaning_clean_all": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_cleaning_robot_program_cleaning_clean_all%]",
|
||||
"consumer_products_cleaning_robot_program_cleaning_clean_map": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_cleaning_robot_program_cleaning_clean_map%]",
|
||||
"consumer_products_cleaning_robot_program_basic_go_home": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_cleaning_robot_program_basic_go_home%]",
|
||||
"consumer_products_coffee_maker_program_beverage_ristretto": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_ristretto%]",
|
||||
"consumer_products_coffee_maker_program_beverage_espresso": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_espresso%]",
|
||||
"consumer_products_coffee_maker_program_beverage_espresso_doppio": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_espresso_doppio%]",
|
||||
"consumer_products_coffee_maker_program_beverage_coffee": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_coffee%]",
|
||||
"consumer_products_coffee_maker_program_beverage_x_l_coffee": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_x_l_coffee%]",
|
||||
"consumer_products_coffee_maker_program_beverage_caffe_grande": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_caffe_grande%]",
|
||||
"consumer_products_coffee_maker_program_beverage_espresso_macchiato": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_espresso_macchiato%]",
|
||||
"consumer_products_coffee_maker_program_beverage_cappuccino": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_cappuccino%]",
|
||||
"consumer_products_coffee_maker_program_beverage_latte_macchiato": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_latte_macchiato%]",
|
||||
"consumer_products_coffee_maker_program_beverage_caffe_latte": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_caffe_latte%]",
|
||||
"consumer_products_coffee_maker_program_beverage_milk_froth": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_milk_froth%]",
|
||||
"consumer_products_coffee_maker_program_beverage_warm_milk": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_warm_milk%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_kleiner_brauner": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_kleiner_brauner%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_grosser_brauner": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_grosser_brauner%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_verlaengerter": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_verlaengerter%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_wiener_melange": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_wiener_melange%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_flat_white": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_flat_white%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_cortado": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_cortado%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_cafe_cortado": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_cafe_cortado%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_cafe_con_leche": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_cafe_con_leche%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_cafe_au_lait": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_cafe_au_lait%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_doppio": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_doppio%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_kaapi": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_kaapi%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_galao": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_galao%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_garoto": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_garoto%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_americano": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_americano%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_red_eye": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_red_eye%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_black_eye": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_black_eye%]",
|
||||
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_coffee_world_dead_eye%]",
|
||||
"consumer_products_coffee_maker_program_beverage_hot_water": "[%key:component::home_connect::entity::select::selected_program::state::consumer_products_coffee_maker_program_beverage_hot_water%]",
|
||||
"dishcare_dishwasher_program_pre_rinse": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_pre_rinse%]",
|
||||
"dishcare_dishwasher_program_auto_1": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_auto_1%]",
|
||||
"dishcare_dishwasher_program_auto_2": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_auto_2%]",
|
||||
"dishcare_dishwasher_program_auto_3": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_auto_3%]",
|
||||
"dishcare_dishwasher_program_eco_50": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_eco_50%]",
|
||||
"dishcare_dishwasher_program_quick_45": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_quick_45%]",
|
||||
"dishcare_dishwasher_program_intensiv_70": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_intensiv_70%]",
|
||||
"dishcare_dishwasher_program_normal_65": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_normal_65%]",
|
||||
"dishcare_dishwasher_program_glas_40": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_glas_40%]",
|
||||
"dishcare_dishwasher_program_glass_care": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_glass_care%]",
|
||||
"dishcare_dishwasher_program_night_wash": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_night_wash%]",
|
||||
"dishcare_dishwasher_program_quick_65": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_quick_65%]",
|
||||
"dishcare_dishwasher_program_normal_45": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_normal_45%]",
|
||||
"dishcare_dishwasher_program_intensiv_45": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_intensiv_45%]",
|
||||
"dishcare_dishwasher_program_auto_half_load": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_auto_half_load%]",
|
||||
"dishcare_dishwasher_program_intensiv_power": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_intensiv_power%]",
|
||||
"dishcare_dishwasher_program_magic_daily": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_magic_daily%]",
|
||||
"dishcare_dishwasher_program_super_60": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_super_60%]",
|
||||
"dishcare_dishwasher_program_kurz_60": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_kurz_60%]",
|
||||
"dishcare_dishwasher_program_express_sparkle_65": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_express_sparkle_65%]",
|
||||
"dishcare_dishwasher_program_machine_care": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_machine_care%]",
|
||||
"dishcare_dishwasher_program_steam_fresh": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_steam_fresh%]",
|
||||
"dishcare_dishwasher_program_maximum_cleaning": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_maximum_cleaning%]",
|
||||
"dishcare_dishwasher_program_mixed_load": "[%key:component::home_connect::entity::select::selected_program::state::dishcare_dishwasher_program_mixed_load%]",
|
||||
"laundry_care_dryer_program_cotton": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_cotton%]",
|
||||
"laundry_care_dryer_program_synthetic": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_synthetic%]",
|
||||
"laundry_care_dryer_program_mix": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_mix%]",
|
||||
"laundry_care_dryer_program_blankets": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_blankets%]",
|
||||
"laundry_care_dryer_program_business_shirts": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_business_shirts%]",
|
||||
"laundry_care_dryer_program_down_feathers": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_down_feathers%]",
|
||||
"laundry_care_dryer_program_hygiene": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_hygiene%]",
|
||||
"laundry_care_dryer_program_jeans": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_jeans%]",
|
||||
"laundry_care_dryer_program_outdoor": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_outdoor%]",
|
||||
"laundry_care_dryer_program_synthetic_refresh": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_synthetic_refresh%]",
|
||||
"laundry_care_dryer_program_towels": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_towels%]",
|
||||
"laundry_care_dryer_program_delicates": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_delicates%]",
|
||||
"laundry_care_dryer_program_super_40": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_super_40%]",
|
||||
"laundry_care_dryer_program_shirts_15": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_shirts_15%]",
|
||||
"laundry_care_dryer_program_pillow": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_pillow%]",
|
||||
"laundry_care_dryer_program_anti_shrink": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_anti_shrink%]",
|
||||
"laundry_care_dryer_program_my_time_my_drying_time": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_my_time_my_drying_time%]",
|
||||
"laundry_care_dryer_program_time_cold": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_cold%]",
|
||||
"laundry_care_dryer_program_time_warm": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_warm%]",
|
||||
"laundry_care_dryer_program_in_basket": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_in_basket%]",
|
||||
"laundry_care_dryer_program_time_cold_fix_time_cold_20": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_cold_fix_time_cold_20%]",
|
||||
"laundry_care_dryer_program_time_cold_fix_time_cold_30": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_cold_fix_time_cold_30%]",
|
||||
"laundry_care_dryer_program_time_cold_fix_time_cold_60": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_cold_fix_time_cold_60%]",
|
||||
"laundry_care_dryer_program_time_warm_fix_time_warm_30": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_warm_fix_time_warm_30%]",
|
||||
"laundry_care_dryer_program_time_warm_fix_time_warm_40": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_warm_fix_time_warm_40%]",
|
||||
"laundry_care_dryer_program_time_warm_fix_time_warm_60": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_time_warm_fix_time_warm_60%]",
|
||||
"laundry_care_dryer_program_dessous": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_dryer_program_dessous%]",
|
||||
"cooking_common_program_hood_automatic": "[%key:component::home_connect::entity::select::selected_program::state::cooking_common_program_hood_automatic%]",
|
||||
"cooking_common_program_hood_venting": "[%key:component::home_connect::entity::select::selected_program::state::cooking_common_program_hood_venting%]",
|
||||
"cooking_common_program_hood_delayed_shut_off": "[%key:component::home_connect::entity::select::selected_program::state::cooking_common_program_hood_delayed_shut_off%]",
|
||||
"cooking_oven_program_heating_mode_pre_heating": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_pre_heating%]",
|
||||
"cooking_oven_program_heating_mode_hot_air": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_eco": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_eco%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_grilling": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_grilling%]",
|
||||
"cooking_oven_program_heating_mode_top_bottom_heating": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_top_bottom_heating%]",
|
||||
"cooking_oven_program_heating_mode_top_bottom_heating_eco": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_top_bottom_heating_eco%]",
|
||||
"cooking_oven_program_heating_mode_bottom_heating": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_bottom_heating%]",
|
||||
"cooking_oven_program_heating_mode_pizza_setting": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_pizza_setting%]",
|
||||
"cooking_oven_program_heating_mode_slow_cook": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_slow_cook%]",
|
||||
"cooking_oven_program_heating_mode_intensive_heat": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_intensive_heat%]",
|
||||
"cooking_oven_program_heating_mode_keep_warm": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_keep_warm%]",
|
||||
"cooking_oven_program_heating_mode_preheat_ovenware": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_preheat_ovenware%]",
|
||||
"cooking_oven_program_heating_mode_frozen_heatup_special": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_frozen_heatup_special%]",
|
||||
"cooking_oven_program_heating_mode_desiccation": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_desiccation%]",
|
||||
"cooking_oven_program_heating_mode_defrost": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_defrost%]",
|
||||
"cooking_oven_program_heating_mode_proof": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_proof%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_30_steam": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_30_steam%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_60_steam": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_60_steam%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_80_steam": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_80_steam%]",
|
||||
"cooking_oven_program_heating_mode_hot_air_100_steam": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_hot_air_100_steam%]",
|
||||
"cooking_oven_program_heating_mode_sabbath_programme": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_sabbath_programme%]",
|
||||
"cooking_oven_program_microwave_90_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_90_watt%]",
|
||||
"cooking_oven_program_microwave_180_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_180_watt%]",
|
||||
"cooking_oven_program_microwave_360_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_360_watt%]",
|
||||
"cooking_oven_program_microwave_600_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_600_watt%]",
|
||||
"cooking_oven_program_microwave_900_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_900_watt%]",
|
||||
"cooking_oven_program_microwave_1000_watt": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_1000_watt%]",
|
||||
"cooking_oven_program_microwave_max": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_microwave_max%]",
|
||||
"cooking_oven_program_heating_mode_warming_drawer": "[%key:component::home_connect::entity::select::selected_program::state::cooking_oven_program_heating_mode_warming_drawer%]",
|
||||
"laundry_care_washer_program_cotton": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_cotton%]",
|
||||
"laundry_care_washer_program_cotton_cotton_eco": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_cotton_cotton_eco%]",
|
||||
"laundry_care_washer_program_cotton_eco_4060": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_cotton_eco_4060%]",
|
||||
"laundry_care_washer_program_cotton_colour": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_cotton_colour%]",
|
||||
"laundry_care_washer_program_easy_care": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_easy_care%]",
|
||||
"laundry_care_washer_program_mix": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_mix%]",
|
||||
"laundry_care_washer_program_mix_night_wash": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_mix_night_wash%]",
|
||||
"laundry_care_washer_program_delicates_silk": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_delicates_silk%]",
|
||||
"laundry_care_washer_program_wool": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_wool%]",
|
||||
"laundry_care_washer_program_sensitive": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_sensitive%]",
|
||||
"laundry_care_washer_program_auto_30": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_auto_30%]",
|
||||
"laundry_care_washer_program_auto_40": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_auto_40%]",
|
||||
"laundry_care_washer_program_auto_60": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_auto_60%]",
|
||||
"laundry_care_washer_program_chiffon": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_chiffon%]",
|
||||
"laundry_care_washer_program_curtains": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_curtains%]",
|
||||
"laundry_care_washer_program_dark_wash": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_dark_wash%]",
|
||||
"laundry_care_washer_program_dessous": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_dessous%]",
|
||||
"laundry_care_washer_program_monsoon": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_monsoon%]",
|
||||
"laundry_care_washer_program_outdoor": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_outdoor%]",
|
||||
"laundry_care_washer_program_plush_toy": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_plush_toy%]",
|
||||
"laundry_care_washer_program_shirts_blouses": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_shirts_blouses%]",
|
||||
"laundry_care_washer_program_sport_fitness": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_sport_fitness%]",
|
||||
"laundry_care_washer_program_towels": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_towels%]",
|
||||
"laundry_care_washer_program_water_proof": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_water_proof%]",
|
||||
"laundry_care_washer_program_power_speed_59": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_power_speed_59%]",
|
||||
"laundry_care_washer_program_super_153045_super_15": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_super_153045_super_15%]",
|
||||
"laundry_care_washer_program_super_153045_super_1530": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_super_153045_super_1530%]",
|
||||
"laundry_care_washer_program_down_duvet_duvet": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_down_duvet_duvet%]",
|
||||
"laundry_care_washer_program_rinse_rinse_spin_drain": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_rinse_rinse_spin_drain%]",
|
||||
"laundry_care_washer_program_drum_clean": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_program_drum_clean%]",
|
||||
"laundry_care_washer_dryer_program_cotton": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_cotton%]",
|
||||
"laundry_care_washer_dryer_program_cotton_eco_4060": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_cotton_eco_4060%]",
|
||||
"laundry_care_washer_dryer_program_mix": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_mix%]",
|
||||
"laundry_care_washer_dryer_program_easy_care": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_easy_care%]",
|
||||
"laundry_care_washer_dryer_program_wash_and_dry_60": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_wash_and_dry_60%]",
|
||||
"laundry_care_washer_dryer_program_wash_and_dry_90": "[%key:component::home_connect::entity::select::selected_program::state::laundry_care_washer_dryer_program_wash_and_dry_90%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"program_progress": {
|
||||
"name": "Program progress"
|
||||
|
||||
@@ -8,12 +8,11 @@ from homeconnect.api import HomeConnectError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import HomeConnectConfigEntry, get_dict_from_home_connect_error
|
||||
from .const import (
|
||||
APPLIANCES_WITH_PROGRAMS,
|
||||
ATTR_ALLOWED_VALUES,
|
||||
ATTR_CONSTRAINTS,
|
||||
ATTR_VALUE,
|
||||
@@ -37,6 +36,18 @@ from .entity import HomeConnectDevice, HomeConnectEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
APPLIANCES_WITH_PROGRAMS = (
|
||||
"CleaningRobot",
|
||||
"CoffeeMaker",
|
||||
"Dishwasher",
|
||||
"Dryer",
|
||||
"Hood",
|
||||
"Oven",
|
||||
"WarmingDrawer",
|
||||
"Washer",
|
||||
"WasherDryer",
|
||||
)
|
||||
|
||||
|
||||
SWITCHES = (
|
||||
SwitchEntityDescription(
|
||||
@@ -134,7 +145,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
self._attr_available = False
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="turn_on",
|
||||
translation_placeholders={
|
||||
@@ -158,7 +169,7 @@ class HomeConnectSwitch(HomeConnectEntity, SwitchEntity):
|
||||
except HomeConnectError as err:
|
||||
_LOGGER.error("Error while trying to turn off: %s", err)
|
||||
self._attr_available = False
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="turn_off",
|
||||
translation_placeholders={
|
||||
@@ -209,7 +220,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
self.device.appliance.start_program, self.program_name
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="start_program",
|
||||
translation_placeholders={
|
||||
@@ -225,7 +236,7 @@ class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity):
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self.device.appliance.stop_program)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="stop_program",
|
||||
translation_placeholders={
|
||||
@@ -278,7 +289,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
self._attr_is_on = False
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="power_on",
|
||||
translation_placeholders={
|
||||
@@ -291,7 +302,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Switch the device off."""
|
||||
if not hasattr(self, "power_off_state"):
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unable_to_retrieve_turn_off",
|
||||
translation_placeholders={
|
||||
@@ -300,7 +311,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
|
||||
)
|
||||
|
||||
if self.power_off_state is None:
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="turn_off_not_supported",
|
||||
translation_placeholders={
|
||||
@@ -316,7 +327,7 @@ class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity):
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
self._attr_is_on = True
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="power_off",
|
||||
translation_placeholders={
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeconnect.api import HomeConnectError
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import HomeConnectConfigEntry, get_dict_from_home_connect_error
|
||||
@@ -80,7 +80,7 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
|
||||
time_to_seconds(value),
|
||||
)
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_setting",
|
||||
translation_placeholders={
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.7"],
|
||||
"requirements": ["aiohomekit==3.2.6"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -9,15 +9,13 @@ from typing import Any, NamedTuple
|
||||
from homewizard_energy import HomeWizardEnergyV1
|
||||
from homewizard_energy.errors import DisabledError, RequestError, UnsupportedError
|
||||
from homewizard_energy.v1.models import Device
|
||||
import voluptuous as vol
|
||||
from voluptuous import Required, Schema
|
||||
|
||||
from homeassistant.components import onboarding, zeroconf
|
||||
from homeassistant.components.dhcp import DhcpServiceInfo
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PATH
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.selector import TextSelector
|
||||
|
||||
from .const import (
|
||||
CONF_API_ENABLED,
|
||||
@@ -70,11 +68,11 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
user_input = user_input or {}
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
data_schema=Schema(
|
||||
{
|
||||
vol.Required(
|
||||
Required(
|
||||
CONF_IP_ADDRESS, default=user_input.get(CONF_IP_ADDRESS)
|
||||
): TextSelector(),
|
||||
): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
@@ -112,32 +110,6 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle dhcp discovery to update existing entries.
|
||||
|
||||
This flow is triggered only by DHCP discovery of known devices.
|
||||
"""
|
||||
try:
|
||||
device = await self._async_try_connect(discovery_info.ip)
|
||||
except RecoverableError as ex:
|
||||
_LOGGER.error(ex)
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
await self.async_set_unique_id(
|
||||
f"{device.product_type}_{discovery_info.macaddress}"
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_IP_ADDRESS: discovery_info.ip}
|
||||
)
|
||||
|
||||
# This situation should never happen, as Home Assistant will only
|
||||
# send updates for existing entries. In case it does, we'll just
|
||||
# abort the flow with an unknown error.
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -198,43 +170,6 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self.async_show_form(step_id="reauth_confirm", errors=errors)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of the integration."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input:
|
||||
try:
|
||||
device_info = await self._async_try_connect(user_input[CONF_IP_ADDRESS])
|
||||
except RecoverableError as ex:
|
||||
_LOGGER.error(ex)
|
||||
errors = {"base": ex.error_code}
|
||||
else:
|
||||
await self.async_set_unique_id(
|
||||
f"{device_info.product_type}_{device_info.serial}"
|
||||
)
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_device")
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates=user_input,
|
||||
)
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_IP_ADDRESS,
|
||||
default=reconfigure_entry.data.get(CONF_IP_ADDRESS),
|
||||
): TextSelector(),
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"title": reconfigure_entry.title,
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _async_try_connect(ip_address: str) -> Device:
|
||||
"""Try to connect.
|
||||
|
||||
@@ -3,15 +3,9 @@
|
||||
"name": "HomeWizard Energy",
|
||||
"codeowners": ["@DCSBL"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
"registered_devices": true
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/homewizard",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["homewizard_energy"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-homewizard-energy==v7.0.0"],
|
||||
"zeroconf": ["_hwenergy._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -46,7 +46,11 @@ rules:
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery-update-info:
|
||||
status: todo
|
||||
comment: |
|
||||
The integration doesn't update the device info based on DHCP discovery
|
||||
of known existing devices.
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
@@ -65,7 +69,7 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
|
||||
@@ -17,15 +17,6 @@
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The local API is disabled. Go to the HomeWizard Energy app and enable the API in the device settings."
|
||||
},
|
||||
"reconfigure": {
|
||||
"description": "Update configuration for {title}.",
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]"
|
||||
},
|
||||
"data_description": {
|
||||
"ip_address": "[%key:component::homewizard::config::step::user::data_description::ip_address%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -38,9 +29,7 @@
|
||||
"device_not_supported": "This device is not supported",
|
||||
"unknown_error": "[%key:common::config_flow::error::unknown%]",
|
||||
"unsupported_api_version": "Detected unsupported API version",
|
||||
"reauth_successful": "Enabling API was successful",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"wrong_device": "The configured device is not the same found on this IP address."
|
||||
"reauth_successful": "Enabling API was successful"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -326,8 +326,7 @@ class HomeAssistantApplication(web.Application):
|
||||
protocol,
|
||||
writer,
|
||||
task,
|
||||
# loop will never be None when called from aiohttp
|
||||
loop=self._loop, # type: ignore[arg-type]
|
||||
loop=self._loop,
|
||||
client_max_size=self._client_max_size,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import StrEnum
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, final
|
||||
|
||||
@@ -21,6 +22,11 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.deprecation import (
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from homeassistant.helpers.entity import ToggleEntity, ToggleEntityDescription
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -28,6 +34,9 @@ from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import ( # noqa: F401
|
||||
_DEPRECATED_DEVICE_CLASS_DEHUMIDIFIER,
|
||||
_DEPRECATED_DEVICE_CLASS_HUMIDIFIER,
|
||||
_DEPRECATED_SUPPORT_MODES,
|
||||
ATTR_ACTION,
|
||||
ATTR_AVAILABLE_MODES,
|
||||
ATTR_CURRENT_HUMIDITY,
|
||||
@@ -305,3 +314,13 @@ async def async_service_humidity_set(
|
||||
)
|
||||
|
||||
await entity.async_set_humidity(humidity)
|
||||
|
||||
|
||||
# As we import deprecated constants from the const module, we need to add these two functions
|
||||
# otherwise this module will be logged for using deprecated constants and not the custom component
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
"""Provides the constants needed for component."""
|
||||
|
||||
from enum import IntFlag, StrEnum
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedConstant,
|
||||
DeprecatedConstantEnum,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
|
||||
MODE_NORMAL = "normal"
|
||||
MODE_ECO = "eco"
|
||||
@@ -34,6 +43,15 @@ DEFAULT_MAX_HUMIDITY = 100
|
||||
|
||||
DOMAIN = "humidifier"
|
||||
|
||||
# DEVICE_CLASS_* below are deprecated as of 2021.12
|
||||
# use the HumidifierDeviceClass enum instead.
|
||||
_DEPRECATED_DEVICE_CLASS_HUMIDIFIER = DeprecatedConstant(
|
||||
"humidifier", "HumidifierDeviceClass.HUMIDIFIER", "2025.1"
|
||||
)
|
||||
_DEPRECATED_DEVICE_CLASS_DEHUMIDIFIER = DeprecatedConstant(
|
||||
"dehumidifier", "HumidifierDeviceClass.DEHUMIDIFIER", "2025.1"
|
||||
)
|
||||
|
||||
SERVICE_SET_MODE = "set_mode"
|
||||
SERVICE_SET_HUMIDITY = "set_humidity"
|
||||
|
||||
@@ -42,3 +60,17 @@ class HumidifierEntityFeature(IntFlag):
|
||||
"""Supported features of the humidifier entity."""
|
||||
|
||||
MODES = 1
|
||||
|
||||
|
||||
# The SUPPORT_MODES constant is deprecated as of Home Assistant 2022.5.
|
||||
# Please use the HumidifierEntityFeature enum instead.
|
||||
_DEPRECATED_SUPPORT_MODES = DeprecatedConstantEnum(
|
||||
HumidifierEntityFeature.MODES, "2025.1"
|
||||
)
|
||||
|
||||
# These can be removed if no deprecated constant are in this module anymore
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
|
||||
@@ -22,8 +22,6 @@ from .entity import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AutomowerButtonEntityDescription(ButtonEntityDescription):
|
||||
|
||||
@@ -22,10 +22,6 @@ from .const import DOMAIN
|
||||
from .coordinator import AutomowerDataUpdateCoordinator
|
||||
from .entity import AutomowerAvailableEntity, handle_sending_exception
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
DOCKED_ACTIVITIES = (MowerActivities.PARKED_IN_CS, MowerActivities.CHARGING)
|
||||
MOWING_ACTIVITIES = (
|
||||
MowerActivities.MOWING,
|
||||
@@ -46,6 +42,9 @@ PARK = "park"
|
||||
OVERRIDE_MODES = [MOW, PARK]
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AutomowerConfigEntry,
|
||||
|
||||
@@ -24,8 +24,6 @@ from .entity import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@callback
|
||||
def _async_get_cutting_height(data: MowerAttributes) -> int:
|
||||
|
||||
@@ -16,7 +16,6 @@ from .entity import AutomowerControlEntity, handle_sending_exception
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
HEADLIGHT_MODES: list = [
|
||||
HeadlightModes.ALWAYS_OFF.lower(),
|
||||
|
||||
@@ -349,7 +349,6 @@ MOWER_SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = (
|
||||
key="number_of_collisions",
|
||||
translation_key="number_of_collisions",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
exists_fn=lambda data: data.statistics.number_of_collisions is not None,
|
||||
value_fn=attrgetter("statistics.number_of_collisions"),
|
||||
|
||||
@@ -19,8 +19,6 @@ from .entity import (
|
||||
handle_sending_exception,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN, TIMER_DATA
|
||||
from .timers import (
|
||||
CancelAllTimersIntentHandler,
|
||||
CancelTimerIntentHandler,
|
||||
DecreaseTimerIntentHandler,
|
||||
IncreaseTimerIntentHandler,
|
||||
@@ -131,7 +130,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
intent.async_register(hass, SetPositionIntentHandler())
|
||||
intent.async_register(hass, StartTimerIntentHandler())
|
||||
intent.async_register(hass, CancelTimerIntentHandler())
|
||||
intent.async_register(hass, CancelAllTimersIntentHandler())
|
||||
intent.async_register(hass, IncreaseTimerIntentHandler())
|
||||
intent.async_register(hass, DecreaseTimerIntentHandler())
|
||||
intent.async_register(hass, PauseTimerIntentHandler())
|
||||
@@ -364,7 +362,7 @@ class NevermindIntentHandler(intent.IntentHandler):
|
||||
"""Takes no action."""
|
||||
|
||||
intent_type = intent.INTENT_NEVERMIND
|
||||
description = "Cancel the current conversation if it was started by mistake or the user wants it to stop."
|
||||
description = "Cancels the current request and does nothing"
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
"""Do nothing and produces an empty response."""
|
||||
|
||||
@@ -887,36 +887,6 @@ class CancelTimerIntentHandler(intent.IntentHandler):
|
||||
return intent_obj.create_response()
|
||||
|
||||
|
||||
class CancelAllTimersIntentHandler(intent.IntentHandler):
|
||||
"""Intent handler for cancelling all timers."""
|
||||
|
||||
intent_type = intent.INTENT_CANCEL_ALL_TIMERS
|
||||
description = "Cancels all timers"
|
||||
slot_schema = {
|
||||
vol.Optional("area"): cv.string,
|
||||
}
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
"""Handle the intent."""
|
||||
hass = intent_obj.hass
|
||||
timer_manager: TimerManager = hass.data[TIMER_DATA]
|
||||
slots = self.async_validate_slots(intent_obj.slots)
|
||||
canceled = 0
|
||||
|
||||
for timer in _find_timers(hass, intent_obj.device_id, slots):
|
||||
timer_manager.cancel_timer(timer.id)
|
||||
canceled += 1
|
||||
|
||||
response = intent_obj.create_response()
|
||||
speech_slots = {"canceled": canceled}
|
||||
if "area" in slots:
|
||||
speech_slots["area"] = slots["area"]["value"]
|
||||
|
||||
response.async_set_speech_slots(speech_slots)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class IncreaseTimerIntentHandler(intent.IntentHandler):
|
||||
"""Intent handler for increasing the time of a timer."""
|
||||
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/iron_os",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pynecil", "aiogithubapi"],
|
||||
"requirements": ["pynecil==1.0.1", "aiogithubapi==24.6.0"]
|
||||
"requirements": ["pynecil==0.2.1", "aiogithubapi==24.6.0"]
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ from . import IronOSConfigEntry
|
||||
from .const import DOMAIN, MAX_TEMP, MIN_TEMP
|
||||
from .entity import IronOSBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class IronOSNumberEntityDescription(NumberEntityDescription):
|
||||
|
||||
@@ -107,7 +107,6 @@ PINECIL_SENSOR_DESCRIPTIONS: tuple[IronOSSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=OHM,
|
||||
value_fn=lambda data: data.tip_resistance,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
IronOSSensorEntityDescription(
|
||||
key=PinecilSensor.UPTIME,
|
||||
@@ -138,7 +137,7 @@ PINECIL_SENSOR_DESCRIPTIONS: tuple[IronOSSensorEntityDescription, ...] = (
|
||||
IronOSSensorEntityDescription(
|
||||
key=PinecilSensor.TIP_VOLTAGE,
|
||||
translation_key=PinecilSensor.TIP_VOLTAGE,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.MICROVOLT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=3,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user