mirror of
https://github.com/home-assistant/core.git
synced 2025-09-07 05:41:32 +02:00
Merge branch 'dev_target_triggers_conditions' of github.com:home-assistant/core into target_trigger
This commit is contained in:
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -175,7 +175,7 @@ jobs:
|
|||||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -462,7 +462,7 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -970,7 +970,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||||
- name: Download pytest_buckets
|
- name: Download pytest_buckets
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
- name: Compile English translations
|
- name: Compile English translations
|
||||||
@@ -1336,7 +1336,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
@@ -1486,7 +1486,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
@@ -1511,7 +1511,7 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
pattern: test-results-*
|
pattern: test-results-*
|
||||||
- name: Upload test results to Codecov
|
- name: Upload test results to Codecov
|
||||||
|
@@ -231,7 +231,7 @@ jobs:
|
|||||||
- name: Detect duplicates using AI
|
- name: Detect duplicates using AI
|
||||||
id: ai_detection
|
id: ai_detection
|
||||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||||
uses: actions/ai-inference@v1.2.7
|
uses: actions/ai-inference@v1.2.8
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Detect language using AI
|
- name: Detect language using AI
|
||||||
id: ai_language_detection
|
id: ai_language_detection
|
||||||
if: steps.detect_language.outputs.should_continue == 'true'
|
if: steps.detect_language.outputs.should_continue == 'true'
|
||||||
uses: actions/ai-inference@v1.2.7
|
uses: actions/ai-inference@v1.2.8
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
14
.github/workflows/wheels.yml
vendored
14
.github/workflows/wheels.yml
vendored
@@ -138,17 +138,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download build_constraints
|
- name: Download build_constraints
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: build_constraints
|
name: build_constraints
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
@@ -187,22 +187,22 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download build_constraints
|
- name: Download build_constraints
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: build_constraints
|
name: build_constraints
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
- name: Download requirements_all_wheels
|
- name: Download requirements_all_wheels
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
|
|
||||||
|
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1613,8 +1613,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tplink_omada/ @MarkGodwin
|
/tests/components/tplink_omada/ @MarkGodwin
|
||||||
/homeassistant/components/traccar/ @ludeeus
|
/homeassistant/components/traccar/ @ludeeus
|
||||||
/tests/components/traccar/ @ludeeus
|
/tests/components/traccar/ @ludeeus
|
||||||
/homeassistant/components/traccar_server/ @ludeeus
|
|
||||||
/tests/components/traccar_server/ @ludeeus
|
|
||||||
/homeassistant/components/trace/ @home-assistant/core
|
/homeassistant/components/trace/ @home-assistant/core
|
||||||
/tests/components/trace/ @home-assistant/core
|
/tests/components/trace/ @home-assistant/core
|
||||||
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu
|
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu
|
||||||
|
@@ -9,7 +9,6 @@ DOMAIN: Final = "amberelectric"
|
|||||||
CONF_SITE_NAME = "site_name"
|
CONF_SITE_NAME = "site_name"
|
||||||
CONF_SITE_ID = "site_id"
|
CONF_SITE_ID = "site_id"
|
||||||
|
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
ATTR_CHANNEL_TYPE = "channel_type"
|
ATTR_CHANNEL_TYPE = "channel_type"
|
||||||
|
|
||||||
ATTRIBUTION = "Data provided by Amber Electric"
|
ATTRIBUTION = "Data provided by Amber Electric"
|
||||||
|
@@ -4,6 +4,7 @@ from amberelectric.models.channel import ChannelType
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall,
|
ServiceCall,
|
||||||
@@ -16,7 +17,6 @@ from homeassistant.util.json import JsonValueType
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_CHANNEL_TYPE,
|
ATTR_CHANNEL_TYPE,
|
||||||
ATTR_CONFIG_ENTRY_ID,
|
|
||||||
CONTROLLED_LOAD_CHANNEL,
|
CONTROLLED_LOAD_CHANNEL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
FEED_IN_CHANNEL,
|
FEED_IN_CHANNEL,
|
||||||
|
@@ -81,11 +81,15 @@ async def async_update_options(
|
|||||||
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||||
"""Migrate integration entry structure."""
|
"""Migrate integration entry structure."""
|
||||||
|
|
||||||
entries = hass.config_entries.async_entries(DOMAIN)
|
# Make sure we get enabled config entries first
|
||||||
|
entries = sorted(
|
||||||
|
hass.config_entries.async_entries(DOMAIN),
|
||||||
|
key=lambda e: e.disabled_by is not None,
|
||||||
|
)
|
||||||
if not any(entry.version == 1 for entry in entries):
|
if not any(entry.version == 1 for entry in entries):
|
||||||
return
|
return
|
||||||
|
|
||||||
api_keys_entries: dict[str, ConfigEntry] = {}
|
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
@@ -99,30 +103,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
||||||
use_existing = True
|
use_existing = True
|
||||||
api_keys_entries[entry.data[CONF_API_KEY]] = entry
|
all_disabled = all(
|
||||||
|
e.disabled_by is not None
|
||||||
|
for e in entries
|
||||||
|
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
|
||||||
|
)
|
||||||
|
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
|
||||||
|
|
||||||
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
|
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||||
|
|
||||||
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
||||||
conversation_entity = entity_registry.async_get_entity_id(
|
conversation_entity_id = entity_registry.async_get_entity_id(
|
||||||
"conversation",
|
"conversation",
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
)
|
)
|
||||||
if conversation_entity is not None:
|
|
||||||
entity_registry.async_update_entity(
|
|
||||||
conversation_entity,
|
|
||||||
config_entry_id=parent_entry.entry_id,
|
|
||||||
config_subentry_id=subentry.subentry_id,
|
|
||||||
new_unique_id=subentry.subentry_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
device = device_registry.async_get_device(
|
device = device_registry.async_get_device(
|
||||||
identifiers={(DOMAIN, entry.entry_id)}
|
identifiers={(DOMAIN, entry.entry_id)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if conversation_entity_id is not None:
|
||||||
|
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
|
||||||
|
entity_disabled_by = conversation_entity_entry.disabled_by
|
||||||
|
if (
|
||||||
|
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
|
||||||
|
and not all_disabled
|
||||||
|
):
|
||||||
|
# Device and entity registries don't update the disabled_by flag
|
||||||
|
# when moving a device or entity from one config entry to another,
|
||||||
|
# so we need to do it manually.
|
||||||
|
entity_disabled_by = (
|
||||||
|
er.RegistryEntryDisabler.DEVICE
|
||||||
|
if device
|
||||||
|
else er.RegistryEntryDisabler.USER
|
||||||
|
)
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
conversation_entity_id,
|
||||||
|
config_entry_id=parent_entry.entry_id,
|
||||||
|
config_subentry_id=subentry.subentry_id,
|
||||||
|
disabled_by=entity_disabled_by,
|
||||||
|
new_unique_id=subentry.subentry_id,
|
||||||
|
)
|
||||||
|
|
||||||
if device is not None:
|
if device is not None:
|
||||||
|
# Device and entity registries don't update the disabled_by flag when
|
||||||
|
# moving a device or entity from one config entry to another, so we
|
||||||
|
# need to do it manually.
|
||||||
|
device_disabled_by = device.disabled_by
|
||||||
|
if (
|
||||||
|
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
|
||||||
|
and not all_disabled
|
||||||
|
):
|
||||||
|
device_disabled_by = dr.DeviceEntryDisabler.USER
|
||||||
device_registry.async_update_device(
|
device_registry.async_update_device(
|
||||||
device.id,
|
device.id,
|
||||||
|
disabled_by=device_disabled_by,
|
||||||
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
||||||
add_config_subentry_id=subentry.subentry_id,
|
add_config_subentry_id=subentry.subentry_id,
|
||||||
add_config_entry_id=parent_entry.entry_id,
|
add_config_entry_id=parent_entry.entry_id,
|
||||||
@@ -147,7 +182,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
|||||||
title=DEFAULT_CONVERSATION_NAME,
|
title=DEFAULT_CONVERSATION_NAME,
|
||||||
options={},
|
options={},
|
||||||
version=2,
|
version=2,
|
||||||
minor_version=2,
|
minor_version=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -173,6 +208,38 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
|
|||||||
|
|
||||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||||
|
|
||||||
|
if entry.version == 2 and entry.minor_version == 2:
|
||||||
|
# Fix migration where the disabled_by flag was not set correctly.
|
||||||
|
# We can currently only correct this for enabled config entries,
|
||||||
|
# because migration does not run for disabled config entries. This
|
||||||
|
# is asserted in tests, and if that behavior is changed, we should
|
||||||
|
# correct also disabled config entries.
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||||
|
entity_entries = er.async_entries_for_config_entry(
|
||||||
|
entity_registry, entry.entry_id
|
||||||
|
)
|
||||||
|
if entry.disabled_by is None:
|
||||||
|
# If the config entry is not disabled, we need to set the disabled_by
|
||||||
|
# flag on devices to USER, and on entities to DEVICE, if they are set
|
||||||
|
# to CONFIG_ENTRY.
|
||||||
|
for device in devices:
|
||||||
|
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
|
||||||
|
continue
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device.id,
|
||||||
|
disabled_by=dr.DeviceEntryDisabler.USER,
|
||||||
|
)
|
||||||
|
for entity in entity_entries:
|
||||||
|
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
|
||||||
|
continue
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
entity.entity_id,
|
||||||
|
disabled_by=er.RegistryEntryDisabler.DEVICE,
|
||||||
|
)
|
||||||
|
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||||
|
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||||
)
|
)
|
||||||
|
@@ -75,7 +75,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for Anthropic."""
|
"""Handle a config flow for Anthropic."""
|
||||||
|
|
||||||
VERSION = 2
|
VERSION = 2
|
||||||
MINOR_VERSION = 2
|
MINOR_VERSION = 3
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
@@ -28,5 +28,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pubnub", "yalexs"],
|
"loggers": ["pubnub", "yalexs"],
|
||||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"]
|
"requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"]
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["axis"],
|
"loggers": ["axis"],
|
||||||
"requirements": ["axis==64"],
|
"requirements": ["axis==65"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"manufacturer": "AXIS"
|
"manufacturer": "AXIS"
|
||||||
|
@@ -25,7 +25,6 @@ SERVICE_TRIGGER = "trigger_camera"
|
|||||||
SERVICE_SAVE_VIDEO = "save_video"
|
SERVICE_SAVE_VIDEO = "save_video"
|
||||||
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
|
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
|
||||||
SERVICE_SEND_PIN = "send_pin"
|
SERVICE_SEND_PIN = "send_pin"
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
|
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
Platform.ALARM_CONTROL_PANEL,
|
Platform.ALARM_CONTROL_PANEL,
|
||||||
|
@@ -5,12 +5,12 @@ from __future__ import annotations
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import CONF_PIN
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_PIN
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_SEND_PIN
|
from .const import DOMAIN, SERVICE_SEND_PIN
|
||||||
from .coordinator import BlinkConfigEntry
|
from .coordinator import BlinkConfigEntry
|
||||||
|
|
||||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||||
|
@@ -21,6 +21,6 @@
|
|||||||
"bluetooth-auto-recovery==1.5.2",
|
"bluetooth-auto-recovery==1.5.2",
|
||||||
"bluetooth-data-tools==1.28.2",
|
"bluetooth-data-tools==1.28.2",
|
||||||
"dbus-fast==2.44.3",
|
"dbus-fast==2.44.3",
|
||||||
"habluetooth==4.0.1"
|
"habluetooth==4.0.2"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -6,4 +6,3 @@ CONF_INSTALLER_CODE = "installer_code"
|
|||||||
CONF_USER_CODE = "user_code"
|
CONF_USER_CODE = "user_code"
|
||||||
ATTR_DATETIME = "datetime"
|
ATTR_DATETIME = "datetime"
|
||||||
SERVICE_SET_DATE_TIME = "set_date_time"
|
SERVICE_SET_DATE_TIME = "set_date_time"
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
|
@@ -9,12 +9,13 @@ from typing import Any
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
|
from .const import ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
|
||||||
from .types import BoschAlarmConfigEntry
|
from .types import BoschAlarmConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
@@ -100,16 +100,10 @@ set_hvac_mode:
|
|||||||
fields:
|
fields:
|
||||||
hvac_mode:
|
hvac_mode:
|
||||||
selector:
|
selector:
|
||||||
select:
|
state:
|
||||||
options:
|
hide_states:
|
||||||
- "off"
|
- unavailable
|
||||||
- "auto"
|
- unknown
|
||||||
- "cool"
|
|
||||||
- "dry"
|
|
||||||
- "fan_only"
|
|
||||||
- "heat_cool"
|
|
||||||
- "heat"
|
|
||||||
translation_key: hvac_mode
|
|
||||||
set_swing_mode:
|
set_swing_mode:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
|
@@ -13,6 +13,6 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||||
"requirements": ["hass-nabucasa==0.111.0"],
|
"requirements": ["hass-nabucasa==0.111.1"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@@ -61,7 +61,7 @@ class DeviceCondition(Condition):
|
|||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def async_validate_condition_config(
|
async def async_validate_config(
|
||||||
cls, hass: HomeAssistant, config: ConfigType
|
cls, hass: HomeAssistant, config: ConfigType
|
||||||
) -> ConfigType:
|
) -> ConfigType:
|
||||||
"""Validate device condition config."""
|
"""Validate device condition config."""
|
||||||
@@ -69,7 +69,7 @@ class DeviceCondition(Condition):
|
|||||||
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
|
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_condition_from_config(self) -> condition.ConditionCheckerType:
|
async def async_get_checker(self) -> condition.ConditionCheckerType:
|
||||||
"""Test a device condition."""
|
"""Test a device condition."""
|
||||||
platform = await async_get_device_automation_platform(
|
platform = await async_get_device_automation_platform(
|
||||||
self._hass, self._config[CONF_DOMAIN], DeviceAutomationType.CONDITION
|
self._hass, self._config[CONF_DOMAIN], DeviceAutomationType.CONDITION
|
||||||
@@ -80,7 +80,7 @@ class DeviceCondition(Condition):
|
|||||||
|
|
||||||
|
|
||||||
CONDITIONS: dict[str, type[Condition]] = {
|
CONDITIONS: dict[str, type[Condition]] = {
|
||||||
"device": DeviceCondition,
|
"_device": DeviceCondition,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -20,7 +20,6 @@ from homeassistant.const import Platform
|
|||||||
_LOGGER = logging.getLogger(__package__)
|
_LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
DOMAIN = "ecobee"
|
DOMAIN = "ecobee"
|
||||||
ATTR_CONFIG_ENTRY_ID = "entry_id"
|
|
||||||
ATTR_AVAILABLE_SENSORS = "available_sensors"
|
ATTR_AVAILABLE_SENSORS = "available_sensors"
|
||||||
ATTR_ACTIVE_SENSORS = "active_sensors"
|
ATTR_ACTIVE_SENSORS = "active_sensors"
|
||||||
|
|
||||||
|
@@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20250805.0"]
|
"requirements": ["home-assistant-frontend==20250806.0"]
|
||||||
}
|
}
|
||||||
|
@@ -86,9 +86,11 @@ UNSUPPORTED_REASONS = {
|
|||||||
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
|
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
|
||||||
UNHEALTHY_REASONS = {
|
UNHEALTHY_REASONS = {
|
||||||
"docker",
|
"docker",
|
||||||
"supervisor",
|
"duplicate_os_installation",
|
||||||
"setup",
|
"oserror_bad_message",
|
||||||
"privileged",
|
"privileged",
|
||||||
|
"setup",
|
||||||
|
"supervisor",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -117,35 +117,43 @@
|
|||||||
},
|
},
|
||||||
"unhealthy": {
|
"unhealthy": {
|
||||||
"title": "Unhealthy system - {reason}",
|
"title": "Unhealthy system - {reason}",
|
||||||
"description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this."
|
"description": "System is currently unhealthy due to {reason}. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unhealthy_docker": {
|
"unhealthy_docker": {
|
||||||
"title": "Unhealthy system - Docker misconfigured",
|
"title": "Unhealthy system - Docker misconfigured",
|
||||||
"description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this."
|
"description": "System is currently unhealthy because Docker is configured incorrectly. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unhealthy_supervisor": {
|
"unhealthy_duplicate_os_installation": {
|
||||||
"title": "Unhealthy system - Supervisor update failed",
|
"description": "System is currently unhealthy because it has detected multiple Home Assistant OS installations. For troubleshooting information, select Learn more.",
|
||||||
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this."
|
"title": "Unhealthy system - Duplicate Home Assistant OS installation"
|
||||||
},
|
},
|
||||||
"unhealthy_setup": {
|
"unhealthy_oserror_bad_message": {
|
||||||
"title": "Unhealthy system - Setup failed",
|
"description": "System is currently unhealthy because the operating system has reported an OS error: Bad message. For troubleshooting information, select Learn more.",
|
||||||
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this."
|
"title": "Unhealthy system - Operating System error: Bad message"
|
||||||
},
|
},
|
||||||
"unhealthy_privileged": {
|
"unhealthy_privileged": {
|
||||||
"title": "Unhealthy system - Not privileged",
|
"title": "Unhealthy system - Not privileged",
|
||||||
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this."
|
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. For troubleshooting information, select Learn more."
|
||||||
|
},
|
||||||
|
"unhealthy_setup": {
|
||||||
|
"title": "Unhealthy system - Setup failed",
|
||||||
|
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, For troubleshooting information, select Learn more."
|
||||||
|
},
|
||||||
|
"unhealthy_supervisor": {
|
||||||
|
"title": "Unhealthy system - Supervisor update failed",
|
||||||
|
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unhealthy_untrusted": {
|
"unhealthy_untrusted": {
|
||||||
"title": "Unhealthy system - Untrusted code",
|
"title": "Unhealthy system - Untrusted code",
|
||||||
"description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this."
|
"description": "System is currently unhealthy because it has detected untrusted code or images in use. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported": {
|
"unsupported": {
|
||||||
"title": "Unsupported system - {reason}",
|
"title": "Unsupported system - {reason}",
|
||||||
"description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this."
|
"description": "System is unsupported due to {reason}. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_apparmor": {
|
"unsupported_apparmor": {
|
||||||
"title": "Unsupported system - AppArmor issues",
|
"title": "Unsupported system - AppArmor issues",
|
||||||
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_cgroup_version": {
|
"unsupported_cgroup_version": {
|
||||||
"title": "Unsupported system - CGroup version",
|
"title": "Unsupported system - CGroup version",
|
||||||
@@ -153,23 +161,23 @@
|
|||||||
},
|
},
|
||||||
"unsupported_connectivity_check": {
|
"unsupported_connectivity_check": {
|
||||||
"title": "Unsupported system - Connectivity check disabled",
|
"title": "Unsupported system - Connectivity check disabled",
|
||||||
"description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because Home Assistant cannot determine when an Internet connection is available. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_content_trust": {
|
"unsupported_content_trust": {
|
||||||
"title": "Unsupported system - Content-trust check disabled",
|
"title": "Unsupported system - Content-trust check disabled",
|
||||||
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_dbus": {
|
"unsupported_dbus": {
|
||||||
"title": "Unsupported system - D-Bus issues",
|
"title": "Unsupported system - D-Bus issues",
|
||||||
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_dns_server": {
|
"unsupported_dns_server": {
|
||||||
"title": "Unsupported system - DNS server issues",
|
"title": "Unsupported system - DNS server issues",
|
||||||
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_docker_configuration": {
|
"unsupported_docker_configuration": {
|
||||||
"title": "Unsupported system - Docker misconfigured",
|
"title": "Unsupported system - Docker misconfigured",
|
||||||
"description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because the Docker daemon is running in an unexpected way. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_docker_version": {
|
"unsupported_docker_version": {
|
||||||
"title": "Unsupported system - Docker version",
|
"title": "Unsupported system - Docker version",
|
||||||
@@ -177,15 +185,15 @@
|
|||||||
},
|
},
|
||||||
"unsupported_job_conditions": {
|
"unsupported_job_conditions": {
|
||||||
"title": "Unsupported system - Protections disabled",
|
"title": "Unsupported system - Protections disabled",
|
||||||
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_lxc": {
|
"unsupported_lxc": {
|
||||||
"title": "Unsupported system - LXC detected",
|
"title": "Unsupported system - LXC detected",
|
||||||
"description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because it is being run in an LXC virtual machine. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_network_manager": {
|
"unsupported_network_manager": {
|
||||||
"title": "Unsupported system - Network Manager issues",
|
"title": "Unsupported system - Network Manager issues",
|
||||||
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_os": {
|
"unsupported_os": {
|
||||||
"title": "Unsupported system - Operating System",
|
"title": "Unsupported system - Operating System",
|
||||||
@@ -193,43 +201,43 @@
|
|||||||
},
|
},
|
||||||
"unsupported_os_agent": {
|
"unsupported_os_agent": {
|
||||||
"title": "Unsupported system - OS-Agent issues",
|
"title": "Unsupported system - OS-Agent issues",
|
||||||
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_restart_policy": {
|
"unsupported_restart_policy": {
|
||||||
"title": "Unsupported system - Container restart policy",
|
"title": "Unsupported system - Container restart policy",
|
||||||
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_software": {
|
"unsupported_software": {
|
||||||
"title": "Unsupported system - Unsupported software",
|
"title": "Unsupported system - Unsupported software",
|
||||||
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_source_mods": {
|
"unsupported_source_mods": {
|
||||||
"title": "Unsupported system - Supervisor source modifications",
|
"title": "Unsupported system - Supervisor source modifications",
|
||||||
"description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because Supervisor source code has been modified. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_supervisor_version": {
|
"unsupported_supervisor_version": {
|
||||||
"title": "Unsupported system - Supervisor version",
|
"title": "Unsupported system - Supervisor version",
|
||||||
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_systemd": {
|
"unsupported_systemd": {
|
||||||
"title": "Unsupported system - Systemd issues",
|
"title": "Unsupported system - Systemd issues",
|
||||||
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_systemd_journal": {
|
"unsupported_systemd_journal": {
|
||||||
"title": "Unsupported system - Systemd Journal issues",
|
"title": "Unsupported system - Systemd Journal issues",
|
||||||
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_systemd_resolved": {
|
"unsupported_systemd_resolved": {
|
||||||
"title": "Unsupported system - Systemd-Resolved issues",
|
"title": "Unsupported system - Systemd-Resolved issues",
|
||||||
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_virtualization_image": {
|
"unsupported_virtualization_image": {
|
||||||
"title": "Unsupported system - Incorrect OS image for virtualization",
|
"title": "Unsupported system - Incorrect OS image for virtualization",
|
||||||
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because the Home Assistant OS image in use is not intended for use in a virtualized environment. For troubleshooting information, select Learn more."
|
||||||
},
|
},
|
||||||
"unsupported_os_version": {
|
"unsupported_os_version": {
|
||||||
"title": "Unsupported system - Home Assistant OS version",
|
"title": "Unsupported system - Home Assistant OS version",
|
||||||
"description": "System is unsupported because the Home Assistant OS version in use is not supported. Use the link to learn more and how to fix this."
|
"description": "System is unsupported because the Home Assistant OS version in use is not supported. For troubleshooting information, select Learn more."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
@@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["holidays==0.77", "babel==2.15.0"]
|
"requirements": ["holidays==0.78", "babel==2.15.0"]
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_CONFIG_ENTRY_ID,
|
||||||
ATTR_HW_VERSION,
|
ATTR_HW_VERSION,
|
||||||
ATTR_MODEL,
|
ATTR_MODEL,
|
||||||
ATTR_SW_VERSION,
|
ATTR_SW_VERSION,
|
||||||
@@ -54,7 +55,6 @@ from homeassistant.helpers.typing import ConfigType
|
|||||||
from .const import (
|
from .const import (
|
||||||
ADMIN_SERVICES,
|
ADMIN_SERVICES,
|
||||||
ALL_KEYS,
|
ALL_KEYS,
|
||||||
ATTR_CONFIG_ENTRY_ID,
|
|
||||||
CONF_MANUFACTURER,
|
CONF_MANUFACTURER,
|
||||||
CONF_UNAUTHENTICATED_MODE,
|
CONF_UNAUTHENTICATED_MODE,
|
||||||
CONF_UPNP_UDN,
|
CONF_UPNP_UDN,
|
||||||
|
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
DOMAIN = "huawei_lte"
|
DOMAIN = "huawei_lte"
|
||||||
|
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
|
|
||||||
CONF_MANUFACTURER = "manufacturer"
|
CONF_MANUFACTURER = "manufacturer"
|
||||||
CONF_TRACK_WIRED_CLIENTS = "track_wired_clients"
|
CONF_TRACK_WIRED_CLIENTS = "track_wired_clients"
|
||||||
CONF_UNAUTHENTICATED_MODE = "unauthenticated_mode"
|
CONF_UNAUTHENTICATED_MODE = "unauthenticated_mode"
|
||||||
|
@@ -8,12 +8,12 @@ from typing import Any
|
|||||||
from huawei_lte_api.exceptions import ResponseErrorException
|
from huawei_lte_api.exceptions import ResponseErrorException
|
||||||
|
|
||||||
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
|
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
|
||||||
from homeassistant.const import CONF_RECIPIENT
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, CONF_RECIPIENT
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import Router
|
from . import Router
|
||||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@@ -42,10 +42,19 @@
|
|||||||
"local_name": "Ink@IAM-T1",
|
"local_name": "Ink@IAM-T1",
|
||||||
"connectable": true
|
"connectable": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"local_name": "Ink@IAM-T2",
|
||||||
|
"connectable": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"manufacturer_id": 12628,
|
"manufacturer_id": 12628,
|
||||||
"manufacturer_data_start": [65, 67, 45],
|
"manufacturer_data_start": [65, 67, 45],
|
||||||
"connectable": true
|
"connectable": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"manufacturer_id": 12884,
|
||||||
|
"manufacturer_data_start": [0, 98, 0],
|
||||||
|
"connectable": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"codeowners": ["@bdraco"],
|
"codeowners": ["@bdraco"],
|
||||||
@@ -53,5 +62,5 @@
|
|||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/inkbird",
|
"documentation": "https://www.home-assistant.io/integrations/inkbird",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"requirements": ["inkbird-ble==0.16.2"]
|
"requirements": ["inkbird-ble==1.1.0"]
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@
|
|||||||
"requirements": [
|
"requirements": [
|
||||||
"xknx==3.8.0",
|
"xknx==3.8.0",
|
||||||
"xknxproject==3.8.2",
|
"xknxproject==3.8.2",
|
||||||
"knx-frontend==2025.8.4.154919"
|
"knx-frontend==2025.8.6.52906"
|
||||||
],
|
],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["letpot"],
|
"loggers": ["letpot"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["letpot==0.5.0"]
|
"requirements": ["letpot==0.6.1"]
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,6 @@ DATA_HASS_CONFIG = "mastodon_hass_config"
|
|||||||
DEFAULT_URL: Final = "https://mastodon.social"
|
DEFAULT_URL: Final = "https://mastodon.social"
|
||||||
DEFAULT_NAME: Final = "Mastodon"
|
DEFAULT_NAME: Final = "Mastodon"
|
||||||
|
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
ATTR_STATUS = "status"
|
ATTR_STATUS = "status"
|
||||||
ATTR_VISIBILITY = "visibility"
|
ATTR_VISIBILITY = "visibility"
|
||||||
ATTR_CONTENT_WARNING = "content_warning"
|
ATTR_CONTENT_WARNING = "content_warning"
|
||||||
|
@@ -9,11 +9,11 @@ from mastodon.Mastodon import MastodonAPIError, MediaAttachment
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, ServiceResponse
|
from homeassistant.core import HomeAssistant, ServiceCall, ServiceResponse
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_CONFIG_ENTRY_ID,
|
|
||||||
ATTR_CONTENT_WARNING,
|
ATTR_CONTENT_WARNING,
|
||||||
ATTR_MEDIA,
|
ATTR_MEDIA,
|
||||||
ATTR_MEDIA_DESCRIPTION,
|
ATTR_MEDIA_DESCRIPTION,
|
||||||
|
@@ -99,6 +99,9 @@
|
|||||||
"esa_opt_out_state": {
|
"esa_opt_out_state": {
|
||||||
"default": "mdi:home-lightning-bolt"
|
"default": "mdi:home-lightning-bolt"
|
||||||
},
|
},
|
||||||
|
"esa_state": {
|
||||||
|
"default": "mdi:home-lightning-bolt"
|
||||||
|
},
|
||||||
"evse_state": {
|
"evse_state": {
|
||||||
"default": "mdi:ev-station"
|
"default": "mdi:ev-station"
|
||||||
},
|
},
|
||||||
|
@@ -140,11 +140,6 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
|||||||
def _update_from_device(self) -> None:
|
def _update_from_device(self) -> None:
|
||||||
"""Update from device."""
|
"""Update from device."""
|
||||||
self._calculate_features()
|
self._calculate_features()
|
||||||
# optional battery level
|
|
||||||
if VacuumEntityFeature.BATTERY & self._attr_supported_features:
|
|
||||||
self._attr_battery_level = self.get_matter_attribute_value(
|
|
||||||
clusters.PowerSource.Attributes.BatPercentRemaining
|
|
||||||
)
|
|
||||||
# derive state from the run mode + operational state
|
# derive state from the run mode + operational state
|
||||||
run_mode_raw: int = self.get_matter_attribute_value(
|
run_mode_raw: int = self.get_matter_attribute_value(
|
||||||
clusters.RvcRunMode.Attributes.CurrentMode
|
clusters.RvcRunMode.Attributes.CurrentMode
|
||||||
@@ -188,11 +183,6 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
|||||||
supported_features |= VacuumEntityFeature.STATE
|
supported_features |= VacuumEntityFeature.STATE
|
||||||
supported_features |= VacuumEntityFeature.STOP
|
supported_features |= VacuumEntityFeature.STOP
|
||||||
|
|
||||||
# optional battery attribute = battery feature
|
|
||||||
if self.get_matter_attribute_value(
|
|
||||||
clusters.PowerSource.Attributes.BatPercentRemaining
|
|
||||||
):
|
|
||||||
supported_features |= VacuumEntityFeature.BATTERY
|
|
||||||
# optional identify cluster = locate feature (value must be not None or 0)
|
# optional identify cluster = locate feature (value must be not None or 0)
|
||||||
if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType):
|
if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType):
|
||||||
supported_features |= VacuumEntityFeature.LOCATE
|
supported_features |= VacuumEntityFeature.LOCATE
|
||||||
@@ -230,7 +220,6 @@ DISCOVERY_SCHEMAS = [
|
|||||||
clusters.RvcRunMode.Attributes.CurrentMode,
|
clusters.RvcRunMode.Attributes.CurrentMode,
|
||||||
clusters.RvcOperationalState.Attributes.OperationalState,
|
clusters.RvcOperationalState.Attributes.OperationalState,
|
||||||
),
|
),
|
||||||
optional_attributes=(clusters.PowerSource.Attributes.BatPercentRemaining,),
|
|
||||||
device_type=(device_types.RoboticVacuumCleaner,),
|
device_type=(device_types.RoboticVacuumCleaner,),
|
||||||
allow_none_value=True,
|
allow_none_value=True,
|
||||||
),
|
),
|
||||||
|
@@ -8,7 +8,6 @@ DOMAIN = "mealie"
|
|||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
ATTR_START_DATE = "start_date"
|
ATTR_START_DATE = "start_date"
|
||||||
ATTR_END_DATE = "end_date"
|
ATTR_END_DATE = "end_date"
|
||||||
ATTR_RECIPE_ID = "recipe_id"
|
ATTR_RECIPE_ID = "recipe_id"
|
||||||
|
@@ -13,7 +13,7 @@ from aiomealie import (
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import ATTR_DATE
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, ATTR_DATE
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall,
|
ServiceCall,
|
||||||
@@ -25,7 +25,6 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
|||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_CONFIG_ENTRY_ID,
|
|
||||||
ATTR_END_DATE,
|
ATTR_END_DATE,
|
||||||
ATTR_ENTRY_TYPE,
|
ATTR_ENTRY_TYPE,
|
||||||
ATTR_INCLUDE_TAGS,
|
ATTR_INCLUDE_TAGS,
|
||||||
|
@@ -25,7 +25,6 @@ ATTR_APP_DATA = "app_data"
|
|||||||
ATTR_APP_ID = "app_id"
|
ATTR_APP_ID = "app_id"
|
||||||
ATTR_APP_NAME = "app_name"
|
ATTR_APP_NAME = "app_name"
|
||||||
ATTR_APP_VERSION = "app_version"
|
ATTR_APP_VERSION = "app_version"
|
||||||
ATTR_CONFIG_ENTRY_ID = "entry_id"
|
|
||||||
ATTR_DEVICE_NAME = "device_name"
|
ATTR_DEVICE_NAME = "device_name"
|
||||||
ATTR_MANUFACTURER = "manufacturer"
|
ATTR_MANUFACTURER = "manufacturer"
|
||||||
ATTR_MODEL = "model"
|
ATTR_MODEL = "model"
|
||||||
|
@@ -5,5 +5,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/modbus",
|
"documentation": "https://www.home-assistant.io/integrations/modbus",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pymodbus"],
|
"loggers": ["pymodbus"],
|
||||||
"requirements": ["pymodbus==3.9.2"]
|
"requirements": ["pymodbus==3.11.0"]
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
{
|
{
|
||||||
"issues": {
|
"issues": {
|
||||||
|
"deprecated_vacuum_battery_feature": {
|
||||||
|
"title": "Deprecated battery feature used",
|
||||||
|
"description": "Vacuum entity {entity_id} implements the battery feature which is deprecated. This will stop working in Home Assistant 2026.2. Implement a separate entity for the battery state instead. To fix the issue, remove the `battery` feature from the configured supported features, and restart Home Assistant."
|
||||||
|
},
|
||||||
"invalid_platform_config": {
|
"invalid_platform_config": {
|
||||||
"title": "Invalid config found for MQTT {domain} item",
|
"title": "Invalid config found for MQTT {domain} item",
|
||||||
"description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
|
"description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
|
||||||
|
@@ -17,7 +17,7 @@ from homeassistant.components.vacuum import (
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
|
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.json import json_dumps
|
from homeassistant.helpers.json import json_dumps
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType
|
||||||
@@ -25,11 +25,11 @@ from homeassistant.util.json import json_loads_object
|
|||||||
|
|
||||||
from . import subscription
|
from . import subscription
|
||||||
from .config import MQTT_BASE_SCHEMA
|
from .config import MQTT_BASE_SCHEMA
|
||||||
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC
|
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN
|
||||||
from .entity import MqttEntity, async_setup_entity_entry_helper
|
from .entity import IssueSeverity, MqttEntity, async_setup_entity_entry_helper
|
||||||
from .models import ReceiveMessage
|
from .models import ReceiveMessage
|
||||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||||
from .util import valid_publish_topic
|
from .util import learn_more_url, valid_publish_topic
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@@ -84,6 +84,8 @@ SERVICE_TO_STRING: dict[VacuumEntityFeature, str] = {
|
|||||||
VacuumEntityFeature.STOP: "stop",
|
VacuumEntityFeature.STOP: "stop",
|
||||||
VacuumEntityFeature.RETURN_HOME: "return_home",
|
VacuumEntityFeature.RETURN_HOME: "return_home",
|
||||||
VacuumEntityFeature.FAN_SPEED: "fan_speed",
|
VacuumEntityFeature.FAN_SPEED: "fan_speed",
|
||||||
|
# Use of the battery feature was deprecated in HA Core 2025.8
|
||||||
|
# and will be removed with HA Core 2026.2
|
||||||
VacuumEntityFeature.BATTERY: "battery",
|
VacuumEntityFeature.BATTERY: "battery",
|
||||||
VacuumEntityFeature.STATUS: "status",
|
VacuumEntityFeature.STATUS: "status",
|
||||||
VacuumEntityFeature.SEND_COMMAND: "send_command",
|
VacuumEntityFeature.SEND_COMMAND: "send_command",
|
||||||
@@ -96,7 +98,6 @@ DEFAULT_SERVICES = (
|
|||||||
VacuumEntityFeature.START
|
VacuumEntityFeature.START
|
||||||
| VacuumEntityFeature.STOP
|
| VacuumEntityFeature.STOP
|
||||||
| VacuumEntityFeature.RETURN_HOME
|
| VacuumEntityFeature.RETURN_HOME
|
||||||
| VacuumEntityFeature.BATTERY
|
|
||||||
| VacuumEntityFeature.CLEAN_SPOT
|
| VacuumEntityFeature.CLEAN_SPOT
|
||||||
)
|
)
|
||||||
ALL_SERVICES = (
|
ALL_SERVICES = (
|
||||||
@@ -251,10 +252,35 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def mqtt_async_added_to_hass(self) -> None:
|
||||||
|
"""Check for use of deprecated battery features."""
|
||||||
|
if self.supported_features & VacuumEntityFeature.BATTERY:
|
||||||
|
ir.async_create_issue(
|
||||||
|
self.hass,
|
||||||
|
DOMAIN,
|
||||||
|
f"deprecated_vacuum_battery_feature_{self.entity_id}",
|
||||||
|
issue_domain=vacuum.DOMAIN,
|
||||||
|
breaks_in_ha_version="2026.2",
|
||||||
|
is_fixable=False,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
learn_more_url=learn_more_url(vacuum.DOMAIN),
|
||||||
|
translation_placeholders={"entity_id": self.entity_id},
|
||||||
|
translation_key="deprecated_vacuum_battery_feature",
|
||||||
|
)
|
||||||
|
_LOGGER.warning(
|
||||||
|
"MQTT vacuum entity %s implements the battery feature "
|
||||||
|
"which is deprecated. This will stop working "
|
||||||
|
"in Home Assistant 2026.2. Implement a separate entity "
|
||||||
|
"for the battery status instead",
|
||||||
|
self.entity_id,
|
||||||
|
)
|
||||||
|
|
||||||
def _update_state_attributes(self, payload: dict[str, Any]) -> None:
|
def _update_state_attributes(self, payload: dict[str, Any]) -> None:
|
||||||
"""Update the entity state attributes."""
|
"""Update the entity state attributes."""
|
||||||
self._state_attrs.update(payload)
|
self._state_attrs.update(payload)
|
||||||
self._attr_fan_speed = self._state_attrs.get(FAN_SPEED, 0)
|
self._attr_fan_speed = self._state_attrs.get(FAN_SPEED, 0)
|
||||||
|
# Use of the battery feature was deprecated in HA Core 2025.8
|
||||||
|
# and will be removed with HA Core 2026.2
|
||||||
self._attr_battery_level = max(0, min(100, self._state_attrs.get(BATTERY, 0)))
|
self._attr_battery_level = max(0, min(100, self._state_attrs.get(BATTERY, 0)))
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
@@ -8,6 +8,7 @@ from music_assistant_models.enums import MediaType
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall,
|
ServiceCall,
|
||||||
@@ -24,7 +25,6 @@ from .const import (
|
|||||||
ATTR_ALBUMS,
|
ATTR_ALBUMS,
|
||||||
ATTR_ARTISTS,
|
ATTR_ARTISTS,
|
||||||
ATTR_AUDIOBOOKS,
|
ATTR_AUDIOBOOKS,
|
||||||
ATTR_CONFIG_ENTRY_ID,
|
|
||||||
ATTR_FAVORITE,
|
ATTR_FAVORITE,
|
||||||
ATTR_ITEMS,
|
ATTR_ITEMS,
|
||||||
ATTR_LIBRARY_ONLY,
|
ATTR_LIBRARY_ONLY,
|
||||||
|
@@ -26,7 +26,6 @@ ATTR_OFFSET = "offset"
|
|||||||
ATTR_ORDER_BY = "order_by"
|
ATTR_ORDER_BY = "order_by"
|
||||||
ATTR_ALBUM_TYPE = "album_type"
|
ATTR_ALBUM_TYPE = "album_type"
|
||||||
ATTR_ALBUM_ARTISTS_ONLY = "album_artists_only"
|
ATTR_ALBUM_ARTISTS_ONLY = "album_artists_only"
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
ATTR_URI = "uri"
|
ATTR_URI = "uri"
|
||||||
ATTR_IMAGE = "image"
|
ATTR_IMAGE = "image"
|
||||||
ATTR_VERSION = "version"
|
ATTR_VERSION = "version"
|
||||||
|
@@ -92,11 +92,15 @@ async def async_update_options(hass: HomeAssistant, entry: OllamaConfigEntry) ->
|
|||||||
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||||
"""Migrate integration entry structure."""
|
"""Migrate integration entry structure."""
|
||||||
|
|
||||||
entries = hass.config_entries.async_entries(DOMAIN)
|
# Make sure we get enabled config entries first
|
||||||
|
entries = sorted(
|
||||||
|
hass.config_entries.async_entries(DOMAIN),
|
||||||
|
key=lambda e: e.disabled_by is not None,
|
||||||
|
)
|
||||||
if not any(entry.version == 1 for entry in entries):
|
if not any(entry.version == 1 for entry in entries):
|
||||||
return
|
return
|
||||||
|
|
||||||
api_keys_entries: dict[str, ConfigEntry] = {}
|
url_entries: dict[str, tuple[ConfigEntry, bool]] = {}
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
@@ -112,33 +116,64 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
|||||||
title=entry.title,
|
title=entry.title,
|
||||||
unique_id=None,
|
unique_id=None,
|
||||||
)
|
)
|
||||||
if entry.data[CONF_URL] not in api_keys_entries:
|
if entry.data[CONF_URL] not in url_entries:
|
||||||
use_existing = True
|
use_existing = True
|
||||||
api_keys_entries[entry.data[CONF_URL]] = entry
|
all_disabled = all(
|
||||||
|
e.disabled_by is not None
|
||||||
|
for e in entries
|
||||||
|
if e.data[CONF_URL] == entry.data[CONF_URL]
|
||||||
|
)
|
||||||
|
url_entries[entry.data[CONF_URL]] = (entry, all_disabled)
|
||||||
|
|
||||||
parent_entry = api_keys_entries[entry.data[CONF_URL]]
|
parent_entry, all_disabled = url_entries[entry.data[CONF_URL]]
|
||||||
|
|
||||||
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
||||||
|
|
||||||
conversation_entity = entity_registry.async_get_entity_id(
|
conversation_entity_id = entity_registry.async_get_entity_id(
|
||||||
"conversation",
|
"conversation",
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
)
|
)
|
||||||
if conversation_entity is not None:
|
|
||||||
entity_registry.async_update_entity(
|
|
||||||
conversation_entity,
|
|
||||||
config_entry_id=parent_entry.entry_id,
|
|
||||||
config_subentry_id=subentry.subentry_id,
|
|
||||||
new_unique_id=subentry.subentry_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
device = device_registry.async_get_device(
|
device = device_registry.async_get_device(
|
||||||
identifiers={(DOMAIN, entry.entry_id)}
|
identifiers={(DOMAIN, entry.entry_id)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if conversation_entity_id is not None:
|
||||||
|
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
|
||||||
|
entity_disabled_by = conversation_entity_entry.disabled_by
|
||||||
|
if (
|
||||||
|
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
|
||||||
|
and not all_disabled
|
||||||
|
):
|
||||||
|
# Device and entity registries don't update the disabled_by flag
|
||||||
|
# when moving a device or entity from one config entry to another,
|
||||||
|
# so we need to do it manually.
|
||||||
|
entity_disabled_by = (
|
||||||
|
er.RegistryEntryDisabler.DEVICE
|
||||||
|
if device
|
||||||
|
else er.RegistryEntryDisabler.USER
|
||||||
|
)
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
conversation_entity_id,
|
||||||
|
config_entry_id=parent_entry.entry_id,
|
||||||
|
config_subentry_id=subentry.subentry_id,
|
||||||
|
disabled_by=entity_disabled_by,
|
||||||
|
new_unique_id=subentry.subentry_id,
|
||||||
|
)
|
||||||
|
|
||||||
if device is not None:
|
if device is not None:
|
||||||
|
# Device and entity registries don't update the disabled_by flag when
|
||||||
|
# moving a device or entity from one config entry to another, so we
|
||||||
|
# need to do it manually.
|
||||||
|
device_disabled_by = device.disabled_by
|
||||||
|
if (
|
||||||
|
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
|
||||||
|
and not all_disabled
|
||||||
|
):
|
||||||
|
device_disabled_by = dr.DeviceEntryDisabler.USER
|
||||||
device_registry.async_update_device(
|
device_registry.async_update_device(
|
||||||
device.id,
|
device.id,
|
||||||
|
disabled_by=device_disabled_by,
|
||||||
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
||||||
add_config_subentry_id=subentry.subentry_id,
|
add_config_subentry_id=subentry.subentry_id,
|
||||||
add_config_entry_id=parent_entry.entry_id,
|
add_config_entry_id=parent_entry.entry_id,
|
||||||
@@ -158,6 +193,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
|||||||
if not use_existing:
|
if not use_existing:
|
||||||
await hass.config_entries.async_remove(entry.entry_id)
|
await hass.config_entries.async_remove(entry.entry_id)
|
||||||
else:
|
else:
|
||||||
|
_add_ai_task_subentry(hass, entry)
|
||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry(
|
||||||
entry,
|
entry,
|
||||||
title=DEFAULT_NAME,
|
title=DEFAULT_NAME,
|
||||||
@@ -165,7 +201,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
|||||||
data={CONF_URL: entry.data[CONF_URL]},
|
data={CONF_URL: entry.data[CONF_URL]},
|
||||||
options={},
|
options={},
|
||||||
version=3,
|
version=3,
|
||||||
minor_version=1,
|
minor_version=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -211,32 +247,69 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OllamaConfigEntry) ->
|
|||||||
)
|
)
|
||||||
|
|
||||||
if entry.version == 3 and entry.minor_version == 1:
|
if entry.version == 3 and entry.minor_version == 1:
|
||||||
# Add AI Task subentry with default options. We can only create a new
|
_add_ai_task_subentry(hass, entry)
|
||||||
# subentry if we can find an existing model in the entry. The model
|
|
||||||
# was removed in the previous migration step, so we need to
|
|
||||||
# check the subentries for an existing model.
|
|
||||||
existing_model = next(
|
|
||||||
iter(
|
|
||||||
model
|
|
||||||
for subentry in entry.subentries.values()
|
|
||||||
if (model := subentry.data.get(CONF_MODEL)) is not None
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
if existing_model:
|
|
||||||
hass.config_entries.async_add_subentry(
|
|
||||||
entry,
|
|
||||||
ConfigSubentry(
|
|
||||||
data=MappingProxyType({CONF_MODEL: existing_model}),
|
|
||||||
subentry_type="ai_task_data",
|
|
||||||
title=DEFAULT_AI_TASK_NAME,
|
|
||||||
unique_id=None,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||||
|
|
||||||
|
if entry.version == 3 and entry.minor_version == 2:
|
||||||
|
# Fix migration where the disabled_by flag was not set correctly.
|
||||||
|
# We can currently only correct this for enabled config entries,
|
||||||
|
# because migration does not run for disabled config entries. This
|
||||||
|
# is asserted in tests, and if that behavior is changed, we should
|
||||||
|
# correct also disabled config entries.
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||||
|
entity_entries = er.async_entries_for_config_entry(
|
||||||
|
entity_registry, entry.entry_id
|
||||||
|
)
|
||||||
|
if entry.disabled_by is None:
|
||||||
|
# If the config entry is not disabled, we need to set the disabled_by
|
||||||
|
# flag on devices to USER, and on entities to DEVICE, if they are set
|
||||||
|
# to CONFIG_ENTRY.
|
||||||
|
for device in devices:
|
||||||
|
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
|
||||||
|
continue
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device.id,
|
||||||
|
disabled_by=dr.DeviceEntryDisabler.USER,
|
||||||
|
)
|
||||||
|
for entity in entity_entries:
|
||||||
|
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
|
||||||
|
continue
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
entity.entity_id,
|
||||||
|
disabled_by=er.RegistryEntryDisabler.DEVICE,
|
||||||
|
)
|
||||||
|
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _add_ai_task_subentry(hass: HomeAssistant, entry: OllamaConfigEntry) -> None:
|
||||||
|
"""Add AI Task subentry to the config entry."""
|
||||||
|
# Add AI Task subentry with default options. We can only create a new
|
||||||
|
# subentry if we can find an existing model in the entry. The model
|
||||||
|
# was removed in the previous migration step, so we need to
|
||||||
|
# check the subentries for an existing model.
|
||||||
|
existing_model = next(
|
||||||
|
iter(
|
||||||
|
model
|
||||||
|
for subentry in entry.subentries.values()
|
||||||
|
if (model := subentry.data.get(CONF_MODEL)) is not None
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if existing_model:
|
||||||
|
hass.config_entries.async_add_subentry(
|
||||||
|
entry,
|
||||||
|
ConfigSubentry(
|
||||||
|
data=MappingProxyType({CONF_MODEL: existing_model}),
|
||||||
|
subentry_type="ai_task_data",
|
||||||
|
title=DEFAULT_AI_TASK_NAME,
|
||||||
|
unique_id=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@@ -76,7 +76,7 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for Ollama."""
|
"""Handle a config flow for Ollama."""
|
||||||
|
|
||||||
VERSION = 3
|
VERSION = 3
|
||||||
MINOR_VERSION = 2
|
MINOR_VERSION = 3
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize config flow."""
|
"""Initialize config flow."""
|
||||||
|
@@ -53,8 +53,6 @@ class OneWireEntity(Entity):
|
|||||||
"""Return the state attributes of the entity."""
|
"""Return the state attributes of the entity."""
|
||||||
return {
|
return {
|
||||||
"device_file": self._device_file,
|
"device_file": self._device_file,
|
||||||
# raw_value attribute is deprecated and can be removed in 2025.8
|
|
||||||
"raw_value": self._value_raw,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def _read_value(self) -> str:
|
def _read_value(self) -> str:
|
||||||
|
@@ -272,11 +272,15 @@ async def async_update_options(hass: HomeAssistant, entry: OpenAIConfigEntry) ->
|
|||||||
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||||
"""Migrate integration entry structure."""
|
"""Migrate integration entry structure."""
|
||||||
|
|
||||||
entries = hass.config_entries.async_entries(DOMAIN)
|
# Make sure we get enabled config entries first
|
||||||
|
entries = sorted(
|
||||||
|
hass.config_entries.async_entries(DOMAIN),
|
||||||
|
key=lambda e: e.disabled_by is not None,
|
||||||
|
)
|
||||||
if not any(entry.version == 1 for entry in entries):
|
if not any(entry.version == 1 for entry in entries):
|
||||||
return
|
return
|
||||||
|
|
||||||
api_keys_entries: dict[str, ConfigEntry] = {}
|
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
@@ -290,30 +294,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
||||||
use_existing = True
|
use_existing = True
|
||||||
api_keys_entries[entry.data[CONF_API_KEY]] = entry
|
all_disabled = all(
|
||||||
|
e.disabled_by is not None
|
||||||
|
for e in entries
|
||||||
|
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
|
||||||
|
)
|
||||||
|
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
|
||||||
|
|
||||||
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
|
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||||
|
|
||||||
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
||||||
conversation_entity = entity_registry.async_get_entity_id(
|
conversation_entity_id = entity_registry.async_get_entity_id(
|
||||||
"conversation",
|
"conversation",
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
)
|
)
|
||||||
if conversation_entity is not None:
|
|
||||||
entity_registry.async_update_entity(
|
|
||||||
conversation_entity,
|
|
||||||
config_entry_id=parent_entry.entry_id,
|
|
||||||
config_subentry_id=subentry.subentry_id,
|
|
||||||
new_unique_id=subentry.subentry_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
device = device_registry.async_get_device(
|
device = device_registry.async_get_device(
|
||||||
identifiers={(DOMAIN, entry.entry_id)}
|
identifiers={(DOMAIN, entry.entry_id)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if conversation_entity_id is not None:
|
||||||
|
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
|
||||||
|
entity_disabled_by = conversation_entity_entry.disabled_by
|
||||||
|
if (
|
||||||
|
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
|
||||||
|
and not all_disabled
|
||||||
|
):
|
||||||
|
# Device and entity registries don't update the disabled_by flag
|
||||||
|
# when moving a device or entity from one config entry to another,
|
||||||
|
# so we need to do it manually.
|
||||||
|
entity_disabled_by = (
|
||||||
|
er.RegistryEntryDisabler.DEVICE
|
||||||
|
if device
|
||||||
|
else er.RegistryEntryDisabler.USER
|
||||||
|
)
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
conversation_entity_id,
|
||||||
|
config_entry_id=parent_entry.entry_id,
|
||||||
|
config_subentry_id=subentry.subentry_id,
|
||||||
|
disabled_by=entity_disabled_by,
|
||||||
|
new_unique_id=subentry.subentry_id,
|
||||||
|
)
|
||||||
|
|
||||||
if device is not None:
|
if device is not None:
|
||||||
|
# Device and entity registries don't update the disabled_by flag when
|
||||||
|
# moving a device or entity from one config entry to another, so we
|
||||||
|
# need to do it manually.
|
||||||
|
device_disabled_by = device.disabled_by
|
||||||
|
if (
|
||||||
|
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
|
||||||
|
and not all_disabled
|
||||||
|
):
|
||||||
|
device_disabled_by = dr.DeviceEntryDisabler.USER
|
||||||
device_registry.async_update_device(
|
device_registry.async_update_device(
|
||||||
device.id,
|
device.id,
|
||||||
|
disabled_by=device_disabled_by,
|
||||||
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
||||||
add_config_subentry_id=subentry.subentry_id,
|
add_config_subentry_id=subentry.subentry_id,
|
||||||
add_config_entry_id=parent_entry.entry_id,
|
add_config_entry_id=parent_entry.entry_id,
|
||||||
@@ -333,12 +368,13 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
|||||||
if not use_existing:
|
if not use_existing:
|
||||||
await hass.config_entries.async_remove(entry.entry_id)
|
await hass.config_entries.async_remove(entry.entry_id)
|
||||||
else:
|
else:
|
||||||
|
_add_ai_task_subentry(hass, entry)
|
||||||
hass.config_entries.async_update_entry(
|
hass.config_entries.async_update_entry(
|
||||||
entry,
|
entry,
|
||||||
title=DEFAULT_NAME,
|
title=DEFAULT_NAME,
|
||||||
options={},
|
options={},
|
||||||
version=2,
|
version=2,
|
||||||
minor_version=2,
|
minor_version=4,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -365,19 +401,56 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) ->
|
|||||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||||
|
|
||||||
if entry.version == 2 and entry.minor_version == 2:
|
if entry.version == 2 and entry.minor_version == 2:
|
||||||
hass.config_entries.async_add_subentry(
|
_add_ai_task_subentry(hass, entry)
|
||||||
entry,
|
|
||||||
ConfigSubentry(
|
|
||||||
data=MappingProxyType(RECOMMENDED_AI_TASK_OPTIONS),
|
|
||||||
subentry_type="ai_task_data",
|
|
||||||
title=DEFAULT_AI_TASK_NAME,
|
|
||||||
unique_id=None,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
hass.config_entries.async_update_entry(entry, minor_version=3)
|
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||||
|
|
||||||
|
if entry.version == 2 and entry.minor_version == 3:
|
||||||
|
# Fix migration where the disabled_by flag was not set correctly.
|
||||||
|
# We can currently only correct this for enabled config entries,
|
||||||
|
# because migration does not run for disabled config entries. This
|
||||||
|
# is asserted in tests, and if that behavior is changed, we should
|
||||||
|
# correct also disabled config entries.
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||||
|
entity_entries = er.async_entries_for_config_entry(
|
||||||
|
entity_registry, entry.entry_id
|
||||||
|
)
|
||||||
|
if entry.disabled_by is None:
|
||||||
|
# If the config entry is not disabled, we need to set the disabled_by
|
||||||
|
# flag on devices to USER, and on entities to DEVICE, if they are set
|
||||||
|
# to CONFIG_ENTRY.
|
||||||
|
for device in devices:
|
||||||
|
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
|
||||||
|
continue
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device.id,
|
||||||
|
disabled_by=dr.DeviceEntryDisabler.USER,
|
||||||
|
)
|
||||||
|
for entity in entity_entries:
|
||||||
|
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
|
||||||
|
continue
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
entity.entity_id,
|
||||||
|
disabled_by=er.RegistryEntryDisabler.DEVICE,
|
||||||
|
)
|
||||||
|
hass.config_entries.async_update_entry(entry, minor_version=4)
|
||||||
|
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _add_ai_task_subentry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> None:
|
||||||
|
"""Add AI Task subentry to the config entry."""
|
||||||
|
hass.config_entries.async_add_subentry(
|
||||||
|
entry,
|
||||||
|
ConfigSubentry(
|
||||||
|
data=MappingProxyType(RECOMMENDED_AI_TASK_OPTIONS),
|
||||||
|
subentry_type="ai_task_data",
|
||||||
|
title=DEFAULT_AI_TASK_NAME,
|
||||||
|
unique_id=None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@@ -98,7 +98,7 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for OpenAI Conversation."""
|
"""Handle a config flow for OpenAI Conversation."""
|
||||||
|
|
||||||
VERSION = 2
|
VERSION = 2
|
||||||
MINOR_VERSION = 3
|
MINOR_VERSION = 4
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
@@ -9,6 +9,8 @@ from typing import Any
|
|||||||
from opower import (
|
from opower import (
|
||||||
CannotConnect,
|
CannotConnect,
|
||||||
InvalidAuth,
|
InvalidAuth,
|
||||||
|
MfaChallenge,
|
||||||
|
MfaHandlerBase,
|
||||||
Opower,
|
Opower,
|
||||||
create_cookie_jar,
|
create_cookie_jar,
|
||||||
get_supported_utility_names,
|
get_supported_utility_names,
|
||||||
@@ -16,49 +18,34 @@ from opower import (
|
|||||||
)
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
from homeassistant.helpers.typing import VolDictType
|
from homeassistant.helpers.typing import VolDictType
|
||||||
|
|
||||||
from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
|
from .const import CONF_LOGIN_DATA, CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONF_MFA_CODE = "mfa_code"
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
CONF_MFA_METHOD = "mfa_method"
|
||||||
{
|
|
||||||
vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names()),
|
|
||||||
vol.Required(CONF_USERNAME): str,
|
|
||||||
vol.Required(CONF_PASSWORD): str,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _validate_login(
|
async def _validate_login(
|
||||||
hass: HomeAssistant, login_data: dict[str, str]
|
hass: HomeAssistant,
|
||||||
) -> dict[str, str]:
|
data: Mapping[str, Any],
|
||||||
"""Validate login data and return any errors."""
|
) -> None:
|
||||||
|
"""Validate login data and raise exceptions on failure."""
|
||||||
api = Opower(
|
api = Opower(
|
||||||
async_create_clientsession(hass, cookie_jar=create_cookie_jar()),
|
async_create_clientsession(hass, cookie_jar=create_cookie_jar()),
|
||||||
login_data[CONF_UTILITY],
|
data[CONF_UTILITY],
|
||||||
login_data[CONF_USERNAME],
|
data[CONF_USERNAME],
|
||||||
login_data[CONF_PASSWORD],
|
data[CONF_PASSWORD],
|
||||||
login_data.get(CONF_TOTP_SECRET),
|
data.get(CONF_TOTP_SECRET),
|
||||||
|
data.get(CONF_LOGIN_DATA),
|
||||||
)
|
)
|
||||||
errors: dict[str, str] = {}
|
await api.async_login()
|
||||||
try:
|
|
||||||
await api.async_login()
|
|
||||||
except InvalidAuth:
|
|
||||||
_LOGGER.exception(
|
|
||||||
"Invalid auth when connecting to %s", login_data[CONF_UTILITY]
|
|
||||||
)
|
|
||||||
errors["base"] = "invalid_auth"
|
|
||||||
except CannotConnect:
|
|
||||||
_LOGGER.exception("Could not connect to %s", login_data[CONF_UTILITY])
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
|
class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
@@ -68,81 +55,147 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize a new OpowerConfigFlow."""
|
"""Initialize a new OpowerConfigFlow."""
|
||||||
self.utility_info: dict[str, Any] | None = None
|
self._data: dict[str, Any] = {}
|
||||||
|
self.mfa_handler: MfaHandlerBase | None = None
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle the initial step."""
|
"""Handle the initial step (select utility)."""
|
||||||
errors: dict[str, str] = {}
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self._async_abort_entries_match(
|
self._data[CONF_UTILITY] = user_input[CONF_UTILITY]
|
||||||
{
|
return await self.async_step_credentials()
|
||||||
CONF_UTILITY: user_input[CONF_UTILITY],
|
|
||||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if select_utility(user_input[CONF_UTILITY]).accepts_mfa():
|
|
||||||
self.utility_info = user_input
|
|
||||||
return await self.async_step_mfa()
|
|
||||||
|
|
||||||
errors = await _validate_login(self.hass, user_input)
|
|
||||||
if not errors:
|
|
||||||
return self._async_create_opower_entry(user_input)
|
|
||||||
else:
|
|
||||||
user_input = {}
|
|
||||||
user_input.pop(CONF_PASSWORD, None)
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(
|
||||||
|
{vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names())}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_credentials(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle credentials step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
utility = select_utility(self._data[CONF_UTILITY])
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
self._data.update(user_input)
|
||||||
|
|
||||||
|
self._async_abort_entries_match(
|
||||||
|
{
|
||||||
|
CONF_UTILITY: self._data[CONF_UTILITY],
|
||||||
|
CONF_USERNAME: self._data[CONF_USERNAME],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await _validate_login(self.hass, self._data)
|
||||||
|
except MfaChallenge as exc:
|
||||||
|
self.mfa_handler = exc.handler
|
||||||
|
return await self.async_step_mfa_options()
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
return self._async_create_opower_entry(self._data)
|
||||||
|
|
||||||
|
schema_dict: VolDictType = {
|
||||||
|
vol.Required(CONF_USERNAME): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
if utility.accepts_totp_secret():
|
||||||
|
schema_dict[vol.Optional(CONF_TOTP_SECRET)] = str
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="credentials",
|
||||||
data_schema=self.add_suggested_values_to_schema(
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
STEP_USER_DATA_SCHEMA, user_input
|
vol.Schema(schema_dict), user_input
|
||||||
),
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_mfa(
|
async def async_step_mfa_options(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle MFA step."""
|
"""Handle MFA options step."""
|
||||||
assert self.utility_info is not None
|
errors: dict[str, str] = {}
|
||||||
|
assert self.mfa_handler is not None
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
method = user_input[CONF_MFA_METHOD]
|
||||||
|
try:
|
||||||
|
await self.mfa_handler.async_select_mfa_option(method)
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
return await self.async_step_mfa_code()
|
||||||
|
|
||||||
|
mfa_options = await self.mfa_handler.async_get_mfa_options()
|
||||||
|
if not mfa_options:
|
||||||
|
return await self.async_step_mfa_code()
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="mfa_options",
|
||||||
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
vol.Schema({vol.Required(CONF_MFA_METHOD): vol.In(mfa_options)}),
|
||||||
|
user_input,
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_mfa_code(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle MFA code submission step."""
|
||||||
|
assert self.mfa_handler is not None
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
data = {**self.utility_info, **user_input}
|
code = user_input[CONF_MFA_CODE]
|
||||||
errors = await _validate_login(self.hass, data)
|
try:
|
||||||
if not errors:
|
login_data = await self.mfa_handler.async_submit_mfa_code(code)
|
||||||
return self._async_create_opower_entry(data)
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_mfa_code"
|
||||||
if errors:
|
except CannotConnect:
|
||||||
schema = {
|
errors["base"] = "cannot_connect"
|
||||||
vol.Required(
|
else:
|
||||||
CONF_USERNAME, default=self.utility_info[CONF_USERNAME]
|
self._data[CONF_LOGIN_DATA] = login_data
|
||||||
): str,
|
if self.source == SOURCE_REAUTH:
|
||||||
vol.Required(CONF_PASSWORD): str,
|
return self.async_update_reload_and_abort(
|
||||||
}
|
self._get_reauth_entry(), data=self._data
|
||||||
else:
|
)
|
||||||
schema = {}
|
return self._async_create_opower_entry(self._data)
|
||||||
|
|
||||||
schema[vol.Required(CONF_TOTP_SECRET)] = str
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="mfa",
|
step_id="mfa_code",
|
||||||
data_schema=vol.Schema(schema),
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
vol.Schema({vol.Required(CONF_MFA_CODE): str}), user_input
|
||||||
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_create_opower_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
def _async_create_opower_entry(
|
||||||
|
self, data: dict[str, Any], **kwargs: Any
|
||||||
|
) -> ConfigFlowResult:
|
||||||
"""Create the config entry."""
|
"""Create the config entry."""
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=f"{data[CONF_UTILITY]} ({data[CONF_USERNAME]})",
|
title=f"{data[CONF_UTILITY]} ({data[CONF_USERNAME]})",
|
||||||
data=data,
|
data=data,
|
||||||
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_reauth(
|
async def async_step_reauth(
|
||||||
self, entry_data: Mapping[str, Any]
|
self, entry_data: Mapping[str, Any]
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle configuration by re-auth."""
|
"""Handle configuration by re-auth."""
|
||||||
return await self.async_step_reauth_confirm()
|
reauth_entry = self._get_reauth_entry()
|
||||||
|
self._data = dict(reauth_entry.data)
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth_confirm",
|
||||||
|
description_placeholders={CONF_NAME: reauth_entry.title},
|
||||||
|
)
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
async def async_step_reauth_confirm(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
@@ -150,21 +203,34 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Dialog that informs the user that reauth is required."""
|
"""Dialog that informs the user that reauth is required."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
reauth_entry = self._get_reauth_entry()
|
reauth_entry = self._get_reauth_entry()
|
||||||
if user_input is not None:
|
|
||||||
data = {**reauth_entry.data, **user_input}
|
|
||||||
errors = await _validate_login(self.hass, data)
|
|
||||||
if not errors:
|
|
||||||
return self.async_update_reload_and_abort(reauth_entry, data=data)
|
|
||||||
|
|
||||||
schema: VolDictType = {
|
if user_input is not None:
|
||||||
vol.Required(CONF_USERNAME): reauth_entry.data[CONF_USERNAME],
|
self._data.update(user_input)
|
||||||
|
try:
|
||||||
|
await _validate_login(self.hass, self._data)
|
||||||
|
except MfaChallenge as exc:
|
||||||
|
self.mfa_handler = exc.handler
|
||||||
|
return await self.async_step_mfa_options()
|
||||||
|
except InvalidAuth:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
else:
|
||||||
|
return self.async_update_reload_and_abort(reauth_entry, data=self._data)
|
||||||
|
|
||||||
|
utility = select_utility(self._data[CONF_UTILITY])
|
||||||
|
schema_dict: VolDictType = {
|
||||||
|
vol.Required(CONF_USERNAME): str,
|
||||||
vol.Required(CONF_PASSWORD): str,
|
vol.Required(CONF_PASSWORD): str,
|
||||||
}
|
}
|
||||||
if select_utility(reauth_entry.data[CONF_UTILITY]).accepts_mfa():
|
if utility.accepts_totp_secret():
|
||||||
schema[vol.Optional(CONF_TOTP_SECRET)] = str
|
schema_dict[vol.Optional(CONF_TOTP_SECRET)] = str
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reauth_confirm",
|
step_id="reauth_confirm",
|
||||||
data_schema=vol.Schema(schema),
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
vol.Schema(schema_dict), self._data
|
||||||
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders={CONF_NAME: reauth_entry.title},
|
description_placeholders={CONF_NAME: reauth_entry.title},
|
||||||
)
|
)
|
||||||
|
@@ -4,3 +4,4 @@ DOMAIN = "opower"
|
|||||||
|
|
||||||
CONF_UTILITY = "utility"
|
CONF_UTILITY = "utility"
|
||||||
CONF_TOTP_SECRET = "totp_secret"
|
CONF_TOTP_SECRET = "totp_secret"
|
||||||
|
CONF_LOGIN_DATA = "login_data"
|
||||||
|
@@ -14,7 +14,7 @@ from opower import (
|
|||||||
ReadResolution,
|
ReadResolution,
|
||||||
create_cookie_jar,
|
create_cookie_jar,
|
||||||
)
|
)
|
||||||
from opower.exceptions import ApiException, CannotConnect, InvalidAuth
|
from opower.exceptions import ApiException, CannotConnect, InvalidAuth, MfaChallenge
|
||||||
|
|
||||||
from homeassistant.components.recorder import get_instance
|
from homeassistant.components.recorder import get_instance
|
||||||
from homeassistant.components.recorder.models import (
|
from homeassistant.components.recorder.models import (
|
||||||
@@ -36,7 +36,7 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
|||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
|
from .const import CONF_LOGIN_DATA, CONF_TOTP_SECRET, CONF_UTILITY, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -69,6 +69,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
|||||||
config_entry.data[CONF_USERNAME],
|
config_entry.data[CONF_USERNAME],
|
||||||
config_entry.data[CONF_PASSWORD],
|
config_entry.data[CONF_PASSWORD],
|
||||||
config_entry.data.get(CONF_TOTP_SECRET),
|
config_entry.data.get(CONF_TOTP_SECRET),
|
||||||
|
config_entry.data.get(CONF_LOGIN_DATA),
|
||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@@ -90,7 +91,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
|
|||||||
# Given the infrequent updating (every 12h)
|
# Given the infrequent updating (every 12h)
|
||||||
# assume previous session has expired and re-login.
|
# assume previous session has expired and re-login.
|
||||||
await self.api.async_login()
|
await self.api.async_login()
|
||||||
except InvalidAuth as err:
|
except (InvalidAuth, MfaChallenge) as err:
|
||||||
_LOGGER.error("Error during login: %s", err)
|
_LOGGER.error("Error during login: %s", err)
|
||||||
raise ConfigEntryAuthFailed from err
|
raise ConfigEntryAuthFailed from err
|
||||||
except CannotConnect as err:
|
except CannotConnect as err:
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["opower"],
|
"loggers": ["opower"],
|
||||||
"requirements": ["opower==0.12.4"]
|
"requirements": ["opower==0.15.1"]
|
||||||
}
|
}
|
||||||
|
@@ -3,27 +3,43 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"utility": "Utility name",
|
"utility": "Utility name"
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"utility": "The name of your utility provider",
|
"utility": "The name of your utility provider"
|
||||||
"username": "The username for your utility account",
|
|
||||||
"password": "The password for your utility account"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mfa": {
|
"credentials": {
|
||||||
"description": "The TOTP secret below is not one of the 6-digit time-based numeric codes. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation.",
|
"title": "Enter Credentials",
|
||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"totp_secret": "TOTP secret"
|
"totp_secret": "TOTP secret"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"username": "[%key:component::opower::config::step::user::data_description::username%]",
|
"username": "The username for your utility account",
|
||||||
"password": "[%key:component::opower::config::step::user::data_description::password%]",
|
"password": "The password for your utility account",
|
||||||
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
|
"totp_secret": "This is not a 6-digit code. It is a string of around 16 characters containing the shared secret that enables your authenticator app to generate the correct time-based code at the appropriate time. See the documentation."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mfa_options": {
|
||||||
|
"title": "Multi-factor authentication",
|
||||||
|
"description": "Your account requires multi-factor authentication (MFA). Select a method to receive your security code.",
|
||||||
|
"data": {
|
||||||
|
"mfa_method": "MFA method"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"mfa_method": "How to receive your security code"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mfa_code": {
|
||||||
|
"title": "Enter security code",
|
||||||
|
"description": "A security code has been sent via your selected method. Please enter it below to complete login.",
|
||||||
|
"data": {
|
||||||
|
"mfa_code": "Security code"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"mfa_code": "Typically a 6-digit code"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
@@ -31,18 +47,19 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"totp_secret": "[%key:component::opower::config::step::mfa::data::totp_secret%]"
|
"totp_secret": "[%key:component::opower::config::step::credentials::data::totp_secret%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"username": "[%key:component::opower::config::step::user::data_description::username%]",
|
"username": "[%key:component::opower::config::step::credentials::data_description::username%]",
|
||||||
"password": "[%key:component::opower::config::step::user::data_description::password%]",
|
"password": "[%key:component::opower::config::step::credentials::data_description::password%]",
|
||||||
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
|
"totp_secret": "[%key:component::opower::config::step::credentials::data_description::totp_secret%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"invalid_mfa_code": "The security code is incorrect. Please try again."
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||||
|
@@ -9,7 +9,6 @@ LOGGER = logging.getLogger(__package__)
|
|||||||
|
|
||||||
REQUESTS = "requests"
|
REQUESTS = "requests"
|
||||||
|
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
ATTR_STATUS = "status"
|
ATTR_STATUS = "status"
|
||||||
ATTR_SORT_ORDER = "sort_order"
|
ATTR_SORT_ORDER = "sort_order"
|
||||||
ATTR_REQUESTED_BY = "requested_by"
|
ATTR_REQUESTED_BY = "requested_by"
|
||||||
|
@@ -7,6 +7,7 @@ from python_overseerr import OverseerrClient, OverseerrConnectionError
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall,
|
ServiceCall,
|
||||||
@@ -17,14 +18,7 @@ from homeassistant.core import (
|
|||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||||
from homeassistant.util.json import JsonValueType
|
from homeassistant.util.json import JsonValueType
|
||||||
|
|
||||||
from .const import (
|
from .const import ATTR_REQUESTED_BY, ATTR_SORT_ORDER, ATTR_STATUS, DOMAIN, LOGGER
|
||||||
ATTR_CONFIG_ENTRY_ID,
|
|
||||||
ATTR_REQUESTED_BY,
|
|
||||||
ATTR_SORT_ORDER,
|
|
||||||
ATTR_STATUS,
|
|
||||||
DOMAIN,
|
|
||||||
LOGGER,
|
|
||||||
)
|
|
||||||
from .coordinator import OverseerrConfigEntry
|
from .coordinator import OverseerrConfigEntry
|
||||||
|
|
||||||
SERVICE_GET_REQUESTS = "get_requests"
|
SERVICE_GET_REQUESTS = "get_requests"
|
||||||
|
@@ -9,7 +9,6 @@ CONF_COORDINATOR = "coordinator"
|
|||||||
|
|
||||||
SERVICE_ADD_PRODUCT_TO_CART = "add_product"
|
SERVICE_ADD_PRODUCT_TO_CART = "add_product"
|
||||||
|
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
ATTR_PRODUCT_ID = "product_id"
|
ATTR_PRODUCT_ID = "product_id"
|
||||||
ATTR_PRODUCT_NAME = "product_name"
|
ATTR_PRODUCT_NAME = "product_name"
|
||||||
ATTR_AMOUNT = "amount"
|
ATTR_AMOUNT = "amount"
|
||||||
|
@@ -7,12 +7,12 @@ from typing import cast
|
|||||||
from python_picnic_api2 import PicnicAPI
|
from python_picnic_api2 import PicnicAPI
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_AMOUNT,
|
ATTR_AMOUNT,
|
||||||
ATTR_CONFIG_ENTRY_ID,
|
|
||||||
ATTR_PRODUCT_ID,
|
ATTR_PRODUCT_ID,
|
||||||
ATTR_PRODUCT_IDENTIFIERS,
|
ATTR_PRODUCT_IDENTIFIERS,
|
||||||
ATTR_PRODUCT_NAME,
|
ATTR_PRODUCT_NAME,
|
||||||
|
@@ -8,6 +8,5 @@ CONF_SERIAL_NUMBER = "serial_number"
|
|||||||
CONF_IMPORTED_NAMES = "imported_names"
|
CONF_IMPORTED_NAMES = "imported_names"
|
||||||
|
|
||||||
ATTR_DURATION = "duration"
|
ATTR_DURATION = "duration"
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
|
|
||||||
TIMEOUT_SECONDS = 20
|
TIMEOUT_SECONDS = 20
|
||||||
|
@@ -59,7 +59,7 @@ PLATFORMS = [
|
|||||||
Platform.UPDATE,
|
Platform.UPDATE,
|
||||||
]
|
]
|
||||||
DEVICE_UPDATE_INTERVAL = timedelta(seconds=60)
|
DEVICE_UPDATE_INTERVAL = timedelta(seconds=60)
|
||||||
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=12)
|
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=24)
|
||||||
NUM_CRED_ERRORS = 3
|
NUM_CRED_ERRORS = 3
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
@@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics(
|
|||||||
IPC_cam[ch]["hardware version"] = api.camera_hardware_version(ch)
|
IPC_cam[ch]["hardware version"] = api.camera_hardware_version(ch)
|
||||||
IPC_cam[ch]["firmware version"] = api.camera_sw_version(ch)
|
IPC_cam[ch]["firmware version"] = api.camera_sw_version(ch)
|
||||||
IPC_cam[ch]["encoding main"] = await api.get_encoding(ch)
|
IPC_cam[ch]["encoding main"] = await api.get_encoding(ch)
|
||||||
if (signal := api.wifi_signal(ch)) is not None:
|
if (signal := api.wifi_signal(ch)) is not None and api.wifi_connection(ch):
|
||||||
IPC_cam[ch]["WiFi signal"] = signal
|
IPC_cam[ch]["WiFi signal"] = signal
|
||||||
|
|
||||||
chimes: dict[int, dict[str, Any]] = {}
|
chimes: dict[int, dict[str, Any]] = {}
|
||||||
@@ -43,7 +43,7 @@ async def async_get_config_entry_diagnostics(
|
|||||||
"HTTP(S) port": api.port,
|
"HTTP(S) port": api.port,
|
||||||
"Baichuan port": api.baichuan.port,
|
"Baichuan port": api.baichuan.port,
|
||||||
"Baichuan only": api.baichuan_only,
|
"Baichuan only": api.baichuan_only,
|
||||||
"WiFi connection": api.wifi_connection,
|
"WiFi connection": api.wifi_connection(),
|
||||||
"WiFi signal": api.wifi_signal(),
|
"WiFi signal": api.wifi_signal(),
|
||||||
"RTMP enabled": api.rtmp_enabled,
|
"RTMP enabled": api.rtmp_enabled,
|
||||||
"RTSP enabled": api.rtsp_enabled,
|
"RTSP enabled": api.rtsp_enabled,
|
||||||
|
@@ -19,5 +19,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["reolink_aio"],
|
"loggers": ["reolink_aio"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["reolink-aio==0.14.5"]
|
"requirements": ["reolink-aio==0.14.6"]
|
||||||
}
|
}
|
||||||
|
@@ -148,7 +148,7 @@ HOST_SENSORS = (
|
|||||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
value=lambda api: api.wifi_signal(),
|
value=lambda api: api.wifi_signal(),
|
||||||
supported=lambda api: api.supported(None, "wifi") and api.wifi_connection,
|
supported=lambda api: api.supported(None, "wifi") and api.wifi_connection(),
|
||||||
),
|
),
|
||||||
ReolinkHostSensorEntityDescription(
|
ReolinkHostSensorEntityDescription(
|
||||||
key="cpu_usage",
|
key="cpu_usage",
|
||||||
|
@@ -109,7 +109,6 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
|||||||
| VacuumEntityFeature.STOP
|
| VacuumEntityFeature.STOP
|
||||||
| VacuumEntityFeature.RETURN_HOME
|
| VacuumEntityFeature.RETURN_HOME
|
||||||
| VacuumEntityFeature.FAN_SPEED
|
| VacuumEntityFeature.FAN_SPEED
|
||||||
| VacuumEntityFeature.BATTERY
|
|
||||||
| VacuumEntityFeature.SEND_COMMAND
|
| VacuumEntityFeature.SEND_COMMAND
|
||||||
| VacuumEntityFeature.LOCATE
|
| VacuumEntityFeature.LOCATE
|
||||||
| VacuumEntityFeature.CLEAN_SPOT
|
| VacuumEntityFeature.CLEAN_SPOT
|
||||||
@@ -142,11 +141,6 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
|||||||
assert self._device_status.state is not None
|
assert self._device_status.state is not None
|
||||||
return STATE_CODE_TO_STATE.get(self._device_status.state)
|
return STATE_CODE_TO_STATE.get(self._device_status.state)
|
||||||
|
|
||||||
@property
|
|
||||||
def battery_level(self) -> int | None:
|
|
||||||
"""Return the battery level of the vacuum cleaner."""
|
|
||||||
return self._device_status.battery
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_speed(self) -> str | None:
|
def fan_speed(self) -> str | None:
|
||||||
"""Return the fan speed of the vacuum cleaner."""
|
"""Return the fan speed of the vacuum cleaner."""
|
||||||
|
@@ -48,4 +48,3 @@ SERVICE_ARCHIVE_PACKAGE = "archive_package"
|
|||||||
ATTR_PACKAGE_STATE = "package_state"
|
ATTR_PACKAGE_STATE = "package_state"
|
||||||
ATTR_PACKAGE_TRACKING_NUMBER = "package_tracking_number"
|
ATTR_PACKAGE_TRACKING_NUMBER = "package_tracking_number"
|
||||||
ATTR_PACKAGE_FRIENDLY_NAME = "package_friendly_name"
|
ATTR_PACKAGE_FRIENDLY_NAME = "package_friendly_name"
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
|
@@ -6,7 +6,7 @@ from pyseventeentrack.package import PACKAGE_STATUS_MAP, Package
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||||
from homeassistant.const import ATTR_FRIENDLY_NAME, ATTR_LOCATION
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, ATTR_FRIENDLY_NAME, ATTR_LOCATION
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall,
|
ServiceCall,
|
||||||
@@ -20,7 +20,6 @@ from homeassistant.util import slugify
|
|||||||
|
|
||||||
from . import SeventeenTrackCoordinator
|
from . import SeventeenTrackCoordinator
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_CONFIG_ENTRY_ID,
|
|
||||||
ATTR_DESTINATION_COUNTRY,
|
ATTR_DESTINATION_COUNTRY,
|
||||||
ATTR_INFO_TEXT,
|
ATTR_INFO_TEXT,
|
||||||
ATTR_ORIGIN_COUNTRY,
|
ATTR_ORIGIN_COUNTRY,
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["soco", "sonos_websocket"],
|
"loggers": ["soco", "sonos_websocket"],
|
||||||
"requirements": ["soco==0.30.9", "sonos-websocket==0.1.3"],
|
"requirements": ["soco==0.30.11", "sonos-websocket==0.1.3"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
|
||||||
|
@@ -6,5 +6,4 @@ from typing import Final
|
|||||||
DOMAIN: Final = "stookwijzer"
|
DOMAIN: Final = "stookwijzer"
|
||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
SERVICE_GET_FORECAST = "get_forecast"
|
SERVICE_GET_FORECAST = "get_forecast"
|
||||||
|
@@ -5,6 +5,7 @@ from typing import Required, TypedDict, cast
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall,
|
ServiceCall,
|
||||||
@@ -13,7 +14,7 @@ from homeassistant.core import (
|
|||||||
)
|
)
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
|
||||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_GET_FORECAST
|
from .const import DOMAIN, SERVICE_GET_FORECAST
|
||||||
from .coordinator import StookwijzerConfigEntry
|
from .coordinator import StookwijzerConfigEntry
|
||||||
|
|
||||||
SERVICE_GET_FORECAST_SCHEMA = vol.Schema(
|
SERVICE_GET_FORECAST_SCHEMA = vol.Schema(
|
||||||
|
@@ -131,13 +131,13 @@ class SunCondition(Condition):
|
|||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def async_validate_condition_config(
|
async def async_validate_config(
|
||||||
cls, hass: HomeAssistant, config: ConfigType
|
cls, hass: HomeAssistant, config: ConfigType
|
||||||
) -> ConfigType:
|
) -> ConfigType:
|
||||||
"""Validate config."""
|
"""Validate config."""
|
||||||
return _CONDITION_SCHEMA(config) # type: ignore[no-any-return]
|
return _CONDITION_SCHEMA(config) # type: ignore[no-any-return]
|
||||||
|
|
||||||
async def async_condition_from_config(self) -> ConditionCheckerType:
|
async def async_get_checker(self) -> ConditionCheckerType:
|
||||||
"""Wrap action method with sun based condition."""
|
"""Wrap action method with sun based condition."""
|
||||||
before = self._config.get("before")
|
before = self._config.get("before")
|
||||||
after = self._config.get("after")
|
after = self._config.get("after")
|
||||||
@@ -153,7 +153,7 @@ class SunCondition(Condition):
|
|||||||
|
|
||||||
|
|
||||||
CONDITIONS: dict[str, type[Condition]] = {
|
CONDITIONS: dict[str, type[Condition]] = {
|
||||||
"sun": SunCondition,
|
"_": SunCondition,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -29,7 +29,6 @@ PLACEHOLDERS = {
|
|||||||
"opendata_url": "http://transport.opendata.ch",
|
"opendata_url": "http://transport.opendata.ch",
|
||||||
}
|
}
|
||||||
|
|
||||||
ATTR_CONFIG_ENTRY_ID: Final = "config_entry_id"
|
|
||||||
ATTR_LIMIT: Final = "limit"
|
ATTR_LIMIT: Final = "limit"
|
||||||
|
|
||||||
SERVICE_FETCH_CONNECTIONS = "fetch_connections"
|
SERVICE_FETCH_CONNECTIONS = "fetch_connections"
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall,
|
ServiceCall,
|
||||||
@@ -19,7 +20,6 @@ from homeassistant.helpers.selector import (
|
|||||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_CONFIG_ENTRY_ID,
|
|
||||||
ATTR_LIMIT,
|
ATTR_LIMIT,
|
||||||
CONNECTIONS_COUNT,
|
CONNECTIONS_COUNT,
|
||||||
CONNECTIONS_MAX,
|
CONNECTIONS_MAX,
|
||||||
|
@@ -41,5 +41,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["switchbot"],
|
"loggers": ["switchbot"],
|
||||||
"quality_scale": "gold",
|
"quality_scale": "gold",
|
||||||
"requirements": ["PySwitchbot==0.68.2"]
|
"requirements": ["PySwitchbot==0.68.3"]
|
||||||
}
|
}
|
||||||
|
@@ -759,7 +759,7 @@
|
|||||||
"data_description": {
|
"data_description": {
|
||||||
"device_id": "[%key:component::template::common::device_id_description%]",
|
"device_id": "[%key:component::template::common::device_id_description%]",
|
||||||
"state": "[%key:component::template::config::step::sensor::data_description::state%]",
|
"state": "[%key:component::template::config::step::sensor::data_description::state%]",
|
||||||
"unit_of_measurement": "[%key:component::template::config::step::sensor::data_description::state%]"
|
"unit_of_measurement": "[%key:component::template::config::step::sensor::data_description::unit_of_measurement%]"
|
||||||
},
|
},
|
||||||
"sections": {
|
"sections": {
|
||||||
"advanced_options": {
|
"advanced_options": {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "traccar_server",
|
"domain": "traccar_server",
|
||||||
"name": "Traccar Server",
|
"name": "Traccar Server",
|
||||||
"codeowners": ["@ludeeus"],
|
"codeowners": [],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/traccar_server",
|
"documentation": "https://www.home-assistant.io/integrations/traccar_server",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
|
@@ -153,11 +153,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool
|
|||||||
# Register known device IDs
|
# Register known device IDs
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
for device in manager.device_map.values():
|
for device in manager.device_map.values():
|
||||||
|
LOGGER.debug(
|
||||||
|
"Register device %s: %s (function: %s, status range: %s)",
|
||||||
|
device.id,
|
||||||
|
device.status,
|
||||||
|
device.function,
|
||||||
|
device.status_range,
|
||||||
|
)
|
||||||
device_registry.async_get_or_create(
|
device_registry.async_get_or_create(
|
||||||
config_entry_id=entry.entry_id,
|
config_entry_id=entry.entry_id,
|
||||||
identifiers={(DOMAIN, device.id)},
|
identifiers={(DOMAIN, device.id)},
|
||||||
manufacturer="Tuya",
|
manufacturer="Tuya",
|
||||||
name=device.name,
|
name=device.name,
|
||||||
|
# Note: the model is overridden via entity.device_info property
|
||||||
|
# when the entity is created. If no entities are generated, it will
|
||||||
|
# stay as unsupported
|
||||||
model=f"{device.product_name} (unsupported)",
|
model=f"{device.product_name} (unsupported)",
|
||||||
model_id=device.product_id,
|
model_id=device.product_id,
|
||||||
)
|
)
|
||||||
@@ -237,6 +247,14 @@ class DeviceListener(SharingDeviceListener):
|
|||||||
# Ensure the device isn't present stale
|
# Ensure the device isn't present stale
|
||||||
self.hass.add_job(self.async_remove_device, device.id)
|
self.hass.add_job(self.async_remove_device, device.id)
|
||||||
|
|
||||||
|
LOGGER.debug(
|
||||||
|
"Add device %s: %s (function: %s, status range: %s)",
|
||||||
|
device.id,
|
||||||
|
device.status,
|
||||||
|
device.function,
|
||||||
|
device.status_range,
|
||||||
|
)
|
||||||
|
|
||||||
dispatcher_send(self.hass, TUYA_DISCOVERY_NEW, [device.id])
|
dispatcher_send(self.hass, TUYA_DISCOVERY_NEW, [device.id])
|
||||||
|
|
||||||
def remove_device(self, device_id: str) -> None:
|
def remove_device(self, device_id: str) -> None:
|
||||||
|
@@ -22,6 +22,7 @@ from . import TuyaConfigEntry
|
|||||||
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
||||||
from .entity import TuyaEntity
|
from .entity import TuyaEntity
|
||||||
from .models import EnumTypeData
|
from .models import EnumTypeData
|
||||||
|
from .util import get_dpcode
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -140,7 +141,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):
|
|||||||
self._master_state = enum_type
|
self._master_state = enum_type
|
||||||
|
|
||||||
# Determine alarm message
|
# Determine alarm message
|
||||||
if dp_code := self.find_dpcode(description.alarm_msg, prefer_function=True):
|
if dp_code := get_dpcode(self.device, description.alarm_msg):
|
||||||
self._alarm_msg_dpcode = dp_code
|
self._alarm_msg_dpcode = dp_code
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@@ -27,6 +27,7 @@ from . import TuyaConfigEntry
|
|||||||
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
||||||
from .entity import TuyaEntity
|
from .entity import TuyaEntity
|
||||||
from .models import IntegerTypeData
|
from .models import IntegerTypeData
|
||||||
|
from .util import get_dpcode
|
||||||
|
|
||||||
TUYA_HVAC_TO_HA = {
|
TUYA_HVAC_TO_HA = {
|
||||||
"auto": HVACMode.HEAT_COOL,
|
"auto": HVACMode.HEAT_COOL,
|
||||||
@@ -229,7 +230,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
|||||||
self._attr_hvac_modes.append(description.switch_only_hvac_mode)
|
self._attr_hvac_modes.append(description.switch_only_hvac_mode)
|
||||||
self._attr_preset_modes = unknown_hvac_modes
|
self._attr_preset_modes = unknown_hvac_modes
|
||||||
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
self._attr_supported_features |= ClimateEntityFeature.PRESET_MODE
|
||||||
elif self.find_dpcode(DPCode.SWITCH, prefer_function=True):
|
elif get_dpcode(self.device, DPCode.SWITCH):
|
||||||
self._attr_hvac_modes = [
|
self._attr_hvac_modes = [
|
||||||
HVACMode.OFF,
|
HVACMode.OFF,
|
||||||
description.switch_only_hvac_mode,
|
description.switch_only_hvac_mode,
|
||||||
@@ -261,24 +262,24 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
|||||||
self._fan_mode_dp_code = enum_type.dpcode
|
self._fan_mode_dp_code = enum_type.dpcode
|
||||||
|
|
||||||
# Determine swing modes
|
# Determine swing modes
|
||||||
if self.find_dpcode(
|
if get_dpcode(
|
||||||
|
self.device,
|
||||||
(
|
(
|
||||||
DPCode.SHAKE,
|
DPCode.SHAKE,
|
||||||
DPCode.SWING,
|
DPCode.SWING,
|
||||||
DPCode.SWITCH_HORIZONTAL,
|
DPCode.SWITCH_HORIZONTAL,
|
||||||
DPCode.SWITCH_VERTICAL,
|
DPCode.SWITCH_VERTICAL,
|
||||||
),
|
),
|
||||||
prefer_function=True,
|
|
||||||
):
|
):
|
||||||
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
|
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
|
||||||
self._attr_swing_modes = [SWING_OFF]
|
self._attr_swing_modes = [SWING_OFF]
|
||||||
if self.find_dpcode((DPCode.SHAKE, DPCode.SWING), prefer_function=True):
|
if get_dpcode(self.device, (DPCode.SHAKE, DPCode.SWING)):
|
||||||
self._attr_swing_modes.append(SWING_ON)
|
self._attr_swing_modes.append(SWING_ON)
|
||||||
|
|
||||||
if self.find_dpcode(DPCode.SWITCH_HORIZONTAL, prefer_function=True):
|
if get_dpcode(self.device, DPCode.SWITCH_HORIZONTAL):
|
||||||
self._attr_swing_modes.append(SWING_HORIZONTAL)
|
self._attr_swing_modes.append(SWING_HORIZONTAL)
|
||||||
|
|
||||||
if self.find_dpcode(DPCode.SWITCH_VERTICAL, prefer_function=True):
|
if get_dpcode(self.device, DPCode.SWITCH_VERTICAL):
|
||||||
self._attr_swing_modes.append(SWING_VERTICAL)
|
self._attr_swing_modes.append(SWING_VERTICAL)
|
||||||
|
|
||||||
if DPCode.SWITCH in self.device.function:
|
if DPCode.SWITCH in self.device.function:
|
||||||
|
@@ -222,6 +222,7 @@ class DPCode(StrEnum):
|
|||||||
HUMIDITY_OUTDOOR_3 = "humidity_outdoor_3" # Outdoor humidity
|
HUMIDITY_OUTDOOR_3 = "humidity_outdoor_3" # Outdoor humidity
|
||||||
HUMIDITY_SET = "humidity_set" # Humidity setting
|
HUMIDITY_SET = "humidity_set" # Humidity setting
|
||||||
HUMIDITY_VALUE = "humidity_value" # Humidity
|
HUMIDITY_VALUE = "humidity_value" # Humidity
|
||||||
|
INSTALLATION_HEIGHT = "installation_height"
|
||||||
IPC_WORK_MODE = "ipc_work_mode"
|
IPC_WORK_MODE = "ipc_work_mode"
|
||||||
LED_TYPE_1 = "led_type_1"
|
LED_TYPE_1 = "led_type_1"
|
||||||
LED_TYPE_2 = "led_type_2"
|
LED_TYPE_2 = "led_type_2"
|
||||||
@@ -232,12 +233,18 @@ class DPCode(StrEnum):
|
|||||||
LEVEL_CURRENT = "level_current"
|
LEVEL_CURRENT = "level_current"
|
||||||
LIGHT = "light" # Light
|
LIGHT = "light" # Light
|
||||||
LIGHT_MODE = "light_mode"
|
LIGHT_MODE = "light_mode"
|
||||||
|
LIQUID_DEPTH = "liquid_depth"
|
||||||
|
LIQUID_DEPTH_MAX = "liquid_depth_max"
|
||||||
|
LIQUID_LEVEL_PERCENT = "liquid_level_percent"
|
||||||
|
LIQUID_STATE = "liquid_state"
|
||||||
LOCK = "lock" # Lock / Child lock
|
LOCK = "lock" # Lock / Child lock
|
||||||
MASTER_MODE = "master_mode" # alarm mode
|
MASTER_MODE = "master_mode" # alarm mode
|
||||||
MASTER_STATE = "master_state" # alarm state
|
MASTER_STATE = "master_state" # alarm state
|
||||||
MACH_OPERATE = "mach_operate"
|
MACH_OPERATE = "mach_operate"
|
||||||
MANUAL_FEED = "manual_feed"
|
MANUAL_FEED = "manual_feed"
|
||||||
MATERIAL = "material" # Material
|
MATERIAL = "material" # Material
|
||||||
|
MAX_SET = "max_set"
|
||||||
|
MINI_SET = "mini_set"
|
||||||
MODE = "mode" # Working mode / Mode
|
MODE = "mode" # Working mode / Mode
|
||||||
MOODLIGHTING = "moodlighting" # Mood light
|
MOODLIGHTING = "moodlighting" # Mood light
|
||||||
MOTION_RECORD = "motion_record"
|
MOTION_RECORD = "motion_record"
|
||||||
|
@@ -23,6 +23,7 @@ from . import TuyaConfigEntry
|
|||||||
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
||||||
from .entity import TuyaEntity
|
from .entity import TuyaEntity
|
||||||
from .models import IntegerTypeData
|
from .models import IntegerTypeData
|
||||||
|
from .util import get_dpcode
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -202,7 +203,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
|||||||
self._attr_supported_features = CoverEntityFeature(0)
|
self._attr_supported_features = CoverEntityFeature(0)
|
||||||
|
|
||||||
# Check if this cover is based on a switch or has controls
|
# Check if this cover is based on a switch or has controls
|
||||||
if self.find_dpcode(description.key, prefer_function=True):
|
if get_dpcode(self.device, description.key):
|
||||||
if device.function[description.key].type == "Boolean":
|
if device.function[description.key].type == "Boolean":
|
||||||
self._attr_supported_features |= (
|
self._attr_supported_features |= (
|
||||||
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
||||||
|
@@ -72,22 +72,17 @@ class TuyaEntity(Entity):
|
|||||||
dptype: Literal[DPType.INTEGER],
|
dptype: Literal[DPType.INTEGER],
|
||||||
) -> IntegerTypeData | None: ...
|
) -> IntegerTypeData | None: ...
|
||||||
|
|
||||||
@overload
|
|
||||||
def find_dpcode(
|
def find_dpcode(
|
||||||
self,
|
self,
|
||||||
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
||||||
*,
|
*,
|
||||||
prefer_function: bool = False,
|
prefer_function: bool = False,
|
||||||
) -> DPCode | None: ...
|
dptype: DPType,
|
||||||
|
) -> EnumTypeData | IntegerTypeData | None:
|
||||||
|
"""Find type information for a matching DP code available for this device."""
|
||||||
|
if dptype not in (DPType.ENUM, DPType.INTEGER):
|
||||||
|
raise NotImplementedError("Only ENUM and INTEGER types are supported")
|
||||||
|
|
||||||
def find_dpcode(
|
|
||||||
self,
|
|
||||||
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
|
|
||||||
*,
|
|
||||||
prefer_function: bool = False,
|
|
||||||
dptype: DPType | None = None,
|
|
||||||
) -> DPCode | EnumTypeData | IntegerTypeData | None:
|
|
||||||
"""Find a matching DP code available on for this device."""
|
|
||||||
if dpcodes is None:
|
if dpcodes is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -100,11 +95,6 @@ class TuyaEntity(Entity):
|
|||||||
if prefer_function:
|
if prefer_function:
|
||||||
order = ["function", "status_range"]
|
order = ["function", "status_range"]
|
||||||
|
|
||||||
# When we are not looking for a specific datatype, we can append status for
|
|
||||||
# searching
|
|
||||||
if not dptype:
|
|
||||||
order.append("status")
|
|
||||||
|
|
||||||
for dpcode in dpcodes:
|
for dpcode in dpcodes:
|
||||||
for key in order:
|
for key in order:
|
||||||
if dpcode not in getattr(self.device, key):
|
if dpcode not in getattr(self.device, key):
|
||||||
@@ -133,9 +123,6 @@ class TuyaEntity(Entity):
|
|||||||
continue
|
continue
|
||||||
return integer_type
|
return integer_type
|
||||||
|
|
||||||
if dptype not in (DPType.ENUM, DPType.INTEGER):
|
|
||||||
return dpcode
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_dptype(
|
def get_dptype(
|
||||||
|
@@ -24,6 +24,7 @@ from . import TuyaConfigEntry
|
|||||||
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
||||||
from .entity import TuyaEntity
|
from .entity import TuyaEntity
|
||||||
from .models import EnumTypeData, IntegerTypeData
|
from .models import EnumTypeData, IntegerTypeData
|
||||||
|
from .util import get_dpcode
|
||||||
|
|
||||||
TUYA_SUPPORT_TYPE = {
|
TUYA_SUPPORT_TYPE = {
|
||||||
# Dehumidifier
|
# Dehumidifier
|
||||||
@@ -90,8 +91,8 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
|
|||||||
"""Init Tuya Fan Device."""
|
"""Init Tuya Fan Device."""
|
||||||
super().__init__(device, device_manager)
|
super().__init__(device, device_manager)
|
||||||
|
|
||||||
self._switch = self.find_dpcode(
|
self._switch = get_dpcode(
|
||||||
(DPCode.SWITCH_FAN, DPCode.FAN_SWITCH, DPCode.SWITCH), prefer_function=True
|
self.device, (DPCode.SWITCH_FAN, DPCode.FAN_SWITCH, DPCode.SWITCH)
|
||||||
)
|
)
|
||||||
|
|
||||||
self._attr_preset_modes = []
|
self._attr_preset_modes = []
|
||||||
@@ -120,8 +121,8 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
|
|||||||
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||||
self._speeds = enum_type
|
self._speeds = enum_type
|
||||||
|
|
||||||
if dpcode := self.find_dpcode(
|
if dpcode := get_dpcode(
|
||||||
(DPCode.SWITCH_HORIZONTAL, DPCode.SWITCH_VERTICAL), prefer_function=True
|
self.device, (DPCode.SWITCH_HORIZONTAL, DPCode.SWITCH_VERTICAL)
|
||||||
):
|
):
|
||||||
self._oscillate = dpcode
|
self._oscillate = dpcode
|
||||||
self._attr_supported_features |= FanEntityFeature.OSCILLATE
|
self._attr_supported_features |= FanEntityFeature.OSCILLATE
|
||||||
|
@@ -21,7 +21,7 @@ from . import TuyaConfigEntry
|
|||||||
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
||||||
from .entity import TuyaEntity
|
from .entity import TuyaEntity
|
||||||
from .models import IntegerTypeData
|
from .models import IntegerTypeData
|
||||||
from .util import ActionDPCodeNotFoundError
|
from .util import ActionDPCodeNotFoundError, get_dpcode
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -105,8 +105,8 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
|
|||||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||||
|
|
||||||
# Determine main switch DPCode
|
# Determine main switch DPCode
|
||||||
self._switch_dpcode = self.find_dpcode(
|
self._switch_dpcode = get_dpcode(
|
||||||
description.dpcode or DPCode(description.key), prefer_function=True
|
self.device, description.dpcode or DPCode(description.key)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Determine humidity parameters
|
# Determine humidity parameters
|
||||||
|
@@ -29,7 +29,7 @@ from . import TuyaConfigEntry
|
|||||||
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType, WorkMode
|
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType, WorkMode
|
||||||
from .entity import TuyaEntity
|
from .entity import TuyaEntity
|
||||||
from .models import IntegerTypeData
|
from .models import IntegerTypeData
|
||||||
from .util import remap_value
|
from .util import get_dpcode, remap_value
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -515,9 +515,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
|||||||
color_modes: set[ColorMode] = {ColorMode.ONOFF}
|
color_modes: set[ColorMode] = {ColorMode.ONOFF}
|
||||||
|
|
||||||
# Determine DPCodes
|
# Determine DPCodes
|
||||||
self._color_mode_dpcode = self.find_dpcode(
|
self._color_mode_dpcode = get_dpcode(self.device, description.color_mode)
|
||||||
description.color_mode, prefer_function=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if int_type := self.find_dpcode(
|
if int_type := self.find_dpcode(
|
||||||
description.brightness, dptype=DPType.INTEGER, prefer_function=True
|
description.brightness, dptype=DPType.INTEGER, prefer_function=True
|
||||||
@@ -532,7 +530,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
dpcode := self.find_dpcode(description.color_data, prefer_function=True)
|
dpcode := get_dpcode(self.device, description.color_data)
|
||||||
) and self.get_dptype(dpcode) == DPType.JSON:
|
) and self.get_dptype(dpcode) == DPType.JSON:
|
||||||
self._color_data_dpcode = dpcode
|
self._color_data_dpcode = dpcode
|
||||||
color_modes.add(ColorMode.HS)
|
color_modes.add(ColorMode.HS)
|
||||||
|
@@ -339,6 +339,32 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
|
|||||||
entity_category=EntityCategory.CONFIG,
|
entity_category=EntityCategory.CONFIG,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
# Tank Level Sensor
|
||||||
|
# Note: Undocumented
|
||||||
|
"ywcgq": (
|
||||||
|
NumberEntityDescription(
|
||||||
|
key=DPCode.MAX_SET,
|
||||||
|
translation_key="alarm_maximum",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
),
|
||||||
|
NumberEntityDescription(
|
||||||
|
key=DPCode.MINI_SET,
|
||||||
|
translation_key="alarm_minimum",
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
),
|
||||||
|
NumberEntityDescription(
|
||||||
|
key=DPCode.INSTALLATION_HEIGHT,
|
||||||
|
translation_key="installation_height",
|
||||||
|
device_class=NumberDeviceClass.DISTANCE,
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
),
|
||||||
|
NumberEntityDescription(
|
||||||
|
key=DPCode.LIQUID_DEPTH_MAX,
|
||||||
|
translation_key="maximum_liquid_depth",
|
||||||
|
device_class=NumberDeviceClass.DISTANCE,
|
||||||
|
entity_category=EntityCategory.CONFIG,
|
||||||
|
),
|
||||||
|
),
|
||||||
# Vibration Sensor
|
# Vibration Sensor
|
||||||
# https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno
|
# https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno
|
||||||
"zd": (
|
"zd": (
|
||||||
|
@@ -985,6 +985,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
|||||||
translation_key="rolling_brush_life",
|
translation_key="rolling_brush_life",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.ELECTRICITY_LEFT,
|
||||||
|
translation_key="battery",
|
||||||
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
# Smart Water Timer
|
# Smart Water Timer
|
||||||
"sfkzq": (
|
"sfkzq": (
|
||||||
@@ -1294,6 +1301,25 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
|||||||
),
|
),
|
||||||
*BATTERY_SENSORS,
|
*BATTERY_SENSORS,
|
||||||
),
|
),
|
||||||
|
# Tank Level Sensor
|
||||||
|
# Note: Undocumented
|
||||||
|
"ywcgq": (
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.LIQUID_STATE,
|
||||||
|
translation_key="liquid_state",
|
||||||
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.LIQUID_DEPTH,
|
||||||
|
translation_key="depth",
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
TuyaSensorEntityDescription(
|
||||||
|
key=DPCode.LIQUID_LEVEL_PERCENT,
|
||||||
|
translation_key="liquid_level",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
),
|
||||||
# Vibration Sensor
|
# Vibration Sensor
|
||||||
# https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno
|
# https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno
|
||||||
"zd": BATTERY_SENSORS,
|
"zd": BATTERY_SENSORS,
|
||||||
|
@@ -213,6 +213,18 @@
|
|||||||
},
|
},
|
||||||
"siren_duration": {
|
"siren_duration": {
|
||||||
"name": "Siren duration"
|
"name": "Siren duration"
|
||||||
|
},
|
||||||
|
"alarm_maximum": {
|
||||||
|
"name": "Alarm maximum"
|
||||||
|
},
|
||||||
|
"alarm_minimum": {
|
||||||
|
"name": "Alarm minimum"
|
||||||
|
},
|
||||||
|
"installation_height": {
|
||||||
|
"name": "Installation height"
|
||||||
|
},
|
||||||
|
"maximum_liquid_depth": {
|
||||||
|
"name": "Maximum liquid depth"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
@@ -711,6 +723,20 @@
|
|||||||
"charging": "[%key:common::state::charging%]",
|
"charging": "[%key:common::state::charging%]",
|
||||||
"charge_done": "Charge done"
|
"charge_done": "Charge done"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"liquid_state": {
|
||||||
|
"name": "Liquid state",
|
||||||
|
"state": {
|
||||||
|
"normal": "[%key:common::state::normal%]",
|
||||||
|
"lower_alarm": "[%key:common::state::low%]",
|
||||||
|
"upper_alarm": "[%key:common::state::high%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"liquid_depth": {
|
||||||
|
"name": "Liquid depth"
|
||||||
|
},
|
||||||
|
"liquid_level": {
|
||||||
|
"name": "Liquid level"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
@@ -758,6 +758,18 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = {
|
|||||||
translation_placeholders={"index": "4"},
|
translation_placeholders={"index": "4"},
|
||||||
device_class=SwitchDeviceClass.OUTLET,
|
device_class=SwitchDeviceClass.OUTLET,
|
||||||
),
|
),
|
||||||
|
SwitchEntityDescription(
|
||||||
|
key=DPCode.SWITCH_5,
|
||||||
|
translation_key="indexed_switch",
|
||||||
|
translation_placeholders={"index": "5"},
|
||||||
|
device_class=SwitchDeviceClass.OUTLET,
|
||||||
|
),
|
||||||
|
SwitchEntityDescription(
|
||||||
|
key=DPCode.SWITCH_6,
|
||||||
|
translation_key="indexed_switch",
|
||||||
|
translation_placeholders={"index": "6"},
|
||||||
|
device_class=SwitchDeviceClass.OUTLET,
|
||||||
|
),
|
||||||
SwitchEntityDescription(
|
SwitchEntityDescription(
|
||||||
key=DPCode.CHILD_LOCK,
|
key=DPCode.CHILD_LOCK,
|
||||||
translation_key="child_lock",
|
translation_key="child_lock",
|
||||||
|
@@ -9,6 +9,29 @@ from homeassistant.exceptions import ServiceValidationError
|
|||||||
from .const import DOMAIN, DPCode
|
from .const import DOMAIN, DPCode
|
||||||
|
|
||||||
|
|
||||||
|
def get_dpcode(
|
||||||
|
device: CustomerDevice, dpcodes: str | DPCode | tuple[DPCode, ...] | None
|
||||||
|
) -> DPCode | None:
|
||||||
|
"""Get the first matching DPCode from the device or return None."""
|
||||||
|
if dpcodes is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if isinstance(dpcodes, DPCode):
|
||||||
|
dpcodes = (dpcodes,)
|
||||||
|
elif isinstance(dpcodes, str):
|
||||||
|
dpcodes = (DPCode(dpcodes),)
|
||||||
|
|
||||||
|
for dpcode in dpcodes:
|
||||||
|
if (
|
||||||
|
dpcode in device.function
|
||||||
|
or dpcode in device.status
|
||||||
|
or dpcode in device.status_range
|
||||||
|
):
|
||||||
|
return dpcode
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def remap_value(
|
def remap_value(
|
||||||
value: float,
|
value: float,
|
||||||
from_min: float = 0,
|
from_min: float = 0,
|
||||||
|
@@ -18,7 +18,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
from . import TuyaConfigEntry
|
from . import TuyaConfigEntry
|
||||||
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
||||||
from .entity import TuyaEntity
|
from .entity import TuyaEntity
|
||||||
from .models import EnumTypeData, IntegerTypeData
|
from .models import EnumTypeData
|
||||||
|
from .util import get_dpcode
|
||||||
|
|
||||||
TUYA_MODE_RETURN_HOME = "chargego"
|
TUYA_MODE_RETURN_HOME = "chargego"
|
||||||
TUYA_STATUS_TO_HA = {
|
TUYA_STATUS_TO_HA = {
|
||||||
@@ -77,7 +78,6 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
|
|||||||
"""Tuya Vacuum Device."""
|
"""Tuya Vacuum Device."""
|
||||||
|
|
||||||
_fan_speed: EnumTypeData | None = None
|
_fan_speed: EnumTypeData | None = None
|
||||||
_battery_level: IntegerTypeData | None = None
|
|
||||||
_attr_name = None
|
_attr_name = None
|
||||||
|
|
||||||
def __init__(self, device: CustomerDevice, device_manager: Manager) -> None:
|
def __init__(self, device: CustomerDevice, device_manager: Manager) -> None:
|
||||||
@@ -89,11 +89,11 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
|
|||||||
self._attr_supported_features = (
|
self._attr_supported_features = (
|
||||||
VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.STATE
|
VacuumEntityFeature.SEND_COMMAND | VacuumEntityFeature.STATE
|
||||||
)
|
)
|
||||||
if self.find_dpcode(DPCode.PAUSE, prefer_function=True):
|
if get_dpcode(self.device, DPCode.PAUSE):
|
||||||
self._attr_supported_features |= VacuumEntityFeature.PAUSE
|
self._attr_supported_features |= VacuumEntityFeature.PAUSE
|
||||||
|
|
||||||
self._return_home_use_switch_charge = False
|
self._return_home_use_switch_charge = False
|
||||||
if self.find_dpcode(DPCode.SWITCH_CHARGE, prefer_function=True):
|
if get_dpcode(self.device, DPCode.SWITCH_CHARGE):
|
||||||
self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME
|
self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME
|
||||||
self._return_home_use_switch_charge = True
|
self._return_home_use_switch_charge = True
|
||||||
elif (
|
elif (
|
||||||
@@ -103,10 +103,10 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
|
|||||||
) and TUYA_MODE_RETURN_HOME in enum_type.range:
|
) and TUYA_MODE_RETURN_HOME in enum_type.range:
|
||||||
self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME
|
self._attr_supported_features |= VacuumEntityFeature.RETURN_HOME
|
||||||
|
|
||||||
if self.find_dpcode(DPCode.SEEK, prefer_function=True):
|
if get_dpcode(self.device, DPCode.SEEK):
|
||||||
self._attr_supported_features |= VacuumEntityFeature.LOCATE
|
self._attr_supported_features |= VacuumEntityFeature.LOCATE
|
||||||
|
|
||||||
if self.find_dpcode(DPCode.POWER_GO, prefer_function=True):
|
if get_dpcode(self.device, DPCode.POWER_GO):
|
||||||
self._attr_supported_features |= (
|
self._attr_supported_features |= (
|
||||||
VacuumEntityFeature.STOP | VacuumEntityFeature.START
|
VacuumEntityFeature.STOP | VacuumEntityFeature.START
|
||||||
)
|
)
|
||||||
@@ -118,19 +118,6 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
|
|||||||
self._attr_fan_speed_list = enum_type.range
|
self._attr_fan_speed_list = enum_type.range
|
||||||
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
|
self._attr_supported_features |= VacuumEntityFeature.FAN_SPEED
|
||||||
|
|
||||||
if int_type := self.find_dpcode(DPCode.ELECTRICITY_LEFT, dptype=DPType.INTEGER):
|
|
||||||
self._attr_supported_features |= VacuumEntityFeature.BATTERY
|
|
||||||
self._battery_level = int_type
|
|
||||||
|
|
||||||
@property
|
|
||||||
def battery_level(self) -> int | None:
|
|
||||||
"""Return Tuya device state."""
|
|
||||||
if self._battery_level is None or not (
|
|
||||||
status := self.device.status.get(DPCode.ELECTRICITY_LEFT)
|
|
||||||
):
|
|
||||||
return None
|
|
||||||
return round(self._battery_level.scale_value(status))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_speed(self) -> str | None:
|
def fan_speed(self) -> str | None:
|
||||||
"""Return the fan speed of the vacuum cleaner."""
|
"""Return the fan speed of the vacuum cleaner."""
|
||||||
|
@@ -8,6 +8,7 @@ from aiowebostv import WebOsClient, WebOsTvPairError
|
|||||||
|
|
||||||
from homeassistant.components import notify as hass_notify
|
from homeassistant.components import notify as hass_notify
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_CONFIG_ENTRY_ID,
|
||||||
CONF_CLIENT_SECRET,
|
CONF_CLIENT_SECRET,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
@@ -20,13 +21,7 @@ from homeassistant.helpers import config_validation as cv, discovery
|
|||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import (
|
from .const import DATA_HASS_CONFIG, DOMAIN, PLATFORMS, WEBOSTV_EXCEPTIONS
|
||||||
ATTR_CONFIG_ENTRY_ID,
|
|
||||||
DATA_HASS_CONFIG,
|
|
||||||
DOMAIN,
|
|
||||||
PLATFORMS,
|
|
||||||
WEBOSTV_EXCEPTIONS,
|
|
||||||
)
|
|
||||||
from .helpers import WebOsTvConfigEntry, update_client_key
|
from .helpers import WebOsTvConfigEntry, update_client_key
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
@@ -13,7 +13,6 @@ DATA_HASS_CONFIG = "hass_config"
|
|||||||
DEFAULT_NAME = "LG webOS TV"
|
DEFAULT_NAME = "LG webOS TV"
|
||||||
|
|
||||||
ATTR_BUTTON = "button"
|
ATTR_BUTTON = "button"
|
||||||
ATTR_CONFIG_ENTRY_ID = "entry_id"
|
|
||||||
ATTR_PAYLOAD = "payload"
|
ATTR_PAYLOAD = "payload"
|
||||||
ATTR_SOUND_OUTPUT = "sound_output"
|
ATTR_SOUND_OUTPUT = "sound_output"
|
||||||
|
|
||||||
|
@@ -7,13 +7,13 @@ from typing import Any
|
|||||||
from aiowebostv import WebOsClient
|
from aiowebostv import WebOsClient
|
||||||
|
|
||||||
from homeassistant.components.notify import ATTR_DATA, BaseNotificationService
|
from homeassistant.components.notify import ATTR_DATA, BaseNotificationService
|
||||||
from homeassistant.const import ATTR_ICON
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, ATTR_ICON
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from . import WebOsTvConfigEntry
|
from . import WebOsTvConfigEntry
|
||||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, WEBOSTV_EXCEPTIONS
|
from .const import DOMAIN, WEBOSTV_EXCEPTIONS
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["holidays"],
|
"loggers": ["holidays"],
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["holidays==0.77"]
|
"requirements": ["holidays==0.78"]
|
||||||
}
|
}
|
||||||
|
@@ -13,5 +13,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/yale",
|
"documentation": "https://www.home-assistant.io/integrations/yale",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["socketio", "engineio", "yalexs"],
|
"loggers": ["socketio", "engineio", "yalexs"],
|
||||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"]
|
"requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"]
|
||||||
}
|
}
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
"zha",
|
"zha",
|
||||||
"universal_silabs_flasher"
|
"universal_silabs_flasher"
|
||||||
],
|
],
|
||||||
"requirements": ["zha==0.0.65"],
|
"requirements": ["zha==0.0.67"],
|
||||||
"usb": [
|
"usb": [
|
||||||
{
|
{
|
||||||
"vid": "10C4",
|
"vid": "10C4",
|
||||||
|
@@ -16,6 +16,7 @@ rules:
|
|||||||
status: done
|
status: done
|
||||||
comment: |
|
comment: |
|
||||||
https://mark_hannon@bitbucket.org/mark_hannon/zcc.git
|
https://mark_hannon@bitbucket.org/mark_hannon/zcc.git
|
||||||
|
https://bitbucket.org/mark_hannon/zcc/src/master/bitbucket-pipelines.yml
|
||||||
docs-actions:
|
docs-actions:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
|
@@ -100,13 +100,13 @@ class ZoneCondition(Condition):
|
|||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def async_validate_condition_config(
|
async def async_validate_config(
|
||||||
cls, hass: HomeAssistant, config: ConfigType
|
cls, hass: HomeAssistant, config: ConfigType
|
||||||
) -> ConfigType:
|
) -> ConfigType:
|
||||||
"""Validate config."""
|
"""Validate config."""
|
||||||
return _CONDITION_SCHEMA(config) # type: ignore[no-any-return]
|
return _CONDITION_SCHEMA(config) # type: ignore[no-any-return]
|
||||||
|
|
||||||
async def async_condition_from_config(self) -> ConditionCheckerType:
|
async def async_get_checker(self) -> ConditionCheckerType:
|
||||||
"""Wrap action method with zone based condition."""
|
"""Wrap action method with zone based condition."""
|
||||||
entity_ids = self._config.get(CONF_ENTITY_ID, [])
|
entity_ids = self._config.get(CONF_ENTITY_ID, [])
|
||||||
zone_entity_ids = self._config.get(CONF_ZONE, [])
|
zone_entity_ids = self._config.get(CONF_ZONE, [])
|
||||||
@@ -147,7 +147,7 @@ class ZoneCondition(Condition):
|
|||||||
|
|
||||||
|
|
||||||
CONDITIONS: dict[str, type[Condition]] = {
|
CONDITIONS: dict[str, type[Condition]] = {
|
||||||
"zone": ZoneCondition,
|
"_": ZoneCondition,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -92,7 +92,6 @@ ATTR_CURRENT_VALUE = "current_value"
|
|||||||
ATTR_CURRENT_VALUE_RAW = "current_value_raw"
|
ATTR_CURRENT_VALUE_RAW = "current_value_raw"
|
||||||
ATTR_DESCRIPTION = "description"
|
ATTR_DESCRIPTION = "description"
|
||||||
ATTR_EVENT_SOURCE = "event_source"
|
ATTR_EVENT_SOURCE = "event_source"
|
||||||
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
|
||||||
ATTR_PARTIAL_DICT_MATCH = "partial_dict_match"
|
ATTR_PARTIAL_DICT_MATCH = "partial_dict_match"
|
||||||
|
|
||||||
# service constants
|
# service constants
|
||||||
|
@@ -11,7 +11,12 @@ from zwave_js_server.model.controller import CONTROLLER_EVENT_MODEL_MAP
|
|||||||
from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP, Driver
|
from zwave_js_server.model.driver import DRIVER_EVENT_MODEL_MAP, Driver
|
||||||
from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP
|
from zwave_js_server.model.node import NODE_EVENT_MODEL_MAP
|
||||||
|
|
||||||
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID, CONF_PLATFORM
|
from homeassistant.const import (
|
||||||
|
ATTR_CONFIG_ENTRY_ID,
|
||||||
|
ATTR_DEVICE_ID,
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_PLATFORM,
|
||||||
|
)
|
||||||
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
|
||||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
@@ -19,7 +24,6 @@ from homeassistant.helpers.trigger import Trigger, TriggerActionType, TriggerInf
|
|||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from ..const import (
|
from ..const import (
|
||||||
ATTR_CONFIG_ENTRY_ID,
|
|
||||||
ATTR_EVENT,
|
ATTR_EVENT,
|
||||||
ATTR_EVENT_DATA,
|
ATTR_EVENT_DATA,
|
||||||
ATTR_EVENT_SOURCE,
|
ATTR_EVENT_SOURCE,
|
||||||
@@ -263,13 +267,13 @@ class EventTrigger(Trigger):
|
|||||||
self._hass = hass
|
self._hass = hass
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def async_validate_trigger_config(
|
async def async_validate_config(
|
||||||
cls, hass: HomeAssistant, config: ConfigType
|
cls, hass: HomeAssistant, config: ConfigType
|
||||||
) -> ConfigType:
|
) -> ConfigType:
|
||||||
"""Validate config."""
|
"""Validate config."""
|
||||||
return await async_validate_trigger_config(hass, config)
|
return await async_validate_trigger_config(hass, config)
|
||||||
|
|
||||||
async def async_attach_trigger(
|
async def async_attach(
|
||||||
self,
|
self,
|
||||||
action: TriggerActionType,
|
action: TriggerActionType,
|
||||||
trigger_info: TriggerInfo,
|
trigger_info: TriggerInfo,
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
"""Helpers for Z-Wave JS custom triggers."""
|
"""Helpers for Z-Wave JS custom triggers."""
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import ATTR_DEVICE_ID, ATTR_ENTITY_ID
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID, ATTR_DEVICE_ID, ATTR_ENTITY_ID
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from ..const import ATTR_CONFIG_ENTRY_ID, DOMAIN
|
from ..const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user