Compare commits

..

1 Commits

Author SHA1 Message Date
abmantis
d6caa86ab3 Add support for trigger descriptions with relative keys 2025-08-03 16:25:36 +01:00
743 changed files with 9975 additions and 72441 deletions

View File

@@ -175,7 +175,7 @@ jobs:
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations
uses: actions/download-artifact@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
name: translations
@@ -190,7 +190,7 @@ jobs:
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.5.0
uses: docker/login-action@v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -256,7 +256,7 @@ jobs:
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@v3.5.0
uses: docker/login-action@v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -330,14 +330,14 @@ jobs:
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@v3.5.0
uses: docker/login-action@v3.4.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
if: matrix.registry == 'ghcr.io/home-assistant'
uses: docker/login-action@v3.5.0
uses: docker/login-action@v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -462,7 +462,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
name: translations
@@ -502,7 +502,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Login to GitHub Container Registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}

View File

@@ -255,7 +255,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.4
uses: actions/cache@v4.2.3
with:
path: venv
key: >-
@@ -271,7 +271,7 @@ jobs:
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache@v4.2.4
uses: actions/cache@v4.2.3
with:
path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true
@@ -301,7 +301,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -310,7 +310,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -341,7 +341,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -350,7 +350,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -381,7 +381,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -390,7 +390,7 @@ jobs:
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
@@ -497,7 +497,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v4.2.4
uses: actions/cache@v4.2.3
with:
path: venv
key: >-
@@ -505,7 +505,7 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@v4.2.4
uses: actions/cache@v4.2.3
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -593,7 +593,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -626,7 +626,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -683,7 +683,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -726,7 +726,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -773,7 +773,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -825,7 +825,7 @@ jobs:
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -833,7 +833,7 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@v4.2.4
uses: actions/cache@v4.2.3
with:
path: .mypy_cache
key: >-
@@ -895,7 +895,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -956,7 +956,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -970,7 +970,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: actions/download-artifact@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
name: pytest_buckets
- name: Compile English translations
@@ -1089,7 +1089,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -1231,7 +1231,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -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@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1390,7 +1390,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@v4.2.4
uses: actions/cache/restore@v4.2.3
with:
path: venv
fail-on-cache-miss: true
@@ -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@v5.0.0
uses: actions/download-artifact@v4.3.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@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
pattern: test-results-*
- name: Upload test results to Codecov

View File

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.29.7
uses: github/codeql-action/init@v3.29.5
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.29.7
uses: github/codeql-action/analyze@v3.29.5
with:
category: "/language:python"

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.8
uses: actions/ai-inference@v1.2.3
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.8
uses: actions/ai-inference@v1.2.3
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@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
name: requirements_diff
@@ -187,22 +187,22 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Download env_file
uses: actions/download-artifact@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
name: requirements_diff
- name: Download requirements_all_wheels
uses: actions/download-artifact@v5.0.0
uses: actions/download-artifact@v4.3.0
with:
name: requirements_all_wheels

View File

@@ -310,6 +310,7 @@ homeassistant.components.letpot.*
homeassistant.components.lidarr.*
homeassistant.components.lifx.*
homeassistant.components.light.*
homeassistant.components.linear_garage_door.*
homeassistant.components.linkplay.*
homeassistant.components.litejet.*
homeassistant.components.litterrobot.*

6
CODEOWNERS generated
View File

@@ -862,6 +862,8 @@ build.json @home-assistant/supervisor
/tests/components/lifx/ @Djelibeybi
/homeassistant/components/light/ @home-assistant/core
/tests/components/light/ @home-assistant/core
/homeassistant/components/linear_garage_door/ @IceBotYT
/tests/components/linear_garage_door/ @IceBotYT
/homeassistant/components/linkplay/ @Velleman
/tests/components/linkplay/ @Velleman
/homeassistant/components/linux_battery/ @fabaff
@@ -1597,8 +1599,6 @@ build.json @home-assistant/supervisor
/tests/components/todo/ @home-assistant/core
/homeassistant/components/todoist/ @boralyl
/tests/components/todoist/ @boralyl
/homeassistant/components/togrill/ @elupus
/tests/components/togrill/ @elupus
/homeassistant/components/tolo/ @MatthiasLohr
/tests/components/tolo/ @MatthiasLohr
/homeassistant/components/tomorrowio/ @raman325 @lymanepp
@@ -1613,6 +1613,8 @@ 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

@@ -120,9 +120,6 @@ class AuthStore:
new_user = models.User(**kwargs)
while new_user.id in self._users:
new_user = models.User(**kwargs)
self._users[new_user.id] = new_user
if credentials is None:

View File

@@ -10,10 +10,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
_PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.SENSOR,
]
_PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:

View File

@@ -1,106 +0,0 @@
"""AirOS Binary Sensor component for Home Assistant."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator
from .entity import AirOSEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AirOSBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describe an AirOS binary sensor."""
value_fn: Callable[[AirOSData], bool]
BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = (
AirOSBinarySensorEntityDescription(
key="portfw",
translation_key="port_forwarding",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.portfw,
),
AirOSBinarySensorEntityDescription(
key="dhcp_client",
translation_key="dhcp_client",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.services.dhcpc,
),
AirOSBinarySensorEntityDescription(
key="dhcp_server",
translation_key="dhcp_server",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.services.dhcpd,
entity_registry_enabled_default=False,
),
AirOSBinarySensorEntityDescription(
key="dhcp6_server",
translation_key="dhcp6_server",
device_class=BinarySensorDeviceClass.RUNNING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.services.dhcp6d_stateful,
entity_registry_enabled_default=False,
),
AirOSBinarySensorEntityDescription(
key="pppoe",
translation_key="pppoe",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.services.pppoe,
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AirOSConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the AirOS binary sensors from a config entry."""
coordinator = config_entry.runtime_data
async_add_entities(
AirOSBinarySensor(coordinator, description) for description in BINARY_SENSORS
)
class AirOSBinarySensor(AirOSEntity, BinarySensorEntity):
"""Representation of a binary sensor."""
entity_description: AirOSBinarySensorEntityDescription
def __init__(
self,
coordinator: AirOSDataUpdateCoordinator,
description: AirOSBinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}"
@property
def is_on(self) -> bool:
"""Return the state of the binary sensor."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -6,11 +6,11 @@ import logging
from typing import Any
from airos.exceptions import (
AirOSConnectionAuthenticationError,
AirOSConnectionSetupError,
AirOSDataMissingError,
AirOSDeviceConnectionError,
AirOSKeyDataMissingError,
ConnectionAuthenticationError,
ConnectionSetupError,
DataMissingError,
DeviceConnectionError,
KeyDataMissingError,
)
import voluptuous as vol
@@ -59,13 +59,13 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
airos_data = await airos_device.status()
except (
AirOSConnectionSetupError,
AirOSDeviceConnectionError,
ConnectionSetupError,
DeviceConnectionError,
):
errors["base"] = "cannot_connect"
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
except (ConnectionAuthenticationError, DataMissingError):
errors["base"] = "invalid_auth"
except AirOSKeyDataMissingError:
except KeyDataMissingError:
errors["base"] = "key_data_missing"
except Exception:
_LOGGER.exception("Unexpected exception")

View File

@@ -6,10 +6,10 @@ import logging
from airos.airos8 import AirOS, AirOSData
from airos.exceptions import (
AirOSConnectionAuthenticationError,
AirOSConnectionSetupError,
AirOSDataMissingError,
AirOSDeviceConnectionError,
ConnectionAuthenticationError,
ConnectionSetupError,
DataMissingError,
DeviceConnectionError,
)
from homeassistant.config_entries import ConfigEntry
@@ -47,22 +47,18 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
try:
await self.airos_device.login()
return await self.airos_device.status()
except (AirOSConnectionAuthenticationError,) as err:
except (ConnectionAuthenticationError,) as err:
_LOGGER.exception("Error authenticating with airOS device")
raise ConfigEntryError(
translation_domain=DOMAIN, translation_key="invalid_auth"
) from err
except (
AirOSConnectionSetupError,
AirOSDeviceConnectionError,
TimeoutError,
) as err:
except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err:
_LOGGER.error("Error connecting to airOS device: %s", err)
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from err
except (AirOSDataMissingError,) as err:
except (DataMissingError,) as err:
_LOGGER.error("Expected data not returned by airOS device: %s", err)
raise UpdateFailed(
translation_domain=DOMAIN,

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airos",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["airos==0.2.7"]
"requirements": ["airos==0.2.1"]
}

View File

@@ -54,7 +54,9 @@ rules:
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-disabled-by-default:
status: todo
comment: prepared binary_sensors will provide this
entity-translations: done
exception-translations: done
icon-translations:

View File

@@ -46,7 +46,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
translation_key="host_cpuload",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
value_fn=lambda data: data.host.cpuload,
entity_registry_enabled_default=False,
),
@@ -70,6 +69,13 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
translation_key="wireless_essid",
value_fn=lambda data: data.wireless.essid,
),
AirOSSensorEntityDescription(
key="wireless_mode",
translation_key="wireless_mode",
device_class=SensorDeviceClass.ENUM,
value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(),
options=WIRELESS_MODE_OPTIONS,
),
AirOSSensorEntityDescription(
key="wireless_antenna_gain",
translation_key="wireless_antenna_gain",
@@ -84,8 +90,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.throughput.tx,
),
AirOSSensorEntityDescription(
@@ -94,8 +98,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.throughput.rx,
),
AirOSSensorEntityDescription(
@@ -104,8 +106,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.polling.dl_capacity,
),
AirOSSensorEntityDescription(
@@ -114,8 +114,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=0,
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
value_fn=lambda data: data.wireless.polling.ul_capacity,
),
)

View File

@@ -26,23 +26,6 @@
}
},
"entity": {
"binary_sensor": {
"port_forwarding": {
"name": "Port forwarding"
},
"dhcp_client": {
"name": "DHCP client"
},
"dhcp_server": {
"name": "DHCP server"
},
"dhcp6_server": {
"name": "DHCPv6 server"
},
"pppoe": {
"name": "PPPoE link"
}
},
"sensor": {
"host_cpuload": {
"name": "CPU load"
@@ -60,6 +43,13 @@
"wireless_essid": {
"name": "Wireless SSID"
},
"wireless_mode": {
"name": "Wireless mode",
"state": {
"ap_ptp": "Access point",
"sta_ptp": "Station"
}
},
"wireless_antenna_gain": {
"name": "Antenna gain"
},

View File

@@ -7,18 +7,21 @@ import logging
from airthings import Airthings
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_SECRET
from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator
from .coordinator import AirthingsDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
"""Set up Airthings from a config entry."""
@@ -28,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
async_get_clientsession(hass),
)
coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry)
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
await coordinator.async_config_entry_first_refresh()

View File

@@ -5,7 +5,6 @@ import logging
from airthings import Airthings, AirthingsDevice, AirthingsError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -14,23 +13,15 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(minutes=6)
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
"""Coordinator for Airthings data updates."""
def __init__(
self,
hass: HomeAssistant,
airthings: Airthings,
config_entry: AirthingsConfigEntry,
) -> None:
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_method=self._update_method,
update_interval=SCAN_INTERVAL,

View File

@@ -9,6 +9,7 @@ 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,7 +4,6 @@ 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,
@@ -17,6 +16,7 @@ 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,15 +81,11 @@ async def async_update_options(
async def async_migrate_integration(hass: HomeAssistant) -> None:
"""Migrate integration entry structure."""
# 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,
)
entries = hass.config_entries.async_entries(DOMAIN)
if not any(entry.version == 1 for entry in entries):
return
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
api_keys_entries: dict[str, ConfigEntry] = {}
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
@@ -103,61 +99,30 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
)
if entry.data[CONF_API_KEY] not in api_keys_entries:
use_existing = True
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)
api_keys_entries[entry.data[CONF_API_KEY]] = entry
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
hass.config_entries.async_add_subentry(parent_entry, subentry)
conversation_entity_id = entity_registry.async_get_entity_id(
conversation_entity = entity_registry.async_get_entity_id(
"conversation",
DOMAIN,
entry.entry_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
)
if conversation_entity is not None:
entity_registry.async_update_entity(
conversation_entity_id,
conversation_entity,
config_entry_id=parent_entry.entry_id,
config_subentry_id=subentry.subentry_id,
disabled_by=entity_disabled_by,
new_unique_id=subentry.subentry_id,
)
device = device_registry.async_get_device(
identifiers={(DOMAIN, entry.entry_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,
@@ -182,7 +147,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
title=DEFAULT_CONVERSATION_NAME,
options={},
version=2,
minor_version=3,
minor_version=2,
)
@@ -208,38 +173,6 @@ 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 = 3
MINOR_VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None

View File

@@ -20,8 +20,10 @@ RECOMMENDED_THINKING_BUDGET = 0
MIN_THINKING_BUDGET = 1024
THINKING_MODELS = [
"claude-3-7-sonnet",
"claude-sonnet-4-0",
"claude-3-7-sonnet-20250219",
"claude-3-7-sonnet-latest",
"claude-opus-4-20250514",
"claude-opus-4-0",
"claude-opus-4-1",
"claude-sonnet-4-20250514",
"claude-sonnet-4-0",
]

View File

@@ -361,10 +361,7 @@ class AnthropicBaseLLMEntity(Entity):
"system": system.content,
"stream": True,
}
if (
model.startswith(tuple(THINKING_MODELS))
and thinking_budget >= MIN_THINKING_BUDGET
):
if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET:
model_args["thinking"] = ThinkingConfigEnabledParam(
type="enabled", budget_tokens=thinking_budget
)

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["anthropic==0.62.0"]
"requirements": ["anthropic==0.52.0"]
}

View File

@@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
"iot_class": "local_polling",
"loggers": ["apcaccess"],
"quality_scale": "bronze",
"requirements": ["aioapcaccess==0.4.2"]
}

View File

@@ -1,96 +0,0 @@
rules:
# Bronze
action-setup: done
appropriate-polling: done
brands: done
common-modules:
status: done
comment: |
Consider deriving a base entity.
config-flow-test-coverage:
status: done
comment: |
Consider looking into making a `mock_setup_entry` fixture that just automatically do this.
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
The integration does not provide any actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
The integration does not provide any actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: |
The integration does not provide any additional options.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow:
status: exempt
comment: |
The integration does not require authentication.
test-coverage:
status: todo
comment: |
Patch `aioapcaccess.request_status` where we use it.
# Gold
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: |
This integration cannot be discovered.
discovery:
status: exempt
comment: |
This integration cannot be discovered.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
status: exempt
comment: |
The integration connects to a single service per configuration entry.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues: done
stale-devices:
status: exempt
comment: |
This integration connect to a single service per configuration entry.
# Platinum
async-dependency: done
inject-websession:
status: exempt
comment: |
The integration does not connect via HTTP.
strict-typing: done

View File

@@ -14,22 +14,7 @@
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of the APC UPS Daemon",
"port": "The port the APC UPS Daemon is listening on"
},
"description": "Enter the host and port on which the apcupsd NIS is being served."
},
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "[%key:component::apcupsd::config::step::user::data_description::host%]",
"port": "[%key:component::apcupsd::config::step::user::data_description::port%]"
},
"description": "[%key:component::apcupsd::config::step::user::description%]"
}
}
},

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable, Mapping
from datetime import datetime, timedelta
import logging
from typing import TYPE_CHECKING, Any
from typing import Any
from pyasuswrt import AsusWrtError
@@ -40,9 +40,6 @@ from .const import (
SENSORS_CONNECTED_DEVICE,
)
if TYPE_CHECKING:
from . import AsusWrtConfigEntry
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
SCAN_INTERVAL = timedelta(seconds=30)
@@ -55,13 +52,10 @@ _LOGGER = logging.getLogger(__name__)
class AsusWrtSensorDataHandler:
"""Data handler for AsusWrt sensor."""
def __init__(
self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry
) -> None:
def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None:
"""Initialize a AsusWrt sensor data handler."""
self._hass = hass
self._api = api
self._entry = entry
self._connected_devices = 0
async def _get_connected_devices(self) -> dict[str, int]:
@@ -97,7 +91,6 @@ class AsusWrtSensorDataHandler:
update_method=method,
# Polling interval. Will only be polled if there are subscribers.
update_interval=SCAN_INTERVAL if should_poll else None,
config_entry=self._entry,
)
await coordinator.async_refresh()
@@ -328,9 +321,7 @@ class AsusWrtRouter:
if self._sensors_data_handler:
return
self._sensors_data_handler = AsusWrtSensorDataHandler(
self.hass, self._api, self._entry
)
self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
self._sensors_data_handler.update_device_count(self._connected_devices)
sensors_types = await self._api.async_get_available_sensors()

View File

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

View File

@@ -5,7 +5,6 @@ from __future__ import annotations
from datetime import timedelta
import logging
API_ABS_HUMID = "abs_humid"
API_CO2 = "carbon_dioxide"
API_DEW_POINT = "dew_point"
API_DUST = "dust"

View File

@@ -18,7 +18,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_CONNECTIONS,
ATTR_SW_VERSION,
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
@@ -34,7 +33,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
API_ABS_HUMID,
API_CO2,
API_DEW_POINT,
API_DUST,
@@ -122,14 +120,6 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
AwairSensorEntityDescription(
key=API_ABS_HUMID,
device_class=SensorDeviceClass.ABSOLUTE_HUMIDITY,
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
unique_id_tag="absolute_humidity",
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
),
)
SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = (

View File

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

View File

@@ -127,6 +127,7 @@ class BackupConfigData:
schedule=BackupSchedule(
days=days,
recurrence=ScheduleRecurrence(data["schedule"]["recurrence"]),
state=ScheduleState(data["schedule"].get("state", ScheduleState.NEVER)),
time=time,
),
)
@@ -452,6 +453,7 @@ class StoredBackupSchedule(TypedDict):
days: list[Day]
recurrence: ScheduleRecurrence
state: ScheduleState
time: str | None
@@ -460,6 +462,7 @@ class ScheduleParametersDict(TypedDict, total=False):
days: list[Day]
recurrence: ScheduleRecurrence
state: ScheduleState
time: dt.time | None
@@ -483,12 +486,32 @@ class ScheduleRecurrence(StrEnum):
CUSTOM_DAYS = "custom_days"
class ScheduleState(StrEnum):
"""Represent the schedule recurrence.
This is deprecated and can be remove in HA Core 2025.8.
"""
NEVER = "never"
DAILY = "daily"
MONDAY = "mon"
TUESDAY = "tue"
WEDNESDAY = "wed"
THURSDAY = "thu"
FRIDAY = "fri"
SATURDAY = "sat"
SUNDAY = "sun"
@dataclass(kw_only=True)
class BackupSchedule:
"""Represent the backup schedule."""
days: list[Day] = field(default_factory=list)
recurrence: ScheduleRecurrence = ScheduleRecurrence.NEVER
# Although no longer used, state is kept for backwards compatibility.
# It can be removed in HA Core 2025.8.
state: ScheduleState = ScheduleState.NEVER
time: dt.time | None = None
cron_event: CronSim | None = field(init=False, default=None)
next_automatic_backup: datetime | None = field(init=False, default=None)
@@ -587,6 +610,7 @@ class BackupSchedule:
return StoredBackupSchedule(
days=self.days,
recurrence=self.recurrence,
state=self.state,
time=self.time.isoformat() if self.time else None,
)

View File

@@ -331,6 +331,9 @@ async def handle_config_info(
"""Send the stored backup config."""
manager = hass.data[DATA_MANAGER]
config = manager.config.data.to_dict()
# Remove state from schedule, it's not needed in the frontend
# mypy doesn't like deleting from TypedDict, ignore it
del config["schedule"]["state"] # type: ignore[misc]
connection.send_result(
msg["id"],
{

View File

@@ -25,6 +25,7 @@ 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 ATTR_CONFIG_ENTRY_ID, CONF_PIN
from homeassistant.const import 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 DOMAIN, SERVICE_SEND_PIN
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_SEND_PIN
from .coordinator import BlinkConfigEntry
SERVICE_SEND_PIN_SCHEMA = vol.Schema(

View File

@@ -388,6 +388,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE
scanner = HaScanner(mode, adapter, address)
scanner.async_setup()
try:
await scanner.async_start()
except (RuntimeError, ScannerStartError) as err:
raise ConfigEntryNotReady(
f"{adapter_human_name(adapter, address)}: {err}"
) from err
adapters = await manager.async_get_bluetooth_adapters()
details = adapters[adapter]
if entry.title == address:
@@ -395,16 +401,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry, title=adapter_title(adapter, details)
)
slots: int = details.get(ADAPTER_CONNECTION_SLOTS) or DEFAULT_CONNECTION_SLOTS
# Register the scanner before starting so
# any raw advertisement data can be processed
entry.async_on_unload(async_register_scanner(hass, scanner, connection_slots=slots))
await async_update_device(hass, entry, adapter, details)
try:
await scanner.async_start()
except (RuntimeError, ScannerStartError) as err:
raise ConfigEntryNotReady(
f"{adapter_human_name(adapter, address)}: {err}"
) from err
entry.async_on_unload(entry.add_update_listener(async_update_listener))
entry.async_on_unload(scanner.async_stop)
return True

View File

@@ -235,9 +235,10 @@ class HomeAssistantBluetoothManager(BluetoothManager):
def _async_save_scanner_history(self, scanner: BaseHaScanner) -> None:
"""Save the scanner history."""
self.storage.async_set_advertisement_history(
scanner.source, scanner.serialize_discovered_devices()
)
if isinstance(scanner, BaseHaRemoteScanner):
self.storage.async_set_advertisement_history(
scanner.source, scanner.serialize_discovered_devices()
)
def _async_unregister_scanner(
self, scanner: BaseHaScanner, unregister: CALLBACK_TYPE
@@ -284,8 +285,9 @@ class HomeAssistantBluetoothManager(BluetoothManager):
connection_slots: int | None = None,
) -> CALLBACK_TYPE:
"""Register a scanner."""
if history := self.storage.async_get_advertisement_history(scanner.source):
scanner.restore_discovered_devices(history)
if isinstance(scanner, BaseHaRemoteScanner):
if history := self.storage.async_get_advertisement_history(scanner.source):
scanner.restore_discovered_devices(history)
unregister = super().async_register_scanner(scanner, connection_slots)
return partial(self._async_unregister_scanner, scanner, unregister)

View File

@@ -20,7 +20,7 @@
"bluetooth-adapters==2.0.0",
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.44.3",
"habluetooth==5.0.1"
"dbus-fast==2.44.2",
"habluetooth==4.0.1"
]
}

View File

@@ -39,13 +39,7 @@ def async_setup(hass: HomeAssistant) -> None:
def serialize_service_info(
service_info: BluetoothServiceInfoBleak, time_diff: float
) -> dict[str, Any]:
"""Serialize a BluetoothServiceInfoBleak object.
The raw field is included for:
1. Debugging - to see the actual advertisement packet
2. Data freshness - manufacturer_data and service_data are aggregated
across multiple advertisements, raw shows the latest packet only
"""
"""Serialize a BluetoothServiceInfoBleak object."""
return {
"name": service_info.name,
"address": service_info.address,
@@ -63,7 +57,6 @@ def serialize_service_info(
"connectable": service_info.connectable,
"time": service_info.time + time_diff,
"tx_power": service_info.tx_power,
"raw": service_info.raw.hex() if service_info.raw else None,
}

View File

@@ -6,3 +6,4 @@ 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,13 +9,12 @@ 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_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
from .const import ATTR_CONFIG_ENTRY_ID, ATTR_DATETIME, DOMAIN, SERVICE_SET_DATE_TIME
from .types import BoschAlarmConfigEntry

View File

@@ -95,7 +95,7 @@
"name": "Battery missing"
},
"panel_fault_ac_fail": {
"name": "AC failure"
"name": "AC Failure"
},
"panel_fault_parameter_crc_fail_in_pif": {
"name": "CRC failure in panel configuration"

View File

@@ -64,7 +64,6 @@ class BroadlinkUpdateManager(ABC, Generic[_ApiT]):
device.hass,
_LOGGER,
name=f"{device.name} ({device.api.model} at {device.api.host[0]})",
config_entry=device.config,
update_method=self.async_update,
update_interval=self.SCAN_INTERVAL,
)

View File

@@ -2,16 +2,7 @@
import dataclasses
from bsblan import (
BSBLAN,
BSBLANAuthError,
BSBLANConfig,
BSBLANConnectionError,
BSBLANError,
Device,
Info,
StaticState,
)
from bsblan import BSBLAN, BSBLANConfig, Device, Info, StaticState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -22,14 +13,9 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryError,
ConfigEntryNotReady,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_PASSKEY, DOMAIN
from .const import CONF_PASSKEY
from .coordinator import BSBLanUpdateCoordinator
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
@@ -68,27 +54,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
coordinator = BSBLanUpdateCoordinator(hass, entry, bsblan)
await coordinator.async_config_entry_first_refresh()
try:
# Fetch all required data sequentially
device = await bsblan.device()
info = await bsblan.info()
static = await bsblan.static_values()
except BSBLANConnectionError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="setup_connection_error",
translation_placeholders={"host": entry.data[CONF_HOST]},
) from err
except BSBLANAuthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="setup_auth_error",
) from err
except BSBLANError as err:
raise ConfigEntryError(
translation_domain=DOMAIN,
translation_key="setup_general_error",
) from err
# Fetch all required data concurrently
device = await bsblan.device()
info = await bsblan.info()
static = await bsblan.static_values()
entry.runtime_data = BSBLanData(
client=bsblan,

View File

@@ -211,16 +211,16 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
),
)
# Combine existing data with the user's new input for validation.
# This correctly handles adding, changing, and clearing credentials.
config_data = existing_entry.data.copy()
config_data.update(user_input)
self.host = config_data[CONF_HOST]
self.port = config_data[CONF_PORT]
self.passkey = config_data.get(CONF_PASSKEY)
self.username = config_data.get(CONF_USERNAME)
self.password = config_data.get(CONF_PASSWORD)
# Use existing host and port, update auth credentials
self.host = existing_entry.data[CONF_HOST]
self.port = existing_entry.data[CONF_PORT]
self.passkey = user_input.get(CONF_PASSKEY) or existing_entry.data.get(
CONF_PASSKEY
)
self.username = user_input.get(CONF_USERNAME) or existing_entry.data.get(
CONF_USERNAME
)
self.password = user_input.get(CONF_PASSWORD)
try:
await self._get_bsblan_info(raise_on_progress=False, is_reauth=True)
@@ -267,9 +267,17 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
errors={"base": "cannot_connect"},
)
# Update only the fields that were provided by the user
# Update the config entry with new auth data
data_updates = {}
if self.passkey is not None:
data_updates[CONF_PASSKEY] = self.passkey
if self.username is not None:
data_updates[CONF_USERNAME] = self.username
if self.password is not None:
data_updates[CONF_PASSWORD] = self.password
return self.async_update_reload_and_abort(
existing_entry, data_updates=user_input, reason="reauth_successful"
existing_entry, data_updates=data_updates, reason="reauth_successful"
)
@callback

View File

@@ -41,11 +41,6 @@
"passkey": "[%key:component::bsblan::config::step::user::data::passkey%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"passkey": "[%key:component::bsblan::config::step::user::data_description::passkey%]",
"username": "[%key:component::bsblan::config::step::user::data_description::username%]",
"password": "[%key:component::bsblan::config::step::user::data_description::password%]"
}
}
},
@@ -71,15 +66,6 @@
},
"set_operation_mode_error": {
"message": "An error occurred while setting the operation mode"
},
"setup_connection_error": {
"message": "Failed to retrieve static device data from BSB-Lan device at {host}"
},
"setup_auth_error": {
"message": "Authentication failed while retrieving static device data"
},
"setup_general_error": {
"message": "An unknown error occurred while retrieving static device data"
}
},
"entity": {

View File

@@ -25,7 +25,7 @@
"services": {
"press": {
"name": "Press",
"description": "Presses a button entity."
"description": "Press the button entity."
}
}
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/caldav",
"iot_class": "cloud_polling",
"loggers": ["caldav", "vobject"],
"requirements": ["caldav==1.6.0", "icalendar==6.3.1"]
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
}

View File

@@ -100,10 +100,16 @@ set_hvac_mode:
fields:
hvac_mode:
selector:
state:
hide_states:
- unavailable
- unknown
select:
options:
- "off"
- "auto"
- "cool"
- "dry"
- "fan_only"
- "heat_cool"
- "heat"
translation_key: hvac_mode
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.2"],
"requirements": ["hass-nabucasa==0.110.0"],
"single_config_entry": true
}

View File

@@ -4,13 +4,11 @@ from __future__ import annotations
import asyncio
import logging
from typing import Any
from hass_nabucasa import (
Cloud,
MigratePaypalAgreementInfo,
PaymentsApiError,
SubscriptionInfo,
)
from aiohttp.client_exceptions import ClientError
from hass_nabucasa import Cloud, cloud_api
from hass_nabucasa.payments_api import PaymentsApiError, SubscriptionInfo
from .client import CloudClient
from .const import REQUEST_TIMEOUT
@@ -31,17 +29,17 @@ async def async_subscription_info(cloud: Cloud[CloudClient]) -> SubscriptionInfo
async def async_migrate_paypal_agreement(
cloud: Cloud[CloudClient],
) -> MigratePaypalAgreementInfo | None:
) -> dict[str, Any] | None:
"""Migrate a paypal agreement from legacy."""
try:
async with asyncio.timeout(REQUEST_TIMEOUT):
return await cloud.payments.migrate_paypal_agreement()
return await cloud_api.async_migrate_paypal_agreement(cloud)
except TimeoutError:
_LOGGER.error(
"A timeout of %s was reached while trying to start agreement migration",
REQUEST_TIMEOUT,
)
except PaymentsApiError as exception:
except ClientError as exception:
_LOGGER.error("Failed to start agreement migration - %s", exception)
return None

View File

@@ -161,9 +161,7 @@ class AssistantContent:
role: Literal["assistant"] = field(init=False, default="assistant")
agent_id: str
content: str | None = None
thinking_content: str | None = None
tool_calls: list[llm.ToolInput] | None = None
native: Any = None
@dataclass(frozen=True)
@@ -185,9 +183,7 @@ class AssistantContentDeltaDict(TypedDict, total=False):
role: Literal["assistant"]
content: str | None
thinking_content: str | None
tool_calls: list[llm.ToolInput] | None
native: Any
@dataclass
@@ -310,8 +306,6 @@ class ChatLog:
The keys content and tool_calls will be concatenated if they appear multiple times.
"""
current_content = ""
current_thinking_content = ""
current_native: Any = None
current_tool_calls: list[llm.ToolInput] = []
tool_call_tasks: dict[str, asyncio.Task] = {}
@@ -322,14 +316,6 @@ class ChatLog:
if "role" not in delta:
if delta_content := delta.get("content"):
current_content += delta_content
if delta_thinking_content := delta.get("thinking_content"):
current_thinking_content += delta_thinking_content
if delta_native := delta.get("native"):
if current_native is not None:
raise RuntimeError(
"Native content already set, cannot overwrite"
)
current_native = delta_native
if delta_tool_calls := delta.get("tool_calls"):
if self.llm_api is None:
raise ValueError("No LLM API configured")
@@ -342,12 +328,7 @@ class ChatLog:
name=f"llm_tool_{tool_call.id}",
)
if self.delta_listener:
if filtered_delta := {
k: v for k, v in delta.items() if k != "native"
}:
# We do not want to send the native content to the listener
# as it is not JSON serializable
self.delta_listener(self, filtered_delta)
self.delta_listener(self, delta) # type: ignore[arg-type]
continue
# Starting a new message
@@ -356,18 +337,11 @@ class ChatLog:
raise ValueError(f"Only assistant role expected. Got {delta['role']}")
# Yield the previous message if it has content
if (
current_content
or current_thinking_content
or current_tool_calls
or current_native
):
if current_content or current_tool_calls:
content = AssistantContent(
agent_id=agent_id,
content=current_content or None,
thinking_content=current_thinking_content or None,
tool_calls=current_tool_calls or None,
native=current_native,
)
yield content
async for tool_result in self.async_add_assistant_content(
@@ -378,25 +352,16 @@ class ChatLog:
self.delta_listener(self, asdict(tool_result))
current_content = delta.get("content") or ""
current_thinking_content = delta.get("thinking_content") or ""
current_tool_calls = delta.get("tool_calls") or []
current_native = delta.get("native")
if self.delta_listener:
self.delta_listener(self, delta) # type: ignore[arg-type]
if (
current_content
or current_thinking_content
or current_tool_calls
or current_native
):
if current_content or current_tool_calls:
content = AssistantContent(
agent_id=agent_id,
content=current_content or None,
thinking_content=current_thinking_content or None,
tool_calls=current_tool_calls or None,
native=current_native,
)
yield content
async for tool_result in self.async_add_assistant_content(

View File

@@ -61,7 +61,7 @@ class DeviceCondition(Condition):
self._hass = hass
@classmethod
async def async_validate_config(
async def async_validate_condition_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_get_checker(self) -> condition.ConditionCheckerType:
async def async_condition_from_config(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

@@ -16,7 +16,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.2.0",
"aiodiscover==2.7.1",
"aiodiscover==2.7.0",
"cached-ipaddress==0.10.0"
]
}

View File

@@ -18,9 +18,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# If path is relative, we assume relative to Home Assistant config dir
if not os.path.isabs(download_path):
download_path = hass.config.path(download_path)
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_DOWNLOAD_DIR: download_path}
)
if not await hass.async_add_executor_job(os.path.isdir, download_path):
_LOGGER.error(

View File

@@ -11,7 +11,6 @@ import requests
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
@@ -35,33 +34,24 @@ def download_file(service: ServiceCall) -> None:
entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
download_path = entry.data[CONF_DOWNLOAD_DIR]
url: str = service.data[ATTR_URL]
subdir: str | None = service.data.get(ATTR_SUBDIR)
target_filename: str | None = service.data.get(ATTR_FILENAME)
overwrite: bool = service.data[ATTR_OVERWRITE]
if subdir:
# Check the path
try:
raise_if_invalid_path(subdir)
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="subdir_invalid",
translation_placeholders={"subdir": subdir},
) from err
if os.path.isabs(subdir):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="subdir_not_relative",
translation_placeholders={"subdir": subdir},
)
def do_download() -> None:
"""Download the file."""
final_path = None
filename = target_filename
try:
url = service.data[ATTR_URL]
subdir = service.data.get(ATTR_SUBDIR)
filename = service.data.get(ATTR_FILENAME)
overwrite = service.data.get(ATTR_OVERWRITE)
if subdir:
# Check the path
raise_if_invalid_path(subdir)
final_path = None
req = requests.get(url, stream=True, timeout=10)
if req.status_code != HTTPStatus.OK:

View File

@@ -12,14 +12,6 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"exceptions": {
"subdir_invalid": {
"message": "Invalid subdirectory, got: {subdir}"
},
"subdir_not_relative": {
"message": "Subdirectory must be relative, got: {subdir}"
}
},
"services": {
"download_file": {
"name": "Download file",

View File

@@ -20,6 +20,7 @@ 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

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==13.6.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==13.5.0"]
}

View File

@@ -2,6 +2,7 @@
import logging
CONF_EXCLUDE_FEEDID = "exclude_feed_id"
CONF_ONLY_INCLUDE_FEEDID = "include_only_feed_id"
CONF_MESSAGE = "message"
CONF_SUCCESS = "success"

View File

@@ -34,7 +34,13 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .config_flow import sensor_name
from .const import CONF_ONLY_INCLUDE_FEEDID, FEED_ID, FEED_NAME, FEED_TAG
from .const import (
CONF_EXCLUDE_FEEDID,
CONF_ONLY_INCLUDE_FEEDID,
FEED_ID,
FEED_NAME,
FEED_TAG,
)
from .coordinator import EmonCMSConfigEntry, EmoncmsCoordinator
SENSORS: dict[str | None, SensorEntityDescription] = {
@@ -194,11 +200,12 @@ async def async_setup_entry(
) -> None:
"""Set up the emoncms sensors."""
name = sensor_name(entry.data[CONF_URL])
exclude_feeds = entry.data.get(CONF_EXCLUDE_FEEDID)
include_only_feeds = entry.options.get(
CONF_ONLY_INCLUDE_FEEDID, entry.data.get(CONF_ONLY_INCLUDE_FEEDID)
)
if include_only_feeds is None:
if exclude_feeds is None and include_only_feeds is None:
return
coordinator = entry.runtime_data

View File

@@ -111,6 +111,14 @@
}
},
"issues": {
"remove_value_template": {
"title": "The {domain} integration cannot start",
"description": "Configuring {domain} using YAML is being removed and the `{parameter}` parameter cannot be imported.\n\nPlease remove `{parameter}` from your `{domain}` yaml configuration and restart Home Assistant\n\nAlternatively, you may entirely remove the `{domain}` configuration from your configuration.yaml, restart Home Assistant, and add the {domain} integration manually."
},
"missing_include_only_feed_id": {
"title": "No feed synchronized with the {domain} sensor",
"description": "Configuring {domain} using YAML is being removed.\n\nPlease add manually the feeds you want to synchronize with the `configure` button of the integration."
},
"migrate_database": {
"title": "Upgrade your emoncms version",
"description": "Your [emoncms]({url}) does not ship a unique identifier.\n\nPlease upgrade to at least version 11.5.7 and migrate your emoncms database.\n\nMore info in the [emoncms documentation]({doc_url})"

View File

@@ -1,6 +1,5 @@
"""Data update coordinator for the Enigma2 integration."""
import asyncio
import logging
from openwebif.api import OpenWebIfDevice, OpenWebIfStatus
@@ -31,8 +30,6 @@ from .const import CONF_SOURCE_BOUQUET, DOMAIN
LOGGER = logging.getLogger(__package__)
SETUP_TIMEOUT = 10
type Enigma2ConfigEntry = ConfigEntry[Enigma2UpdateCoordinator]
@@ -82,7 +79,7 @@ class Enigma2UpdateCoordinator(DataUpdateCoordinator[OpenWebIfStatus]):
async def _async_setup(self) -> None:
"""Provide needed data to the device info."""
about = await asyncio.wait_for(self.device.get_about(), timeout=SETUP_TIMEOUT)
about = await self.device.get_about()
self.device.mac_address = about["info"]["ifaces"][0]["mac"]
self.device_info["model"] = about["info"]["model"]
self.device_info["manufacturer"] = about["info"]["brand"]

View File

@@ -117,7 +117,6 @@ class FreeboxRouter:
self.name: str = freebox_config["model_info"]["pretty_name"]
self.mac: str = freebox_config["mac"]
self._sw_v: str = freebox_config["firmware_version"]
self._hw_v: str | None = freebox_config.get("board_name")
self._attrs: dict[str, Any] = {}
self.supports_hosts = True
@@ -283,9 +282,7 @@ class FreeboxRouter:
identifiers={(DOMAIN, self.mac)},
manufacturer="Freebox SAS",
name=self.name,
model=self.name,
sw_version=self._sw_v,
hw_version=self._hw_v,
)
@property

View File

@@ -106,7 +106,6 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_logger_{self.host}",
config_entry=self.config_entry,
)
await self.logger_coordinator.async_config_entry_first_refresh()
@@ -121,7 +120,6 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_meters_{self.host}",
config_entry=self.config_entry,
)
)
@@ -131,7 +129,6 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_ohmpilot_{self.host}",
config_entry=self.config_entry,
)
)
@@ -141,7 +138,6 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_power_flow_{self.host}",
config_entry=self.config_entry,
)
)
@@ -151,7 +147,6 @@ class FroniusSolarNet:
solar_net=self,
logger=_LOGGER,
name=f"{DOMAIN}_storages_{self.host}",
config_entry=self.config_entry,
)
)
@@ -211,7 +206,6 @@ class FroniusSolarNet:
logger=_LOGGER,
name=_inverter_name,
inverter_info=_inverter_info,
config_entry=self.config_entry,
)
if self.config_entry.state == ConfigEntryState.LOADED:
await _coordinator.async_refresh()

View File

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

View File

@@ -124,11 +124,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
f"Error generating content due to content violations, reason: {response.prompt_feedback.block_reason_message}"
)
if (
not response.candidates
or not response.candidates[0].content
or not response.candidates[0].content.parts
):
if not response.candidates[0].content.parts:
raise HomeAssistantError("Unknown error generating content")
return {"text": response.text}

View File

@@ -377,7 +377,7 @@ async def google_generative_ai_config_option_schema(
value=api_model.name,
)
for api_model in sorted(
api_models, key=lambda x: (x.name or "").lstrip("models/")
api_models, key=lambda x: x.name.lstrip("models/") or ""
)
if (
api_model.name

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
import codecs
from collections.abc import AsyncGenerator, AsyncIterator, Callable
from collections.abc import AsyncGenerator, Callable
from dataclasses import replace
import mimetypes
from pathlib import Path
@@ -15,7 +15,6 @@ from google.genai.errors import APIError, ClientError
from google.genai.types import (
AutomaticFunctionCallingConfig,
Content,
ContentDict,
File,
FileState,
FunctionDeclaration,
@@ -24,11 +23,9 @@ from google.genai.types import (
GoogleSearch,
HarmCategory,
Part,
PartUnionDict,
SafetySetting,
Schema,
Tool,
ToolListUnion,
)
import voluptuous as vol
from voluptuous_openapi import convert
@@ -240,7 +237,7 @@ def _convert_content(
async def _transform_stream(
result: AsyncIterator[GenerateContentResponse],
result: AsyncGenerator[GenerateContentResponse],
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
new_message = True
try:
@@ -345,7 +342,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
"""Generate an answer for the chat log."""
options = self.subentry.data
tools: ToolListUnion | None = None
tools: list[Tool | Callable[..., Any]] | None = None
if chat_log.llm_api:
tools = [
_format_tool(tool, chat_log.llm_api.custom_serializer)
@@ -376,7 +373,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
else:
raise HomeAssistantError("Invalid prompt content")
messages: list[Content | ContentDict] = []
messages: list[Content] = []
# Google groups tool results, we do not. Group them before sending.
tool_results: list[conversation.ToolResultContent] = []
@@ -403,10 +400,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
# The SDK requires the first message to be a user message
# This is not the case if user used `start_conversation`
# Workaround from https://github.com/googleapis/python-genai/issues/529#issuecomment-2740964537
if messages and (
(isinstance(messages[0], Content) and messages[0].role != "user")
or (isinstance(messages[0], dict) and messages[0]["role"] != "user")
):
if messages and messages[0].role != "user":
messages.insert(
0,
Content(role="user", parts=[Part.from_text(text=" ")]),
@@ -446,14 +440,14 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
)
user_message = chat_log.content[-1]
assert isinstance(user_message, conversation.UserContent)
chat_request: list[PartUnionDict] = [user_message.content]
chat_request: str | list[Part] = user_message.content
if user_message.attachments:
files = await async_prepare_files_for_prompt(
self.hass,
self._genai_client,
[a.path for a in user_message.attachments],
)
chat_request = [*chat_request, *files]
chat_request = [chat_request, *files]
# To prevent infinite loops, we limit the number of iterations
for _iteration in range(MAX_TOOL_ITERATIONS):
@@ -470,17 +464,15 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
error = ERROR_GETTING_RESPONSE
raise HomeAssistantError(error) from err
chat_request = list(
_create_google_tool_response_parts(
[
content
async for content in chat_log.async_add_delta_content_stream(
self.entity_id,
_transform_stream(chat_response_generator),
)
if isinstance(content, conversation.ToolResultContent)
]
)
chat_request = _create_google_tool_response_parts(
[
content
async for content in chat_log.async_add_delta_content_stream(
self.entity_id,
_transform_stream(chat_response_generator),
)
if isinstance(content, conversation.ToolResultContent)
]
)
if not chat_log.unresponded_tool_results:
@@ -567,13 +559,13 @@ async def async_prepare_files_for_prompt(
await asyncio.sleep(FILE_POLLING_INTERVAL_SECONDS)
uploaded_file = await client.aio.files.get(
name=uploaded_file.name or "",
name=uploaded_file.name,
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
)
if uploaded_file.state == FileState.FAILED:
raise HomeAssistantError(
f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message if uploaded_file.error else 'unknown'}"
f"File `{uploaded_file.name}` processing failed, reason: {uploaded_file.error.message}"
)
prompt_parts = await hass.async_add_executor_job(upload_files)

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/google_generative_ai_conversation",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["google-genai==1.29.0"]
"requirements": ["google-genai==1.7.0"]
}

View File

@@ -123,10 +123,10 @@
},
"ai_task_data": {
"initiate_flow": {
"user": "Add AI task",
"reconfigure": "Reconfigure AI task"
"user": "Add Generate data with AI service",
"reconfigure": "Reconfigure Generate data with AI service"
},
"entry_type": "AI task",
"entry_type": "Generate data with AI service",
"step": {
"set_options": {
"data": {

View File

@@ -146,41 +146,15 @@ class GoogleGenerativeAITextToSpeechEntity(
)
)
)
def _extract_audio_parts(
response: types.GenerateContentResponse,
) -> tuple[bytes, str]:
if (
not response.candidates
or not response.candidates[0].content
or not response.candidates[0].content.parts
or not response.candidates[0].content.parts[0].inline_data
):
raise ValueError("No content returned from TTS generation")
data = response.candidates[0].content.parts[0].inline_data.data
mime_type = response.candidates[0].content.parts[0].inline_data.mime_type
if not isinstance(data, bytes):
raise TypeError(
f"Expected bytes for audio data, got {type(data).__name__}"
)
if not isinstance(mime_type, str):
raise TypeError(
f"Expected str for mime_type, got {type(mime_type).__name__}"
)
return data, mime_type
try:
response = await self._genai_client.aio.models.generate_content(
model=self.subentry.data.get(CONF_CHAT_MODEL, RECOMMENDED_TTS_MODEL),
contents=message,
config=config,
)
data, mime_type = _extract_audio_parts(response)
except (APIError, ClientError, ValueError, TypeError) as exc:
data = response.candidates[0].content.parts[0].inline_data.data
mime_type = response.candidates[0].content.parts[0].inline_data.mime_type
except (APIError, ClientError, ValueError) as exc:
LOGGER.error("Error during TTS: %s", exc, exc_info=True)
raise HomeAssistantError(exc) from exc
return "wav", convert_to_wav(data, mime_type)

View File

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

View File

@@ -117,43 +117,35 @@
},
"unhealthy": {
"title": "Unhealthy system - {reason}",
"description": "System is currently unhealthy due to {reason}. For troubleshooting information, select Learn more."
"description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this."
},
"unhealthy_docker": {
"title": "Unhealthy system - Docker misconfigured",
"description": "System is currently unhealthy because Docker is configured incorrectly. For troubleshooting information, select Learn more."
},
"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_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. 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."
"description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this."
},
"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."
"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_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_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."
},
"unhealthy_untrusted": {
"title": "Unhealthy system - Untrusted code",
"description": "System is currently unhealthy because it has detected untrusted code or images in use. For troubleshooting information, select Learn more."
"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."
},
"unsupported": {
"title": "Unsupported system - {reason}",
"description": "System is unsupported due to {reason}. For troubleshooting information, select Learn more."
"description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this."
},
"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. For troubleshooting information, select Learn more."
"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."
},
"unsupported_cgroup_version": {
"title": "Unsupported system - CGroup version",
@@ -161,23 +153,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. For troubleshooting information, select Learn more."
"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."
},
"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. For troubleshooting information, select Learn more."
"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."
},
"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. For troubleshooting information, select Learn more."
"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."
},
"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. For troubleshooting information, select Learn more."
"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."
},
"unsupported_docker_configuration": {
"title": "Unsupported system - Docker misconfigured",
"description": "System is unsupported because the Docker daemon is running in an unexpected way. For troubleshooting information, select Learn more."
"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."
},
"unsupported_docker_version": {
"title": "Unsupported system - Docker version",
@@ -185,15 +177,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. For troubleshooting information, select Learn more."
"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."
},
"unsupported_lxc": {
"title": "Unsupported system - LXC detected",
"description": "System is unsupported because it is being run in an LXC virtual machine. For troubleshooting information, select Learn more."
"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."
},
"unsupported_network_manager": {
"title": "Unsupported system - Network Manager issues",
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
},
"unsupported_os": {
"title": "Unsupported system - Operating System",
@@ -201,43 +193,39 @@
},
"unsupported_os_agent": {
"title": "Unsupported system - OS-Agent issues",
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
},
"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. For troubleshooting information, select Learn more."
"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."
},
"unsupported_software": {
"title": "Unsupported system - Unsupported software",
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. For troubleshooting information, select Learn more."
"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."
},
"unsupported_source_mods": {
"title": "Unsupported system - Supervisor source modifications",
"description": "System is unsupported because Supervisor source code has been modified. For troubleshooting information, select Learn more."
"description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this."
},
"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. For troubleshooting information, select Learn more."
"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."
},
"unsupported_systemd": {
"title": "Unsupported system - Systemd issues",
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
},
"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. For troubleshooting information, select Learn more."
"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."
},
"unsupported_systemd_resolved": {
"title": "Unsupported system - Systemd-Resolved issues",
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. For troubleshooting information, select Learn more."
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
},
"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. 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. For troubleshooting information, select Learn more."
"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."
}
},
"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.78", "babel==2.15.0"]
"requirements": ["holidays==0.77", "babel==2.15.0"]
}

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import asyncio
from asyncio import sleep as asyncio_sleep
from collections import defaultdict
from collections.abc import Callable
@@ -54,7 +53,6 @@ _LOGGER = logging.getLogger(__name__)
MAX_EXECUTIONS_TIME_WINDOW = 60 * 60 # 1 hour
MAX_EXECUTIONS = 8
UPDATE_PROGRAMS_INTERVAL_SEC = 1 # 1 second
type HomeConnectConfigEntry = ConfigEntry[HomeConnectCoordinator]
@@ -253,30 +251,11 @@ class HomeConnectCoordinator(
EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM,
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM,
):
program_key = ProgramKey(cast(str, event.value))
# If the active program is unknown, options must be updated using the selected program.
if (
event_key
is EventKey.BSH_COMMON_ROOT_ACTIVE_PROGRAM
and event.value is ProgramKey.UNKNOWN
):
select_program_event = events.get(
EventKey.BSH_COMMON_ROOT_SELECTED_PROGRAM
)
if select_program_event:
program_key = cast(
ProgramKey,
select_program_event.value,
)
# Wait for a second before updating options because it may take time for the Home Connect API
# to update the options after the program change.
await asyncio.sleep(
UPDATE_PROGRAMS_INTERVAL_SEC
)
await self.update_options(
event_message_ha_id, event_key, program_key
event_message_ha_id,
event_key,
ProgramKey(cast(str, event.value)),
)
events[event_key] = event
self._call_event_listener(event_message)

View File

@@ -11,16 +11,10 @@ from pyHomee import (
)
import voluptuous as vol
from homeassistant.config_entries import SOURCE_USER, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import (
DOMAIN,
RESULT_CANNOT_CONNECT,
RESULT_INVALID_AUTH,
RESULT_UNKNOWN_ERROR,
)
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -39,137 +33,60 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
homee: Homee
_host: str
_name: str
_reauth_host: str
_reauth_username: str
async def _connect_homee(self) -> dict[str, str]:
errors: dict[str, str] = {}
try:
await self.homee.get_access_token()
except HomeeConnectionFailedException:
errors["base"] = RESULT_CANNOT_CONNECT
except HomeeAuthenticationFailedException:
errors["base"] = RESULT_INVALID_AUTH
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = RESULT_UNKNOWN_ERROR
else:
_LOGGER.info("Got access token for homee")
self.hass.loop.create_task(self.homee.run())
_LOGGER.debug("Homee task created")
await self.homee.wait_until_connected()
_LOGGER.info("Homee connected")
self.homee.disconnect()
_LOGGER.debug("Homee disconnecting")
await self.homee.wait_until_disconnected()
_LOGGER.info("Homee config successfully tested")
await self.async_set_unique_id(
self.homee.settings.uid, raise_on_progress=self.source != SOURCE_USER
)
self._abort_if_unique_id_configured()
_LOGGER.info("Created new homee entry with ID %s", self.homee.settings.uid)
return errors
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial user step."""
errors: dict[str, str] = {}
errors = {}
if user_input is not None:
self.homee = Homee(
user_input[CONF_HOST],
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
errors = await self._connect_homee()
if not errors:
try:
await self.homee.get_access_token()
except HomeeConnectionFailedException:
errors["base"] = "cannot_connect"
except HomeeAuthenticationFailedException:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
_LOGGER.info("Got access token for homee")
self.hass.loop.create_task(self.homee.run())
_LOGGER.debug("Homee task created")
await self.homee.wait_until_connected()
_LOGGER.info("Homee connected")
self.homee.disconnect()
_LOGGER.debug("Homee disconnecting")
await self.homee.wait_until_disconnected()
_LOGGER.info("Homee config successfully tested")
await self.async_set_unique_id(self.homee.settings.uid)
self._abort_if_unique_id_configured()
_LOGGER.info(
"Created new homee entry with ID %s", self.homee.settings.uid
)
return self.async_create_entry(
title=f"{self.homee.settings.homee_name} ({self.homee.host})",
data=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=AUTH_SCHEMA,
errors=errors,
)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
# Ensure that an IPv4 address is received
self._host = discovery_info.host
self._name = discovery_info.hostname[6:18]
if discovery_info.ip_address.version == 6:
return self.async_abort(reason="ipv6_address")
await self.async_set_unique_id(self._name)
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
# Cause an auth-error to see if homee is reachable.
self.homee = Homee(
self._host,
"dummy_username",
"dummy_password",
)
errors = await self._connect_homee()
if errors["base"] != RESULT_INVALID_AUTH:
return self.async_abort(reason=RESULT_CANNOT_CONNECT)
self.context["title_placeholders"] = {"name": self._name, "host": self._host}
return await self.async_step_zeroconf_confirm()
async def async_step_zeroconf_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm the configuration of the device."""
errors: dict[str, str] = {}
if user_input is not None:
self.homee = Homee(
self._host,
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
errors = await self._connect_homee()
if not errors:
return self.async_create_entry(
title=f"{self.homee.settings.homee_name} ({self.homee.host})",
data={
CONF_HOST: self._host,
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
return self.async_show_form(
step_id="zeroconf_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
),
errors=errors,
description_placeholders={
CONF_HOST: self._name,
},
last_step=True,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
@@ -191,12 +108,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
try:
await self.homee.get_access_token()
except HomeeConnectionFailedException:
errors["base"] = RESULT_CANNOT_CONNECT
errors["base"] = "cannot_connect"
except HomeeAuthenticationFailedException:
errors["base"] = RESULT_INVALID_AUTH
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = RESULT_UNKNOWN_ERROR
errors["base"] = "unknown"
else:
self.hass.loop.create_task(self.homee.run())
await self.homee.wait_until_connected()
@@ -244,12 +161,12 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
try:
await self.homee.get_access_token()
except HomeeConnectionFailedException:
errors["base"] = RESULT_CANNOT_CONNECT
errors["base"] = "cannot_connect"
except HomeeAuthenticationFailedException:
errors["base"] = RESULT_INVALID_AUTH
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = RESULT_UNKNOWN_ERROR
errors["base"] = "unknown"
else:
self.hass.loop.create_task(self.homee.run())
await self.homee.wait_until_connected()

View File

@@ -20,11 +20,6 @@ from homeassistant.const import (
# General
DOMAIN = "homee"
# Error strings
RESULT_CANNOT_CONNECT = "cannot_connect"
RESULT_INVALID_AUTH = "invalid_auth"
RESULT_UNKNOWN_ERROR = "unknown"
# Sensor mappings
HOMEE_UNIT_TO_HA_UNIT = {
"": None,

View File

@@ -8,11 +8,5 @@
"iot_class": "local_push",
"loggers": ["homee"],
"quality_scale": "silver",
"requirements": ["pyHomee==1.2.10"],
"zeroconf": [
{
"type": "_ssh._tcp.local.",
"name": "homee-*"
}
]
"requirements": ["pyHomee==1.2.10"]
}

View File

@@ -46,17 +46,6 @@
"data_description": {
"host": "[%key:component::homee::config::step::user::data_description::host%]"
}
},
"zeroconf_confirm": {
"title": "Configure discovered homee {host}",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"username": "[%key:component::homee::config::step::user::data_description::username%]",
"password": "[%key:component::homee::config::step::user::data_description::password%]"
}
}
}
},

View File

@@ -24,7 +24,6 @@ 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,
@@ -55,6 +54,7 @@ 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,6 +2,8 @@
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 ATTR_CONFIG_ENTRY_ID, CONF_RECIPIENT
from homeassistant.const import CONF_RECIPIENT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import Router
from .const import DOMAIN
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN
_LOGGER = logging.getLogger(__name__)

View File

@@ -163,7 +163,6 @@ async def async_setup_entry(
name="light",
update_method=partial(async_safe_fetch, bridge, bridge.api.lights.update),
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
request_refresh_debouncer=Debouncer(
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
),
@@ -198,7 +197,6 @@ async def async_setup_entry(
name="group",
update_method=partial(async_safe_fetch, bridge, bridge.api.groups.update),
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
request_refresh_debouncer=Debouncer(
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True
),

View File

@@ -53,7 +53,6 @@ class SensorManager:
LOGGER,
name="sensor",
update_method=self.async_update_data,
config_entry=bridge.config_entry,
update_interval=self.SCAN_INTERVAL,
request_refresh_debouncer=debounce.Debouncer(
bridge.hass, LOGGER, cooldown=REQUEST_REFRESH_DELAY, immediate=True

View File

@@ -21,7 +21,6 @@ PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.CALENDAR,
Platform.DEVICE_TRACKER,
Platform.EVENT,
Platform.LAWN_MOWER,
Platform.NUMBER,
Platform.SELECT,

View File

@@ -21,20 +21,6 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
async def async_reset_cutting_blade_usage_time(
session: AutomowerSession,
mower_id: str,
) -> None:
"""Reset cutting blade usage time."""
await session.commands.reset_cutting_blade_usage_time(mower_id)
def reset_cutting_blade_usage_time_availability(data: MowerAttributes) -> bool:
"""Return True if blade usage time is greater than 0."""
value = data.statistics.cutting_blade_usage_time
return value is not None and value > 0
@dataclass(frozen=True, kw_only=True)
class AutomowerButtonEntityDescription(ButtonEntityDescription):
"""Describes Automower button entities."""
@@ -42,7 +28,6 @@ class AutomowerButtonEntityDescription(ButtonEntityDescription):
available_fn: Callable[[MowerAttributes], bool] = lambda _: True
exists_fn: Callable[[MowerAttributes], bool] = lambda _: True
press_fn: Callable[[AutomowerSession, str], Awaitable[Any]]
poll_after_sending: bool = False
MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
@@ -58,14 +43,6 @@ MOWER_BUTTON_TYPES: tuple[AutomowerButtonEntityDescription, ...] = (
translation_key="sync_clock",
press_fn=lambda session, mower_id: session.commands.set_datetime(mower_id),
),
AutomowerButtonEntityDescription(
key="reset_cutting_blade_usage_time",
translation_key="reset_cutting_blade_usage_time",
available_fn=reset_cutting_blade_usage_time_availability,
exists_fn=lambda data: data.statistics.cutting_blade_usage_time is not None,
press_fn=async_reset_cutting_blade_usage_time,
poll_after_sending=True,
),
)
@@ -116,5 +93,3 @@ class AutomowerButtonEntity(AutomowerControlEntity, ButtonEntity):
async def async_press(self) -> None:
"""Send a command to the mower."""
await self.entity_description.press_fn(self.coordinator.api, self.mower_id)
if self.entity_description.poll_after_sending:
await self.coordinator.async_request_refresh()

View File

@@ -17,128 +17,3 @@ ERROR_STATES = [
MowerStates.WAIT_POWER_UP,
MowerStates.WAIT_UPDATING,
]
ERROR_KEYS = [
"alarm_mower_in_motion",
"alarm_mower_lifted",
"alarm_mower_stopped",
"alarm_mower_switched_off",
"alarm_mower_tilted",
"alarm_outside_geofence",
"angular_sensor_problem",
"battery_problem",
"battery_restriction_due_to_ambient_temperature",
"can_error",
"charging_current_too_high",
"charging_station_blocked",
"charging_system_problem",
"collision_sensor_defect",
"collision_sensor_error",
"collision_sensor_problem_front",
"collision_sensor_problem_rear",
"com_board_not_available",
"communication_circuit_board_sw_must_be_updated",
"complex_working_area",
"connection_changed",
"connection_not_changed",
"connectivity_problem",
"connectivity_settings_restored",
"cutting_drive_motor_1_defect",
"cutting_drive_motor_2_defect",
"cutting_drive_motor_3_defect",
"cutting_height_blocked",
"cutting_height_problem",
"cutting_height_problem_curr",
"cutting_height_problem_dir",
"cutting_height_problem_drive",
"cutting_motor_problem",
"cutting_stopped_slope_too_steep",
"cutting_system_blocked",
"cutting_system_imbalance_warning",
"cutting_system_major_imbalance",
"destination_not_reachable",
"difficult_finding_home",
"docking_sensor_defect",
"electronic_problem",
"empty_battery",
"folding_cutting_deck_sensor_defect",
"folding_sensor_activated",
"geofence_problem",
"gps_navigation_problem",
"guide_1_not_found",
"guide_2_not_found",
"guide_3_not_found",
"guide_calibration_accomplished",
"guide_calibration_failed",
"high_charging_power_loss",
"high_internal_power_loss",
"high_internal_temperature",
"internal_voltage_error",
"invalid_battery_combination_invalid_combination_of_different_battery_types",
"invalid_sub_device_combination",
"invalid_system_configuration",
"left_brush_motor_overloaded",
"lift_sensor_defect",
"lifted",
"limited_cutting_height_range",
"loop_sensor_defect",
"loop_sensor_problem_front",
"loop_sensor_problem_left",
"loop_sensor_problem_rear",
"loop_sensor_problem_right",
"low_battery",
"memory_circuit_problem",
"mower_lifted",
"mower_tilted",
"no_accurate_position_from_satellites",
"no_confirmed_position",
"no_drive",
"no_loop_signal",
"no_power_in_charging_station",
"no_response_from_charger",
"outside_working_area",
"poor_signal_quality",
"reference_station_communication_problem",
"right_brush_motor_overloaded",
"safety_function_faulty",
"settings_restored",
"sim_card_locked",
"sim_card_not_found",
"sim_card_requires_pin",
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
"slope_too_steep",
"sms_could_not_be_sent",
"stop_button_problem",
"stuck_in_charging_station",
"switch_cord_problem",
"temporary_battery_problem",
"tilt_sensor_problem",
"too_high_discharge_current",
"too_high_internal_current",
"trapped",
"ultrasonic_problem",
"ultrasonic_sensor_1_defect",
"ultrasonic_sensor_2_defect",
"ultrasonic_sensor_3_defect",
"ultrasonic_sensor_4_defect",
"unexpected_cutting_height_adj",
"unexpected_error",
"upside_down",
"weak_gps_signal",
"wheel_drive_problem_left",
"wheel_drive_problem_rear_left",
"wheel_drive_problem_rear_right",
"wheel_drive_problem_right",
"wheel_motor_blocked_left",
"wheel_motor_blocked_rear_left",
"wheel_motor_blocked_rear_right",
"wheel_motor_blocked_right",
"wheel_motor_overloaded_left",
"wheel_motor_overloaded_rear_left",
"wheel_motor_overloaded_rear_right",
"wheel_motor_overloaded_right",
"work_area_not_valid",
"wrong_loop_signal",
"wrong_pin_code",
"zone_generator_problem",
]

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
from collections.abc import Callable
from datetime import datetime, timedelta
from datetime import timedelta
import logging
from typing import override
@@ -12,10 +12,9 @@ from aioautomower.exceptions import (
ApiError,
AuthError,
HusqvarnaTimeoutError,
HusqvarnaWSClientError,
HusqvarnaWSServerHandshakeError,
)
from aioautomower.model import MowerDictionary, MowerStates
from aioautomower.model import MowerDictionary
from aioautomower.session import AutomowerSession
from homeassistant.config_entries import ConfigEntry
@@ -30,9 +29,7 @@ _LOGGER = logging.getLogger(__name__)
MAX_WS_RECONNECT_TIME = 600
SCAN_INTERVAL = timedelta(minutes=8)
DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time
PONG_TIMEOUT = timedelta(seconds=90)
PING_INTERVAL = timedelta(seconds=10)
PING_TIMEOUT = timedelta(seconds=5)
type AutomowerConfigEntry = ConfigEntry[AutomowerDataUpdateCoordinator]
@@ -61,9 +58,6 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
self.new_devices_callbacks: list[Callable[[set[str]], None]] = []
self.new_zones_callbacks: list[Callable[[str, set[str]], None]] = []
self.new_areas_callbacks: list[Callable[[str, set[int]], None]] = []
self.pong: datetime | None = None
self.websocket_alive: bool = False
self._watchdog_task: asyncio.Task | None = None
@override
@callback
@@ -77,18 +71,6 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
await self.api.connect()
self.api.register_data_callback(self.handle_websocket_updates)
self.ws_connected = True
def start_watchdog() -> None:
if self._watchdog_task is not None and not self._watchdog_task.done():
_LOGGER.debug("Cancelling previous watchdog task")
self._watchdog_task.cancel()
self._watchdog_task = self.config_entry.async_create_background_task(
self.hass,
self._pong_watchdog(),
"websocket_watchdog",
)
self.api.register_ws_ready_callback(start_watchdog)
try:
data = await self.api.get_status()
except ApiError as err:
@@ -111,19 +93,6 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
mower_data.capabilities.work_areas for mower_data in self.data.values()
):
self._async_add_remove_work_areas()
if (
not self._should_poll()
and self.update_interval is not None
and self.websocket_alive
):
_LOGGER.debug("All mowers inactive and websocket alive: stop polling")
self.update_interval = None
if self.update_interval is None and self._should_poll():
_LOGGER.debug(
"Polling re-enabled via WebSocket: at least one mower active"
)
self.update_interval = SCAN_INTERVAL
self.hass.async_create_task(self.async_request_refresh())
@callback
def handle_websocket_updates(self, ws_data: MowerDictionary) -> None:
@@ -173,7 +142,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
# Reset reconnect time after successful connection
self.reconnect_time = DEFAULT_RECONNECT_TIME
await automower_client.start_listening()
except (HusqvarnaWSServerHandshakeError, HusqvarnaWSClientError) as err:
except HusqvarnaWSServerHandshakeError as err:
_LOGGER.debug(
"Failed to connect to websocket. Trying to reconnect: %s",
err,
@@ -192,30 +161,6 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
"reconnect_task",
)
def _should_poll(self) -> bool:
"""Return True if at least one mower is connected and at least one is not OFF."""
return any(mower.metadata.connected for mower in self.data.values()) and any(
mower.mower.state != MowerStates.OFF for mower in self.data.values()
)
async def _pong_watchdog(self) -> None:
_LOGGER.debug("Watchdog started")
try:
while True:
_LOGGER.debug("Sending ping")
self.websocket_alive = await self.api.send_empty_message()
_LOGGER.debug("Ping result: %s", self.websocket_alive)
await asyncio.sleep(60)
_LOGGER.debug("Websocket alive %s", self.websocket_alive)
if not self.websocket_alive:
_LOGGER.debug("No pong received → restart polling")
if self.update_interval is None:
self.update_interval = SCAN_INTERVAL
await self.async_request_refresh()
except asyncio.CancelledError:
_LOGGER.debug("Watchdog cancelled")
def _async_add_remove_devices(self) -> None:
"""Add new devices and remove orphaned devices from the registry."""
current_devices = set(self.data)

View File

@@ -1,108 +0,0 @@
"""Creates the event entities for supported mowers."""
from collections.abc import Callable
from aioautomower.model import SingleMessageData
from homeassistant.components.event import (
DOMAIN as EVENT_DOMAIN,
EventEntity,
EventEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AutomowerConfigEntry
from .const import ERROR_KEYS
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerBaseEntity
PARALLEL_UPDATES = 1
ATTR_SEVERITY = "severity"
ATTR_LATITUDE = "latitude"
ATTR_LONGITUDE = "longitude"
ATTR_DATE_TIME = "date_time"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AutomowerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Automower message event entities.
Entities are created dynamically based on messages received from the API,
but only for mowers that support message events.
"""
coordinator = config_entry.runtime_data
entity_registry = er.async_get(hass)
restored_mowers = {
entry.unique_id.removesuffix("_message")
for entry in er.async_entries_for_config_entry(
entity_registry, config_entry.entry_id
)
if entry.domain == EVENT_DOMAIN
}
async_add_entities(
AutomowerMessageEventEntity(mower_id, coordinator)
for mower_id in restored_mowers
if mower_id in coordinator.data
)
@callback
def _handle_message(msg: SingleMessageData) -> None:
if msg.id in restored_mowers:
return
restored_mowers.add(msg.id)
async_add_entities([AutomowerMessageEventEntity(msg.id, coordinator)])
coordinator.api.register_single_message_callback(_handle_message)
class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity):
"""EventEntity for Automower message events."""
entity_description: EventEntityDescription
_message_cb: Callable[[SingleMessageData], None]
_attr_translation_key = "message"
_attr_event_types = ERROR_KEYS
def __init__(
self,
mower_id: str,
coordinator: AutomowerDataUpdateCoordinator,
) -> None:
"""Initialize Automower message event entity."""
super().__init__(mower_id, coordinator)
self._attr_unique_id = f"{mower_id}_message"
@callback
def _handle(self, msg: SingleMessageData) -> None:
"""Handle a message event from the API and trigger the event entity if it matches the entity's mower ID."""
if msg.id != self.mower_id:
return
message = msg.attributes.message
self._trigger_event(
message.code,
{
ATTR_SEVERITY: message.severity,
ATTR_LATITUDE: message.latitude,
ATTR_LONGITUDE: message.longitude,
ATTR_DATE_TIME: message.time,
},
)
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:
"""Register callback when entity is added to hass."""
await super().async_added_to_hass()
self.coordinator.api.register_single_message_callback(self._handle)
async def async_will_remove_from_hass(self) -> None:
"""Unregister WebSocket callback when entity is removed."""
self.coordinator.api.unregister_single_message_callback(self._handle)

View File

@@ -8,14 +8,6 @@
"button": {
"sync_clock": {
"default": "mdi:clock-check-outline"
},
"reset_cutting_blade_usage_time": {
"default": "mdi:saw-blade"
}
},
"event": {
"message": {
"default": "mdi:alert-circle-check-outline"
}
},
"number": {

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2.1.2"]
"requirements": ["aioautomower==2.1.1"]
}

View File

@@ -28,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import AutomowerConfigEntry
from .const import ERROR_KEYS, ERROR_STATES
from .const import ERROR_STATES
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import (
AutomowerBaseEntity,
@@ -42,8 +42,135 @@ PARALLEL_UPDATES = 0
ATTR_WORK_AREA_ID_ASSIGNMENT = "work_area_id_assignment"
ERROR_KEY_LIST = sorted(
set(ERROR_KEYS) | {state.lower() for state in ERROR_STATES} | {"no_error"}
ERROR_KEYS = [
"alarm_mower_in_motion",
"alarm_mower_lifted",
"alarm_mower_stopped",
"alarm_mower_switched_off",
"alarm_mower_tilted",
"alarm_outside_geofence",
"angular_sensor_problem",
"battery_problem",
"battery_restriction_due_to_ambient_temperature",
"can_error",
"charging_current_too_high",
"charging_station_blocked",
"charging_system_problem",
"collision_sensor_defect",
"collision_sensor_error",
"collision_sensor_problem_front",
"collision_sensor_problem_rear",
"com_board_not_available",
"communication_circuit_board_sw_must_be_updated",
"complex_working_area",
"connection_changed",
"connection_not_changed",
"connectivity_problem",
"connectivity_settings_restored",
"cutting_drive_motor_1_defect",
"cutting_drive_motor_2_defect",
"cutting_drive_motor_3_defect",
"cutting_height_blocked",
"cutting_height_problem_curr",
"cutting_height_problem_dir",
"cutting_height_problem_drive",
"cutting_height_problem",
"cutting_motor_problem",
"cutting_stopped_slope_too_steep",
"cutting_system_blocked",
"cutting_system_imbalance_warning",
"cutting_system_major_imbalance",
"destination_not_reachable",
"difficult_finding_home",
"docking_sensor_defect",
"electronic_problem",
"empty_battery",
"folding_cutting_deck_sensor_defect",
"folding_sensor_activated",
"geofence_problem",
"gps_navigation_problem",
"guide_1_not_found",
"guide_2_not_found",
"guide_3_not_found",
"guide_calibration_accomplished",
"guide_calibration_failed",
"high_charging_power_loss",
"high_internal_power_loss",
"high_internal_temperature",
"internal_voltage_error",
"invalid_battery_combination_invalid_combination_of_different_battery_types",
"invalid_sub_device_combination",
"invalid_system_configuration",
"left_brush_motor_overloaded",
"lift_sensor_defect",
"lifted",
"limited_cutting_height_range",
"loop_sensor_defect",
"loop_sensor_problem_front",
"loop_sensor_problem_left",
"loop_sensor_problem_rear",
"loop_sensor_problem_right",
"low_battery",
"memory_circuit_problem",
"mower_lifted",
"mower_tilted",
"no_accurate_position_from_satellites",
"no_confirmed_position",
"no_drive",
"no_error",
"no_loop_signal",
"no_power_in_charging_station",
"no_response_from_charger",
"outside_working_area",
"poor_signal_quality",
"reference_station_communication_problem",
"right_brush_motor_overloaded",
"safety_function_faulty",
"settings_restored",
"sim_card_locked",
"sim_card_not_found",
"sim_card_requires_pin",
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern",
"slope_too_steep",
"sms_could_not_be_sent",
"stop_button_problem",
"stuck_in_charging_station",
"switch_cord_problem",
"temporary_battery_problem",
"tilt_sensor_problem",
"too_high_discharge_current",
"too_high_internal_current",
"trapped",
"ultrasonic_problem",
"ultrasonic_sensor_1_defect",
"ultrasonic_sensor_2_defect",
"ultrasonic_sensor_3_defect",
"ultrasonic_sensor_4_defect",
"unexpected_cutting_height_adj",
"unexpected_error",
"upside_down",
"weak_gps_signal",
"wheel_drive_problem_left",
"wheel_drive_problem_rear_left",
"wheel_drive_problem_rear_right",
"wheel_drive_problem_right",
"wheel_motor_blocked_left",
"wheel_motor_blocked_rear_left",
"wheel_motor_blocked_rear_right",
"wheel_motor_blocked_right",
"wheel_motor_overloaded_left",
"wheel_motor_overloaded_rear_left",
"wheel_motor_overloaded_rear_right",
"wheel_motor_overloaded_right",
"work_area_not_valid",
"wrong_loop_signal",
"wrong_pin_code",
"zone_generator_problem",
]
ERROR_KEY_LIST = list(
dict.fromkeys(ERROR_KEYS + [state.lower() for state in ERROR_STATES])
)
INACTIVE_REASONS: list = [

View File

@@ -53,161 +53,6 @@
},
"sync_clock": {
"name": "Sync clock"
},
"reset_cutting_blade_usage_time": {
"name": "Reset cutting blade usage time"
}
},
"event": {
"message": {
"name": "Message",
"state_attributes": {
"event_type": {
"state": {
"alarm_mower_in_motion": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_in_motion%]",
"alarm_mower_lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_lifted%]",
"alarm_mower_stopped": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_stopped%]",
"alarm_mower_switched_off": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_switched_off%]",
"alarm_mower_tilted": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_mower_tilted%]",
"alarm_outside_geofence": "[%key:component::husqvarna_automower::entity::sensor::error::state::alarm_outside_geofence%]",
"angular_sensor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::angular_sensor_problem%]",
"battery_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_problem%]",
"battery_restriction_due_to_ambient_temperature": "[%key:component::husqvarna_automower::entity::sensor::error::state::battery_restriction_due_to_ambient_temperature%]",
"can_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::can_error%]",
"charging_current_too_high": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_current_too_high%]",
"charging_station_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_station_blocked%]",
"charging_system_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::charging_system_problem%]",
"collision_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_defect%]",
"collision_sensor_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_error%]",
"collision_sensor_problem_front": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_problem_front%]",
"collision_sensor_problem_rear": "[%key:component::husqvarna_automower::entity::sensor::error::state::collision_sensor_problem_rear%]",
"com_board_not_available": "[%key:component::husqvarna_automower::entity::sensor::error::state::com_board_not_available%]",
"communication_circuit_board_sw_must_be_updated": "[%key:component::husqvarna_automower::entity::sensor::error::state::communication_circuit_board_sw_must_be_updated%]",
"complex_working_area": "[%key:component::husqvarna_automower::entity::sensor::error::state::complex_working_area%]",
"connection_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::connection_changed%]",
"connection_not_changed": "[%key:component::husqvarna_automower::entity::sensor::error::state::connection_not_changed%]",
"connectivity_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::connectivity_problem%]",
"connectivity_settings_restored": "[%key:component::husqvarna_automower::entity::sensor::error::state::connectivity_settings_restored%]",
"cutting_drive_motor_1_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_1_defect%]",
"cutting_drive_motor_2_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_2_defect%]",
"cutting_drive_motor_3_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_drive_motor_3_defect%]",
"cutting_height_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_blocked%]",
"cutting_height_problem_curr": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_curr%]",
"cutting_height_problem_dir": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_dir%]",
"cutting_height_problem_drive": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem_drive%]",
"cutting_height_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_height_problem%]",
"cutting_motor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_motor_problem%]",
"cutting_stopped_slope_too_steep": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_stopped_slope_too_steep%]",
"cutting_system_blocked": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_blocked%]",
"cutting_system_imbalance_warning": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_imbalance_warning%]",
"cutting_system_major_imbalance": "[%key:component::husqvarna_automower::entity::sensor::error::state::cutting_system_major_imbalance%]",
"destination_not_reachable": "[%key:component::husqvarna_automower::entity::sensor::error::state::destination_not_reachable%]",
"difficult_finding_home": "[%key:component::husqvarna_automower::entity::sensor::error::state::difficult_finding_home%]",
"docking_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::docking_sensor_defect%]",
"electronic_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::electronic_problem%]",
"empty_battery": "[%key:component::husqvarna_automower::entity::sensor::error::state::empty_battery%]",
"error_at_power_up": "[%key:component::husqvarna_automower::entity::sensor::error::state::error_at_power_up%]",
"error": "[%key:common::state::error%]",
"fatal_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::fatal_error%]",
"folding_cutting_deck_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::folding_cutting_deck_sensor_defect%]",
"folding_sensor_activated": "[%key:component::husqvarna_automower::entity::sensor::error::state::folding_sensor_activated%]",
"geofence_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::geofence_problem%]",
"gps_navigation_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::gps_navigation_problem%]",
"guide_1_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_1_not_found%]",
"guide_2_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_2_not_found%]",
"guide_3_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_3_not_found%]",
"guide_calibration_accomplished": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_calibration_accomplished%]",
"guide_calibration_failed": "[%key:component::husqvarna_automower::entity::sensor::error::state::guide_calibration_failed%]",
"high_charging_power_loss": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_charging_power_loss%]",
"high_internal_power_loss": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_internal_power_loss%]",
"high_internal_temperature": "[%key:component::husqvarna_automower::entity::sensor::error::state::high_internal_temperature%]",
"internal_voltage_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::internal_voltage_error%]",
"invalid_battery_combination_invalid_combination_of_different_battery_types": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_battery_combination_invalid_combination_of_different_battery_types%]",
"invalid_sub_device_combination": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_sub_device_combination%]",
"invalid_system_configuration": "[%key:component::husqvarna_automower::entity::sensor::error::state::invalid_system_configuration%]",
"left_brush_motor_overloaded": "[%key:component::husqvarna_automower::entity::sensor::error::state::left_brush_motor_overloaded%]",
"lift_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::lift_sensor_defect%]",
"lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::lifted%]",
"limited_cutting_height_range": "[%key:component::husqvarna_automower::entity::sensor::error::state::limited_cutting_height_range%]",
"loop_sensor_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_defect%]",
"loop_sensor_problem_front": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_front%]",
"loop_sensor_problem_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_left%]",
"loop_sensor_problem_rear": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_rear%]",
"loop_sensor_problem_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::loop_sensor_problem_right%]",
"low_battery": "[%key:component::husqvarna_automower::entity::sensor::error::state::low_battery%]",
"memory_circuit_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::memory_circuit_problem%]",
"mower_lifted": "[%key:component::husqvarna_automower::entity::sensor::error::state::mower_lifted%]",
"mower_tilted": "[%key:component::husqvarna_automower::entity::sensor::error::state::mower_tilted%]",
"no_accurate_position_from_satellites": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_accurate_position_from_satellites%]",
"no_confirmed_position": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_confirmed_position%]",
"no_drive": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_drive%]",
"no_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_error%]",
"no_loop_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_loop_signal%]",
"no_power_in_charging_station": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_power_in_charging_station%]",
"no_response_from_charger": "[%key:component::husqvarna_automower::entity::sensor::error::state::no_response_from_charger%]",
"off": "[%key:common::state::off%]",
"outside_working_area": "[%key:component::husqvarna_automower::entity::sensor::error::state::outside_working_area%]",
"poor_signal_quality": "[%key:component::husqvarna_automower::entity::sensor::error::state::poor_signal_quality%]",
"reference_station_communication_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::reference_station_communication_problem%]",
"right_brush_motor_overloaded": "[%key:component::husqvarna_automower::entity::sensor::error::state::right_brush_motor_overloaded%]",
"safety_function_faulty": "[%key:component::husqvarna_automower::entity::sensor::error::state::safety_function_faulty%]",
"settings_restored": "[%key:component::husqvarna_automower::entity::sensor::error::state::settings_restored%]",
"sim_card_locked": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_locked%]",
"sim_card_not_found": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_not_found%]",
"sim_card_requires_pin": "[%key:component::husqvarna_automower::entity::sensor::error::state::sim_card_requires_pin%]",
"slipped_mower_has_slipped_situation_not_solved_with_moving_pattern": "[%key:component::husqvarna_automower::entity::sensor::error::state::slipped_mower_has_slipped_situation_not_solved_with_moving_pattern%]",
"slope_too_steep": "[%key:component::husqvarna_automower::entity::sensor::error::state::slope_too_steep%]",
"sms_could_not_be_sent": "[%key:component::husqvarna_automower::entity::sensor::error::state::sms_could_not_be_sent%]",
"stop_button_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::stop_button_problem%]",
"stopped": "[%key:common::state::stopped%]",
"stuck_in_charging_station": "[%key:component::husqvarna_automower::entity::sensor::error::state::stuck_in_charging_station%]",
"switch_cord_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::switch_cord_problem%]",
"temporary_battery_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::temporary_battery_problem%]",
"tilt_sensor_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::tilt_sensor_problem%]",
"too_high_discharge_current": "[%key:component::husqvarna_automower::entity::sensor::error::state::too_high_discharge_current%]",
"too_high_internal_current": "[%key:component::husqvarna_automower::entity::sensor::error::state::too_high_internal_current%]",
"trapped": "[%key:component::husqvarna_automower::entity::sensor::error::state::trapped%]",
"ultrasonic_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_problem%]",
"ultrasonic_sensor_1_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_1_defect%]",
"ultrasonic_sensor_2_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_2_defect%]",
"ultrasonic_sensor_3_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_3_defect%]",
"ultrasonic_sensor_4_defect": "[%key:component::husqvarna_automower::entity::sensor::error::state::ultrasonic_sensor_4_defect%]",
"unexpected_cutting_height_adj": "[%key:component::husqvarna_automower::entity::sensor::error::state::unexpected_cutting_height_adj%]",
"unexpected_error": "[%key:component::husqvarna_automower::entity::sensor::error::state::unexpected_error%]",
"upside_down": "[%key:component::husqvarna_automower::entity::sensor::error::state::upside_down%]",
"wait_power_up": "[%key:component::husqvarna_automower::entity::sensor::error::state::wait_power_up%]",
"wait_updating": "[%key:component::husqvarna_automower::entity::sensor::error::state::wait_updating%]",
"weak_gps_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::weak_gps_signal%]",
"wheel_drive_problem_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_left%]",
"wheel_drive_problem_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_rear_left%]",
"wheel_drive_problem_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_rear_right%]",
"wheel_drive_problem_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_drive_problem_right%]",
"wheel_motor_blocked_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_left%]",
"wheel_motor_blocked_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_rear_left%]",
"wheel_motor_blocked_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_rear_right%]",
"wheel_motor_blocked_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_blocked_right%]",
"wheel_motor_overloaded_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_left%]",
"wheel_motor_overloaded_rear_left": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_rear_left%]",
"wheel_motor_overloaded_rear_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_rear_right%]",
"wheel_motor_overloaded_right": "[%key:component::husqvarna_automower::entity::sensor::error::state::wheel_motor_overloaded_right%]",
"work_area_not_valid": "[%key:component::husqvarna_automower::entity::sensor::error::state::work_area_not_valid%]",
"wrong_loop_signal": "[%key:component::husqvarna_automower::entity::sensor::error::state::wrong_loop_signal%]",
"wrong_pin_code": "[%key:component::husqvarna_automower::entity::sensor::error::state::wrong_pin_code%]",
"zone_generator_problem": "[%key:component::husqvarna_automower::entity::sensor::error::state::zone_generator_problem%]"
}
},
"severity": {
"state": {
"fatal": "Fatal",
"error": "[%key:common::state::error%]",
"warning": "Warning",
"info": "Info",
"debug": "Debug",
"sw": "Software",
"unknown": "Unknown"
}
}
}
}
},
"number": {

View File

@@ -12,5 +12,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
"iot_class": "local_polling",
"requirements": ["automower-ble==0.2.7"]
"requirements": ["automower-ble==0.2.1"]
}

View File

@@ -4,7 +4,7 @@ from homeassistant.const import Platform
DOMAIN = "huum"
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT, Platform.NUMBER]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE, Platform.LIGHT]
CONFIG_STEAMER = 1
CONFIG_LIGHT = 2

View File

@@ -1,13 +0,0 @@
{
"entity": {
"number": {
"humidity": {
"default": "mdi:water",
"range": {
"0": "mdi:water-off",
"1": "mdi:water"
}
}
}
}
}

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