Merge branch 'dev_target_triggers_conditions' of github.com:home-assistant/core into target_trigger

This commit is contained in:
abmantis
2025-08-07 11:40:38 +01:00
170 changed files with 6866 additions and 868 deletions

View File

@@ -175,7 +175,7 @@ jobs:
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: translations
@@ -462,7 +462,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: translations

View File

@@ -970,7 +970,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: pytest_buckets
- name: Compile English translations
@@ -1336,7 +1336,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1486,7 +1486,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1511,7 +1511,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
pattern: test-results-*
- name: Upload test results to Codecov

View File

@@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
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:
model: openai/gpt-4o
system-prompt: |

View File

@@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@v1.2.7
uses: actions/ai-inference@v1.2.8
with:
model: openai/gpt-4o-mini
system-prompt: |

View File

@@ -138,17 +138,17 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Download env_file
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: requirements_diff
@@ -187,22 +187,22 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Download env_file
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: requirements_diff
- name: Download requirements_all_wheels
uses: actions/download-artifact@v4.3.0
uses: actions/download-artifact@v5.0.0
with:
name: requirements_all_wheels

2
CODEOWNERS generated
View File

@@ -1613,8 +1613,6 @@ build.json @home-assistant/supervisor
/tests/components/tplink_omada/ @MarkGodwin
/homeassistant/components/traccar/ @ludeeus
/tests/components/traccar/ @ludeeus
/homeassistant/components/traccar_server/ @ludeeus
/tests/components/traccar_server/ @ludeeus
/homeassistant/components/trace/ @home-assistant/core
/tests/components/trace/ @home-assistant/core
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu

View File

@@ -9,7 +9,6 @@ DOMAIN: Final = "amberelectric"
CONF_SITE_NAME = "site_name"
CONF_SITE_ID = "site_id"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_CHANNEL_TYPE = "channel_type"
ATTRIBUTION = "Data provided by Amber Electric"

View File

@@ -4,6 +4,7 @@ from amberelectric.models.channel import ChannelType
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import (
HomeAssistant,
ServiceCall,
@@ -16,7 +17,6 @@ from homeassistant.util.json import JsonValueType
from .const import (
ATTR_CHANNEL_TYPE,
ATTR_CONFIG_ENTRY_ID,
CONTROLLED_LOAD_CHANNEL,
DOMAIN,
FEED_IN_CHANNEL,

View File

@@ -81,11 +81,15 @@ async def async_update_options(
async def async_migrate_integration(hass: HomeAssistant) -> None:
"""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):
return
api_keys_entries: dict[str, ConfigEntry] = {}
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
entity_registry = er.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:
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)
conversation_entity = entity_registry.async_get_entity_id(
conversation_entity_id = entity_registry.async_get_entity_id(
"conversation",
DOMAIN,
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(
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:
# 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.id,
disabled_by=device_disabled_by,
new_identifiers={(DOMAIN, subentry.subentry_id)},
add_config_subentry_id=subentry.subentry_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,
options={},
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)
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(
"Migration to version %s:%s successful", entry.version, entry.minor_version
)

View File

@@ -75,7 +75,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Anthropic."""
VERSION = 2
MINOR_VERSION = 2
MINOR_VERSION = 3
async def async_step_user(
self, user_input: dict[str, Any] | None = None

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"]
"requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"]
}

View File

@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==64"],
"requirements": ["axis==65"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@@ -25,7 +25,6 @@ SERVICE_TRIGGER = "trigger_camera"
SERVICE_SAVE_VIDEO = "save_video"
SERVICE_SAVE_RECENT_CLIPS = "save_recent_clips"
SERVICE_SEND_PIN = "send_pin"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,

View File

@@ -5,12 +5,12 @@ from __future__ import annotations
import voluptuous as vol
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.exceptions import HomeAssistantError, ServiceValidationError
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
SERVICE_SEND_PIN_SCHEMA = vol.Schema(

View File

@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.44.3",
"habluetooth==4.0.1"
"habluetooth==4.0.2"
]
}

View File

@@ -6,4 +6,3 @@ CONF_INSTALLER_CODE = "installer_code"
CONF_USER_CODE = "user_code"
ATTR_DATETIME = "datetime"
SERVICE_SET_DATE_TIME = "set_date_time"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"

View File

@@ -9,12 +9,13 @@ from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
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

View File

@@ -100,16 +100,10 @@ set_hvac_mode:
fields:
hvac_mode:
selector:
select:
options:
- "off"
- "auto"
- "cool"
- "dry"
- "fan_only"
- "heat_cool"
- "heat"
translation_key: hvac_mode
state:
hide_states:
- unavailable
- unknown
set_swing_mode:
target:
entity:

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.111.0"],
"requirements": ["hass-nabucasa==0.111.1"],
"single_config_entry": true
}

View File

@@ -61,7 +61,7 @@ class DeviceCondition(Condition):
self._hass = hass
@classmethod
async def async_validate_condition_config(
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate device condition config."""
@@ -69,7 +69,7 @@ class DeviceCondition(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."""
platform = await async_get_device_automation_platform(
self._hass, self._config[CONF_DOMAIN], DeviceAutomationType.CONDITION
@@ -80,7 +80,7 @@ class DeviceCondition(Condition):
CONDITIONS: dict[str, type[Condition]] = {
"device": DeviceCondition,
"_device": DeviceCondition,
}

View File

@@ -20,7 +20,6 @@ from homeassistant.const import Platform
_LOGGER = logging.getLogger(__package__)
DOMAIN = "ecobee"
ATTR_CONFIG_ENTRY_ID = "entry_id"
ATTR_AVAILABLE_SENSORS = "available_sensors"
ATTR_ACTIVE_SENSORS = "active_sensors"

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250805.0"]
"requirements": ["home-assistant-frontend==20250806.0"]
}

View File

@@ -86,9 +86,11 @@ UNSUPPORTED_REASONS = {
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
UNHEALTHY_REASONS = {
"docker",
"supervisor",
"setup",
"duplicate_os_installation",
"oserror_bad_message",
"privileged",
"setup",
"supervisor",
"untrusted",
}

View File

@@ -117,35 +117,43 @@
},
"unhealthy": {
"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": {
"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": {
"title": "Unhealthy system - Supervisor update failed",
"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."
"unhealthy_duplicate_os_installation": {
"description": "System is currently unhealthy because it has detected multiple Home Assistant OS installations. For troubleshooting information, select Learn more.",
"title": "Unhealthy system - Duplicate Home Assistant OS installation"
},
"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, use the link to learn more and how to fix this."
"unhealthy_oserror_bad_message": {
"description": "System is currently unhealthy because the operating system has reported an OS error: Bad message. For troubleshooting information, select Learn more.",
"title": "Unhealthy system - Operating System error: Bad message"
},
"unhealthy_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": {
"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": {
"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": {
"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": {
"title": "Unsupported system - CGroup version",
@@ -153,23 +161,23 @@
},
"unsupported_connectivity_check": {
"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": {
"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": {
"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": {
"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": {
"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": {
"title": "Unsupported system - Docker version",
@@ -177,15 +185,15 @@
},
"unsupported_job_conditions": {
"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": {
"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": {
"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": {
"title": "Unsupported system - Operating System",
@@ -193,43 +201,43 @@
},
"unsupported_os_agent": {
"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": {
"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": {
"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": {
"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": {
"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": {
"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": {
"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": {
"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": {
"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": {
"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": {

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.77", "babel==2.15.0"]
"requirements": ["holidays==0.78", "babel==2.15.0"]
}

View File

@@ -24,6 +24,7 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_HW_VERSION,
ATTR_MODEL,
ATTR_SW_VERSION,
@@ -54,7 +55,6 @@ from homeassistant.helpers.typing import ConfigType
from .const import (
ADMIN_SERVICES,
ALL_KEYS,
ATTR_CONFIG_ENTRY_ID,
CONF_MANUFACTURER,
CONF_UNAUTHENTICATED_MODE,
CONF_UPNP_UDN,

View File

@@ -2,8 +2,6 @@
DOMAIN = "huawei_lte"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
CONF_MANUFACTURER = "manufacturer"
CONF_TRACK_WIRED_CLIENTS = "track_wired_clients"
CONF_UNAUTHENTICATED_MODE = "unauthenticated_mode"

View File

@@ -8,12 +8,12 @@ from typing import Any
from huawei_lte_api.exceptions import ResponseErrorException
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.helpers.typing import ConfigType, DiscoveryInfoType
from . import Router
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)

View File

@@ -42,10 +42,19 @@
"local_name": "Ink@IAM-T1",
"connectable": true
},
{
"local_name": "Ink@IAM-T2",
"connectable": true
},
{
"manufacturer_id": 12628,
"manufacturer_data_start": [65, 67, 45],
"connectable": true
},
{
"manufacturer_id": 12884,
"manufacturer_data_start": [0, 98, 0],
"connectable": false
}
],
"codeowners": ["@bdraco"],
@@ -53,5 +62,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/inkbird",
"iot_class": "local_push",
"requirements": ["inkbird-ble==0.16.2"]
"requirements": ["inkbird-ble==1.1.0"]
}

View File

@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.8.0",
"xknxproject==3.8.2",
"knx-frontend==2025.8.4.154919"
"knx-frontend==2025.8.6.52906"
],
"single_config_entry": true
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["letpot"],
"quality_scale": "bronze",
"requirements": ["letpot==0.5.0"]
"requirements": ["letpot==0.6.1"]
}

View File

@@ -12,7 +12,6 @@ DATA_HASS_CONFIG = "mastodon_hass_config"
DEFAULT_URL: Final = "https://mastodon.social"
DEFAULT_NAME: Final = "Mastodon"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_STATUS = "status"
ATTR_VISIBILITY = "visibility"
ATTR_CONTENT_WARNING = "content_warning"

View File

@@ -9,11 +9,11 @@ from mastodon.Mastodon import MastodonAPIError, MediaAttachment
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import HomeAssistant, ServiceCall, ServiceResponse
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from .const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_CONTENT_WARNING,
ATTR_MEDIA,
ATTR_MEDIA_DESCRIPTION,

View File

@@ -99,6 +99,9 @@
"esa_opt_out_state": {
"default": "mdi:home-lightning-bolt"
},
"esa_state": {
"default": "mdi:home-lightning-bolt"
},
"evse_state": {
"default": "mdi:ev-station"
},

View File

@@ -140,11 +140,6 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
def _update_from_device(self) -> None:
"""Update from device."""
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
run_mode_raw: int = self.get_matter_attribute_value(
clusters.RvcRunMode.Attributes.CurrentMode
@@ -188,11 +183,6 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
supported_features |= VacuumEntityFeature.STATE
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)
if self.get_matter_attribute_value(clusters.Identify.Attributes.IdentifyType):
supported_features |= VacuumEntityFeature.LOCATE
@@ -230,7 +220,6 @@ DISCOVERY_SCHEMAS = [
clusters.RvcRunMode.Attributes.CurrentMode,
clusters.RvcOperationalState.Attributes.OperationalState,
),
optional_attributes=(clusters.PowerSource.Attributes.BatPercentRemaining,),
device_type=(device_types.RoboticVacuumCleaner,),
allow_none_value=True,
),

View File

@@ -8,7 +8,6 @@ DOMAIN = "mealie"
LOGGER = logging.getLogger(__package__)
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_START_DATE = "start_date"
ATTR_END_DATE = "end_date"
ATTR_RECIPE_ID = "recipe_id"

View File

@@ -13,7 +13,7 @@ from aiomealie import (
import voluptuous as vol
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 (
HomeAssistant,
ServiceCall,
@@ -25,7 +25,6 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from .const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_END_DATE,
ATTR_ENTRY_TYPE,
ATTR_INCLUDE_TAGS,

View File

@@ -25,7 +25,6 @@ ATTR_APP_DATA = "app_data"
ATTR_APP_ID = "app_id"
ATTR_APP_NAME = "app_name"
ATTR_APP_VERSION = "app_version"
ATTR_CONFIG_ENTRY_ID = "entry_id"
ATTR_DEVICE_NAME = "device_name"
ATTR_MANUFACTURER = "manufacturer"
ATTR_MODEL = "model"

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/modbus",
"iot_class": "local_polling",
"loggers": ["pymodbus"],
"requirements": ["pymodbus==3.9.2"]
"requirements": ["pymodbus==3.11.0"]
}

View File

@@ -1,5 +1,9 @@
{
"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": {
"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."

View File

@@ -17,7 +17,7 @@ from homeassistant.components.vacuum import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
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.json import json_dumps
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, VolSchemaType
@@ -25,11 +25,11 @@ from homeassistant.util.json import json_loads_object
from . import subscription
from .config import MQTT_BASE_SCHEMA
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC
from .entity import MqttEntity, async_setup_entity_entry_helper
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN
from .entity import IssueSeverity, MqttEntity, async_setup_entity_entry_helper
from .models import ReceiveMessage
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
@@ -84,6 +84,8 @@ SERVICE_TO_STRING: dict[VacuumEntityFeature, str] = {
VacuumEntityFeature.STOP: "stop",
VacuumEntityFeature.RETURN_HOME: "return_home",
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.STATUS: "status",
VacuumEntityFeature.SEND_COMMAND: "send_command",
@@ -96,7 +98,6 @@ DEFAULT_SERVICES = (
VacuumEntityFeature.START
| VacuumEntityFeature.STOP
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.BATTERY
| VacuumEntityFeature.CLEAN_SPOT
)
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:
"""Update the entity state attributes."""
self._state_attrs.update(payload)
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)))
@callback

View File

@@ -8,6 +8,7 @@ from music_assistant_models.enums import MediaType
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import (
HomeAssistant,
ServiceCall,
@@ -24,7 +25,6 @@ from .const import (
ATTR_ALBUMS,
ATTR_ARTISTS,
ATTR_AUDIOBOOKS,
ATTR_CONFIG_ENTRY_ID,
ATTR_FAVORITE,
ATTR_ITEMS,
ATTR_LIBRARY_ONLY,

View File

@@ -26,7 +26,6 @@ ATTR_OFFSET = "offset"
ATTR_ORDER_BY = "order_by"
ATTR_ALBUM_TYPE = "album_type"
ATTR_ALBUM_ARTISTS_ONLY = "album_artists_only"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_URI = "uri"
ATTR_IMAGE = "image"
ATTR_VERSION = "version"

View File

@@ -92,11 +92,15 @@ async def async_update_options(hass: HomeAssistant, entry: OllamaConfigEntry) ->
async def async_migrate_integration(hass: HomeAssistant) -> None:
"""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):
return
api_keys_entries: dict[str, ConfigEntry] = {}
url_entries: dict[str, tuple[ConfigEntry, bool]] = {}
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
@@ -112,33 +116,64 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
title=entry.title,
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
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)
conversation_entity = entity_registry.async_get_entity_id(
conversation_entity_id = entity_registry.async_get_entity_id(
"conversation",
DOMAIN,
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(
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:
# 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.id,
disabled_by=device_disabled_by,
new_identifiers={(DOMAIN, subentry.subentry_id)},
add_config_subentry_id=subentry.subentry_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:
await hass.config_entries.async_remove(entry.entry_id)
else:
_add_ai_task_subentry(hass, entry)
hass.config_entries.async_update_entry(
entry,
title=DEFAULT_NAME,
@@ -165,7 +201,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
data={CONF_URL: entry.data[CONF_URL]},
options={},
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:
# 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,
),
)
_add_ai_task_subentry(hass, entry)
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(
"Migration to version %s:%s successful", entry.version, entry.minor_version
)
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,
),
)

View File

@@ -76,7 +76,7 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Ollama."""
VERSION = 3
MINOR_VERSION = 2
MINOR_VERSION = 3
def __init__(self) -> None:
"""Initialize config flow."""

View File

@@ -53,8 +53,6 @@ class OneWireEntity(Entity):
"""Return the state attributes of the entity."""
return {
"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:

View File

@@ -272,11 +272,15 @@ async def async_update_options(hass: HomeAssistant, entry: OpenAIConfigEntry) ->
async def async_migrate_integration(hass: HomeAssistant) -> None:
"""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):
return
api_keys_entries: dict[str, ConfigEntry] = {}
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
entity_registry = er.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:
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)
conversation_entity = entity_registry.async_get_entity_id(
conversation_entity_id = entity_registry.async_get_entity_id(
"conversation",
DOMAIN,
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(
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:
# 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.id,
disabled_by=device_disabled_by,
new_identifiers={(DOMAIN, subentry.subentry_id)},
add_config_subentry_id=subentry.subentry_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:
await hass.config_entries.async_remove(entry.entry_id)
else:
_add_ai_task_subentry(hass, entry)
hass.config_entries.async_update_entry(
entry,
title=DEFAULT_NAME,
options={},
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)
if entry.version == 2 and entry.minor_version == 2:
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,
),
)
_add_ai_task_subentry(hass, entry)
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(
"Migration to version %s:%s successful", entry.version, entry.minor_version
)
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,
),
)

View File

@@ -98,7 +98,7 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for OpenAI Conversation."""
VERSION = 2
MINOR_VERSION = 3
MINOR_VERSION = 4
async def async_step_user(
self, user_input: dict[str, Any] | None = None

View File

@@ -9,6 +9,8 @@ from typing import Any
from opower import (
CannotConnect,
InvalidAuth,
MfaChallenge,
MfaHandlerBase,
Opower,
create_cookie_jar,
get_supported_utility_names,
@@ -16,49 +18,34 @@ from opower import (
)
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.core import HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_create_clientsession
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__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_UTILITY): vol.In(get_supported_utility_names()),
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
CONF_MFA_CODE = "mfa_code"
CONF_MFA_METHOD = "mfa_method"
async def _validate_login(
hass: HomeAssistant, login_data: dict[str, str]
) -> dict[str, str]:
"""Validate login data and return any errors."""
hass: HomeAssistant,
data: Mapping[str, Any],
) -> None:
"""Validate login data and raise exceptions on failure."""
api = Opower(
async_create_clientsession(hass, cookie_jar=create_cookie_jar()),
login_data[CONF_UTILITY],
login_data[CONF_USERNAME],
login_data[CONF_PASSWORD],
login_data.get(CONF_TOTP_SECRET),
data[CONF_UTILITY],
data[CONF_USERNAME],
data[CONF_PASSWORD],
data.get(CONF_TOTP_SECRET),
data.get(CONF_LOGIN_DATA),
)
errors: dict[str, str] = {}
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
await api.async_login()
class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -68,81 +55,147 @@ class OpowerConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""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(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
"""Handle the initial step (select utility)."""
if user_input is not None:
self._async_abort_entries_match(
{
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()
self._data[CONF_UTILITY] = user_input[CONF_UTILITY]
return await self.async_step_credentials()
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(
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(
STEP_USER_DATA_SCHEMA, user_input
vol.Schema(schema_dict), user_input
),
errors=errors,
)
async def async_step_mfa(
async def async_step_mfa_options(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle MFA step."""
assert self.utility_info is not None
"""Handle MFA options step."""
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] = {}
if user_input is not None:
data = {**self.utility_info, **user_input}
errors = await _validate_login(self.hass, data)
if not errors:
return self._async_create_opower_entry(data)
if errors:
schema = {
vol.Required(
CONF_USERNAME, default=self.utility_info[CONF_USERNAME]
): str,
vol.Required(CONF_PASSWORD): str,
}
else:
schema = {}
schema[vol.Required(CONF_TOTP_SECRET)] = str
code = user_input[CONF_MFA_CODE]
try:
login_data = await self.mfa_handler.async_submit_mfa_code(code)
except InvalidAuth:
errors["base"] = "invalid_mfa_code"
except CannotConnect:
errors["base"] = "cannot_connect"
else:
self._data[CONF_LOGIN_DATA] = login_data
if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data=self._data
)
return self._async_create_opower_entry(self._data)
return self.async_show_form(
step_id="mfa",
data_schema=vol.Schema(schema),
step_id="mfa_code",
data_schema=self.add_suggested_values_to_schema(
vol.Schema({vol.Required(CONF_MFA_CODE): str}), user_input
),
errors=errors,
)
@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."""
return self.async_create_entry(
title=f"{data[CONF_UTILITY]} ({data[CONF_USERNAME]})",
data=data,
**kwargs,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""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(
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."""
errors: dict[str, str] = {}
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 = {
vol.Required(CONF_USERNAME): reauth_entry.data[CONF_USERNAME],
if user_input is not None:
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,
}
if select_utility(reauth_entry.data[CONF_UTILITY]).accepts_mfa():
schema[vol.Optional(CONF_TOTP_SECRET)] = str
if utility.accepts_totp_secret():
schema_dict[vol.Optional(CONF_TOTP_SECRET)] = str
return self.async_show_form(
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,
description_placeholders={CONF_NAME: reauth_entry.title},
)

View File

@@ -4,3 +4,4 @@ DOMAIN = "opower"
CONF_UTILITY = "utility"
CONF_TOTP_SECRET = "totp_secret"
CONF_LOGIN_DATA = "login_data"

View File

@@ -14,7 +14,7 @@ from opower import (
ReadResolution,
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.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.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__)
@@ -69,6 +69,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
config_entry.data[CONF_USERNAME],
config_entry.data[CONF_PASSWORD],
config_entry.data.get(CONF_TOTP_SECRET),
config_entry.data.get(CONF_LOGIN_DATA),
)
@callback
@@ -90,7 +91,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
# Given the infrequent updating (every 12h)
# assume previous session has expired and re-login.
await self.api.async_login()
except InvalidAuth as err:
except (InvalidAuth, MfaChallenge) as err:
_LOGGER.error("Error during login: %s", err)
raise ConfigEntryAuthFailed from err
except CannotConnect as err:

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.12.4"]
"requirements": ["opower==0.15.1"]
}

View File

@@ -3,27 +3,43 @@
"step": {
"user": {
"data": {
"utility": "Utility name",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
"utility": "Utility name"
},
"data_description": {
"utility": "The name of your utility provider",
"username": "The username for your utility account",
"password": "The password for your utility account"
"utility": "The name of your utility provider"
}
},
"mfa": {
"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.",
"credentials": {
"title": "Enter Credentials",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"totp_secret": "TOTP secret"
},
"data_description": {
"username": "[%key:component::opower::config::step::user::data_description::username%]",
"password": "[%key:component::opower::config::step::user::data_description::password%]",
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
"username": "The username for your utility account",
"password": "The password for your utility account",
"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": {
@@ -31,18 +47,19 @@
"data": {
"username": "[%key:common::config_flow::data::username%]",
"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": {
"username": "[%key:component::opower::config::step::user::data_description::username%]",
"password": "[%key:component::opower::config::step::user::data_description::password%]",
"totp_secret": "The TOTP secret for your utility account, used for multi-factor authentication (MFA)."
"username": "[%key:component::opower::config::step::credentials::data_description::username%]",
"password": "[%key:component::opower::config::step::credentials::data_description::password%]",
"totp_secret": "[%key:component::opower::config::step::credentials::data_description::totp_secret%]"
}
}
},
"error": {
"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": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",

View File

@@ -9,7 +9,6 @@ LOGGER = logging.getLogger(__package__)
REQUESTS = "requests"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_STATUS = "status"
ATTR_SORT_ORDER = "sort_order"
ATTR_REQUESTED_BY = "requested_by"

View File

@@ -7,6 +7,7 @@ from python_overseerr import OverseerrClient, OverseerrConnectionError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import (
HomeAssistant,
ServiceCall,
@@ -17,14 +18,7 @@ from homeassistant.core import (
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.util.json import JsonValueType
from .const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_REQUESTED_BY,
ATTR_SORT_ORDER,
ATTR_STATUS,
DOMAIN,
LOGGER,
)
from .const import ATTR_REQUESTED_BY, ATTR_SORT_ORDER, ATTR_STATUS, DOMAIN, LOGGER
from .coordinator import OverseerrConfigEntry
SERVICE_GET_REQUESTS = "get_requests"

View File

@@ -9,7 +9,6 @@ CONF_COORDINATOR = "coordinator"
SERVICE_ADD_PRODUCT_TO_CART = "add_product"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_PRODUCT_ID = "product_id"
ATTR_PRODUCT_NAME = "product_name"
ATTR_AMOUNT = "amount"

View File

@@ -7,12 +7,12 @@ from typing import cast
from python_picnic_api2 import PicnicAPI
import voluptuous as vol
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from .const import (
ATTR_AMOUNT,
ATTR_CONFIG_ENTRY_ID,
ATTR_PRODUCT_ID,
ATTR_PRODUCT_IDENTIFIERS,
ATTR_PRODUCT_NAME,

View File

@@ -8,6 +8,5 @@ CONF_SERIAL_NUMBER = "serial_number"
CONF_IMPORTED_NAMES = "imported_names"
ATTR_DURATION = "duration"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
TIMEOUT_SECONDS = 20

View File

@@ -59,7 +59,7 @@ PLATFORMS = [
Platform.UPDATE,
]
DEVICE_UPDATE_INTERVAL = timedelta(seconds=60)
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=12)
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=24)
NUM_CRED_ERRORS = 3
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)

View File

@@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics(
IPC_cam[ch]["hardware version"] = api.camera_hardware_version(ch)
IPC_cam[ch]["firmware version"] = api.camera_sw_version(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
chimes: dict[int, dict[str, Any]] = {}
@@ -43,7 +43,7 @@ async def async_get_config_entry_diagnostics(
"HTTP(S) port": api.port,
"Baichuan port": api.baichuan.port,
"Baichuan only": api.baichuan_only,
"WiFi connection": api.wifi_connection,
"WiFi connection": api.wifi_connection(),
"WiFi signal": api.wifi_signal(),
"RTMP enabled": api.rtmp_enabled,
"RTSP enabled": api.rtsp_enabled,

View File

@@ -19,5 +19,5 @@
"iot_class": "local_push",
"loggers": ["reolink_aio"],
"quality_scale": "platinum",
"requirements": ["reolink-aio==0.14.5"]
"requirements": ["reolink-aio==0.14.6"]
}

View File

@@ -148,7 +148,7 @@ HOST_SENSORS = (
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
entity_registry_enabled_default=False,
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(
key="cpu_usage",

View File

@@ -109,7 +109,6 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
| VacuumEntityFeature.STOP
| VacuumEntityFeature.RETURN_HOME
| VacuumEntityFeature.FAN_SPEED
| VacuumEntityFeature.BATTERY
| VacuumEntityFeature.SEND_COMMAND
| VacuumEntityFeature.LOCATE
| VacuumEntityFeature.CLEAN_SPOT
@@ -142,11 +141,6 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
assert self._device_status.state is not None
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
def fan_speed(self) -> str | None:
"""Return the fan speed of the vacuum cleaner."""

View File

@@ -48,4 +48,3 @@ SERVICE_ARCHIVE_PACKAGE = "archive_package"
ATTR_PACKAGE_STATE = "package_state"
ATTR_PACKAGE_TRACKING_NUMBER = "package_tracking_number"
ATTR_PACKAGE_FRIENDLY_NAME = "package_friendly_name"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"

View File

@@ -6,7 +6,7 @@ from pyseventeentrack.package import PACKAGE_STATUS_MAP, Package
import voluptuous as vol
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 (
HomeAssistant,
ServiceCall,
@@ -20,7 +20,6 @@ from homeassistant.util import slugify
from . import SeventeenTrackCoordinator
from .const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_DESTINATION_COUNTRY,
ATTR_INFO_TEXT,
ATTR_ORIGIN_COUNTRY,

View File

@@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/sonos",
"iot_class": "local_push",
"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": [
{
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"

View File

@@ -6,5 +6,4 @@ from typing import Final
DOMAIN: Final = "stookwijzer"
LOGGER = logging.getLogger(__package__)
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
SERVICE_GET_FORECAST = "get_forecast"

View File

@@ -5,6 +5,7 @@ from typing import Required, TypedDict, cast
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import (
HomeAssistant,
ServiceCall,
@@ -13,7 +14,7 @@ from homeassistant.core import (
)
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
SERVICE_GET_FORECAST_SCHEMA = vol.Schema(

View File

@@ -131,13 +131,13 @@ class SunCondition(Condition):
self._hass = hass
@classmethod
async def async_validate_condition_config(
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
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."""
before = self._config.get("before")
after = self._config.get("after")
@@ -153,7 +153,7 @@ class SunCondition(Condition):
CONDITIONS: dict[str, type[Condition]] = {
"sun": SunCondition,
"_": SunCondition,
}

View File

@@ -29,7 +29,6 @@ PLACEHOLDERS = {
"opendata_url": "http://transport.opendata.ch",
}
ATTR_CONFIG_ENTRY_ID: Final = "config_entry_id"
ATTR_LIMIT: Final = "limit"
SERVICE_FETCH_CONNECTIONS = "fetch_connections"

View File

@@ -3,6 +3,7 @@
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
from homeassistant.core import (
HomeAssistant,
ServiceCall,
@@ -19,7 +20,6 @@ from homeassistant.helpers.selector import (
from homeassistant.helpers.update_coordinator import UpdateFailed
from .const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_LIMIT,
CONNECTIONS_COUNT,
CONNECTIONS_MAX,

View File

@@ -41,5 +41,5 @@
"iot_class": "local_push",
"loggers": ["switchbot"],
"quality_scale": "gold",
"requirements": ["PySwitchbot==0.68.2"]
"requirements": ["PySwitchbot==0.68.3"]
}

View File

@@ -759,7 +759,7 @@
"data_description": {
"device_id": "[%key:component::template::common::device_id_description%]",
"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": {
"advanced_options": {

View File

@@ -1,7 +1,7 @@
{
"domain": "traccar_server",
"name": "Traccar Server",
"codeowners": ["@ludeeus"],
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/traccar_server",
"iot_class": "local_push",

View File

@@ -153,11 +153,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: TuyaConfigEntry) -> bool
# Register known device IDs
device_registry = dr.async_get(hass)
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(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, device.id)},
manufacturer="Tuya",
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_id=device.product_id,
)
@@ -237,6 +247,14 @@ class DeviceListener(SharingDeviceListener):
# Ensure the device isn't present stale
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])
def remove_device(self, device_id: str) -> None:

View File

@@ -22,6 +22,7 @@ from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
from .models import EnumTypeData
from .util import get_dpcode
@dataclass(frozen=True)
@@ -140,7 +141,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):
self._master_state = enum_type
# 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
@property

View File

@@ -27,6 +27,7 @@ from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
from .models import IntegerTypeData
from .util import get_dpcode
TUYA_HVAC_TO_HA = {
"auto": HVACMode.HEAT_COOL,
@@ -229,7 +230,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
self._attr_hvac_modes.append(description.switch_only_hvac_mode)
self._attr_preset_modes = unknown_hvac_modes
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 = [
HVACMode.OFF,
description.switch_only_hvac_mode,
@@ -261,24 +262,24 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
self._fan_mode_dp_code = enum_type.dpcode
# Determine swing modes
if self.find_dpcode(
if get_dpcode(
self.device,
(
DPCode.SHAKE,
DPCode.SWING,
DPCode.SWITCH_HORIZONTAL,
DPCode.SWITCH_VERTICAL,
),
prefer_function=True,
):
self._attr_supported_features |= ClimateEntityFeature.SWING_MODE
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)
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)
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)
if DPCode.SWITCH in self.device.function:

View File

@@ -222,6 +222,7 @@ class DPCode(StrEnum):
HUMIDITY_OUTDOOR_3 = "humidity_outdoor_3" # Outdoor humidity
HUMIDITY_SET = "humidity_set" # Humidity setting
HUMIDITY_VALUE = "humidity_value" # Humidity
INSTALLATION_HEIGHT = "installation_height"
IPC_WORK_MODE = "ipc_work_mode"
LED_TYPE_1 = "led_type_1"
LED_TYPE_2 = "led_type_2"
@@ -232,12 +233,18 @@ class DPCode(StrEnum):
LEVEL_CURRENT = "level_current"
LIGHT = "light" # Light
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
MASTER_MODE = "master_mode" # alarm mode
MASTER_STATE = "master_state" # alarm state
MACH_OPERATE = "mach_operate"
MANUAL_FEED = "manual_feed"
MATERIAL = "material" # Material
MAX_SET = "max_set"
MINI_SET = "mini_set"
MODE = "mode" # Working mode / Mode
MOODLIGHTING = "moodlighting" # Mood light
MOTION_RECORD = "motion_record"

View File

@@ -23,6 +23,7 @@ from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
from .models import IntegerTypeData
from .util import get_dpcode
@dataclass(frozen=True)
@@ -202,7 +203,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
self._attr_supported_features = CoverEntityFeature(0)
# 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":
self._attr_supported_features |= (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE

View File

@@ -72,22 +72,17 @@ class TuyaEntity(Entity):
dptype: Literal[DPType.INTEGER],
) -> IntegerTypeData | None: ...
@overload
def find_dpcode(
self,
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
*,
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:
return None
@@ -100,11 +95,6 @@ class TuyaEntity(Entity):
if prefer_function:
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 key in order:
if dpcode not in getattr(self.device, key):
@@ -133,9 +123,6 @@ class TuyaEntity(Entity):
continue
return integer_type
if dptype not in (DPType.ENUM, DPType.INTEGER):
return dpcode
return None
def get_dptype(

View File

@@ -24,6 +24,7 @@ from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
from .models import EnumTypeData, IntegerTypeData
from .util import get_dpcode
TUYA_SUPPORT_TYPE = {
# Dehumidifier
@@ -90,8 +91,8 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
"""Init Tuya Fan Device."""
super().__init__(device, device_manager)
self._switch = self.find_dpcode(
(DPCode.SWITCH_FAN, DPCode.FAN_SWITCH, DPCode.SWITCH), prefer_function=True
self._switch = get_dpcode(
self.device, (DPCode.SWITCH_FAN, DPCode.FAN_SWITCH, DPCode.SWITCH)
)
self._attr_preset_modes = []
@@ -120,8 +121,8 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
self._attr_supported_features |= FanEntityFeature.SET_SPEED
self._speeds = enum_type
if dpcode := self.find_dpcode(
(DPCode.SWITCH_HORIZONTAL, DPCode.SWITCH_VERTICAL), prefer_function=True
if dpcode := get_dpcode(
self.device, (DPCode.SWITCH_HORIZONTAL, DPCode.SWITCH_VERTICAL)
):
self._oscillate = dpcode
self._attr_supported_features |= FanEntityFeature.OSCILLATE

View File

@@ -21,7 +21,7 @@ from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
from .models import IntegerTypeData
from .util import ActionDPCodeNotFoundError
from .util import ActionDPCodeNotFoundError, get_dpcode
@dataclass(frozen=True)
@@ -105,8 +105,8 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
self._attr_unique_id = f"{super().unique_id}{description.key}"
# Determine main switch DPCode
self._switch_dpcode = self.find_dpcode(
description.dpcode or DPCode(description.key), prefer_function=True
self._switch_dpcode = get_dpcode(
self.device, description.dpcode or DPCode(description.key)
)
# Determine humidity parameters

View File

@@ -29,7 +29,7 @@ from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType, WorkMode
from .entity import TuyaEntity
from .models import IntegerTypeData
from .util import remap_value
from .util import get_dpcode, remap_value
@dataclass
@@ -515,9 +515,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
color_modes: set[ColorMode] = {ColorMode.ONOFF}
# Determine DPCodes
self._color_mode_dpcode = self.find_dpcode(
description.color_mode, prefer_function=True
)
self._color_mode_dpcode = get_dpcode(self.device, description.color_mode)
if int_type := self.find_dpcode(
description.brightness, dptype=DPType.INTEGER, prefer_function=True
@@ -532,7 +530,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
)
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:
self._color_data_dpcode = dpcode
color_modes.add(ColorMode.HS)

View File

@@ -339,6 +339,32 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
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
# https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno
"zd": (

View File

@@ -985,6 +985,13 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
translation_key="rolling_brush_life",
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
"sfkzq": (
@@ -1294,6 +1301,25 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
),
*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
# https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno
"zd": BATTERY_SENSORS,

View File

@@ -213,6 +213,18 @@
},
"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": {
@@ -711,6 +723,20 @@
"charging": "[%key:common::state::charging%]",
"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": {

View File

@@ -758,6 +758,18 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = {
translation_placeholders={"index": "4"},
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(
key=DPCode.CHILD_LOCK,
translation_key="child_lock",

View File

@@ -9,6 +9,29 @@ from homeassistant.exceptions import ServiceValidationError
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(
value: float,
from_min: float = 0,

View File

@@ -18,7 +18,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import TuyaConfigEntry
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
from .entity import TuyaEntity
from .models import EnumTypeData, IntegerTypeData
from .models import EnumTypeData
from .util import get_dpcode
TUYA_MODE_RETURN_HOME = "chargego"
TUYA_STATUS_TO_HA = {
@@ -77,7 +78,6 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
"""Tuya Vacuum Device."""
_fan_speed: EnumTypeData | None = None
_battery_level: IntegerTypeData | None = None
_attr_name = None
def __init__(self, device: CustomerDevice, device_manager: Manager) -> None:
@@ -89,11 +89,11 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
self._attr_supported_features = (
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._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._return_home_use_switch_charge = True
elif (
@@ -103,10 +103,10 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
) and TUYA_MODE_RETURN_HOME in enum_type.range:
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
if self.find_dpcode(DPCode.POWER_GO, prefer_function=True):
if get_dpcode(self.device, DPCode.POWER_GO):
self._attr_supported_features |= (
VacuumEntityFeature.STOP | VacuumEntityFeature.START
)
@@ -118,19 +118,6 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
self._attr_fan_speed_list = enum_type.range
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
def fan_speed(self) -> str | None:
"""Return the fan speed of the vacuum cleaner."""

View File

@@ -8,6 +8,7 @@ from aiowebostv import WebOsClient, WebOsTvPairError
from homeassistant.components import notify as hass_notify
from homeassistant.const import (
ATTR_CONFIG_ENTRY_ID,
CONF_CLIENT_SECRET,
CONF_HOST,
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.typing import ConfigType
from .const import (
ATTR_CONFIG_ENTRY_ID,
DATA_HASS_CONFIG,
DOMAIN,
PLATFORMS,
WEBOSTV_EXCEPTIONS,
)
from .const import DATA_HASS_CONFIG, DOMAIN, PLATFORMS, WEBOSTV_EXCEPTIONS
from .helpers import WebOsTvConfigEntry, update_client_key
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)

View File

@@ -13,7 +13,6 @@ DATA_HASS_CONFIG = "hass_config"
DEFAULT_NAME = "LG webOS TV"
ATTR_BUTTON = "button"
ATTR_CONFIG_ENTRY_ID = "entry_id"
ATTR_PAYLOAD = "payload"
ATTR_SOUND_OUTPUT = "sound_output"

View File

@@ -7,13 +7,13 @@ from typing import Any
from aiowebostv import WebOsClient
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.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import WebOsTvConfigEntry
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, WEBOSTV_EXCEPTIONS
from .const import DOMAIN, WEBOSTV_EXCEPTIONS
PARALLEL_UPDATES = 0

View File

@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["holidays"],
"quality_scale": "internal",
"requirements": ["holidays==0.77"]
"requirements": ["holidays==0.78"]
}

View File

@@ -13,5 +13,5 @@
"documentation": "https://www.home-assistant.io/integrations/yale",
"iot_class": "cloud_push",
"loggers": ["socketio", "engineio", "yalexs"],
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.1.2"]
"requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"]
}

View File

@@ -21,7 +21,7 @@
"zha",
"universal_silabs_flasher"
],
"requirements": ["zha==0.0.65"],
"requirements": ["zha==0.0.67"],
"usb": [
{
"vid": "10C4",

View File

@@ -16,6 +16,7 @@ rules:
status: done
comment: |
https://mark_hannon@bitbucket.org/mark_hannon/zcc.git
https://bitbucket.org/mark_hannon/zcc/src/master/bitbucket-pipelines.yml
docs-actions:
status: exempt
comment: |

View File

@@ -100,13 +100,13 @@ class ZoneCondition(Condition):
self._config = config
@classmethod
async def async_validate_condition_config(
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
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."""
entity_ids = self._config.get(CONF_ENTITY_ID, [])
zone_entity_ids = self._config.get(CONF_ZONE, [])
@@ -147,7 +147,7 @@ class ZoneCondition(Condition):
CONDITIONS: dict[str, type[Condition]] = {
"zone": ZoneCondition,
"_": ZoneCondition,
}

View File

@@ -92,7 +92,6 @@ ATTR_CURRENT_VALUE = "current_value"
ATTR_CURRENT_VALUE_RAW = "current_value_raw"
ATTR_DESCRIPTION = "description"
ATTR_EVENT_SOURCE = "event_source"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_PARTIAL_DICT_MATCH = "partial_dict_match"
# service constants

View File

@@ -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.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.helpers import config_validation as cv, device_registry as dr
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 ..const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_EVENT,
ATTR_EVENT_DATA,
ATTR_EVENT_SOURCE,
@@ -263,13 +267,13 @@ class EventTrigger(Trigger):
self._hass = hass
@classmethod
async def async_validate_trigger_config(
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return await async_validate_trigger_config(hass, config)
async def async_attach_trigger(
async def async_attach(
self,
action: TriggerActionType,
trigger_info: TriggerInfo,

View File

@@ -1,12 +1,12 @@
"""Helpers for Z-Wave JS custom triggers."""
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.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.typing import ConfigType
from ..const import ATTR_CONFIG_ENTRY_ID, DOMAIN
from ..const import DOMAIN
@callback

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