mirror of
https://github.com/home-assistant/core.git
synced 2026-04-21 00:49:54 +02:00
Compare commits
2 Commits
edenhaus-e
...
area-motio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
554c6e0cca | ||
|
|
7bac640267 |
4
.github/workflows/builder.yml
vendored
4
.github/workflows/builder.yml
vendored
@@ -509,7 +509,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
@@ -522,7 +522,7 @@ jobs:
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
||||
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
|
||||
42
.github/workflows/ci.yaml
vendored
42
.github/workflows/ci.yaml
vendored
@@ -653,7 +653,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@v4.7.1
|
||||
uses: actions/dependency-review-action@v4.7.0
|
||||
with:
|
||||
license-check: false # We use our own license audit checks
|
||||
|
||||
@@ -944,8 +944,7 @@ jobs:
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libgammu-dev \
|
||||
libxml2-utils
|
||||
libgammu-dev
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -1021,12 +1020,6 @@ jobs:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Beautify test results
|
||||
# For easier identification of parsing errors
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
run: |
|
||||
xmllint --format "junit.xml" > "junit.xml-tmp"
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
@@ -1077,8 +1070,7 @@ jobs:
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libmariadb-dev-compat \
|
||||
libxml2-utils
|
||||
libmariadb-dev-compat
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -1162,12 +1154,6 @@ jobs:
|
||||
steps.pytest-partial.outputs.mariadb }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Beautify test results
|
||||
# For easier identification of parsing errors
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
run: |
|
||||
xmllint --format "junit.xml" > "junit.xml-tmp"
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
@@ -1216,8 +1202,7 @@ jobs:
|
||||
sudo apt-get -y install \
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libxml2-utils
|
||||
libturbojpeg
|
||||
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
|
||||
sudo apt-get -y install \
|
||||
postgresql-server-dev-14
|
||||
@@ -1305,12 +1290,6 @@ jobs:
|
||||
steps.pytest-partial.outputs.postgresql }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Beautify test results
|
||||
# For easier identification of parsing errors
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
run: |
|
||||
xmllint --format "junit.xml" > "junit.xml-tmp"
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
@@ -1341,7 +1320,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'true'
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
flags: full-suite
|
||||
@@ -1378,8 +1357,7 @@ jobs:
|
||||
bluez \
|
||||
ffmpeg \
|
||||
libturbojpeg \
|
||||
libgammu-dev \
|
||||
libxml2-utils
|
||||
libgammu-dev
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
@@ -1458,12 +1436,6 @@ jobs:
|
||||
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
|
||||
path: coverage.xml
|
||||
overwrite: true
|
||||
- name: Beautify test results
|
||||
# For easier identification of parsing errors
|
||||
if: needs.info.outputs.skip_coverage != 'true'
|
||||
run: |
|
||||
xmllint --format "junit.xml" > "junit.xml-tmp"
|
||||
mv "junit.xml-tmp" "junit.xml"
|
||||
- name: Upload test results artifact
|
||||
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
|
||||
uses: actions/upload-artifact@v4.6.2
|
||||
@@ -1491,7 +1463,7 @@ jobs:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
uses: codecov/codecov-action@v5.4.2
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.18
|
||||
uses: github/codeql-action/init@v3.28.17
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.18
|
||||
uses: github/codeql-action/analyze@v3.28.17
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -270,7 +270,6 @@ homeassistant.components.image_processing.*
|
||||
homeassistant.components.image_upload.*
|
||||
homeassistant.components.imap.*
|
||||
homeassistant.components.imgw_pib.*
|
||||
homeassistant.components.immich.*
|
||||
homeassistant.components.incomfort.*
|
||||
homeassistant.components.input_button.*
|
||||
homeassistant.components.input_select.*
|
||||
|
||||
6
CODEOWNERS
generated
6
CODEOWNERS
generated
@@ -710,8 +710,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/imeon_inverter/ @Imeon-Energy
|
||||
/homeassistant/components/imgw_pib/ @bieniu
|
||||
/tests/components/imgw_pib/ @bieniu
|
||||
/homeassistant/components/immich/ @mib1185
|
||||
/tests/components/immich/ @mib1185
|
||||
/homeassistant/components/improv_ble/ @emontnemery
|
||||
/tests/components/improv_ble/ @emontnemery
|
||||
/homeassistant/components/incomfort/ @jbouwh
|
||||
@@ -1486,8 +1484,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/subaru/ @G-Two
|
||||
/homeassistant/components/suez_water/ @ooii @jb101010-2
|
||||
/tests/components/suez_water/ @ooii @jb101010-2
|
||||
/homeassistant/components/sun/ @home-assistant/core
|
||||
/tests/components/sun/ @home-assistant/core
|
||||
/homeassistant/components/sun/ @Swamp-Ig
|
||||
/tests/components/sun/ @Swamp-Ig
|
||||
/homeassistant/components/supla/ @mwegrzynek
|
||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Any, Concatenate
|
||||
from airgradient import AirGradientConnectionError, AirGradientError, get_model_name
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -30,7 +29,6 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
|
||||
model_id=measures.model,
|
||||
serial_number=coordinator.serial_number,
|
||||
sw_version=measures.firmware_version,
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, coordinator.serial_number)},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ from .const import (
|
||||
CONF_VIDEO_SOURCE,
|
||||
DEFAULT_STREAM_PROFILE,
|
||||
DEFAULT_VIDEO_SOURCE,
|
||||
DOMAIN,
|
||||
DOMAIN as AXIS_DOMAIN,
|
||||
)
|
||||
from .errors import AuthenticationRequired, CannotConnect
|
||||
from .hub import AxisHub, get_axis_api
|
||||
@@ -58,7 +58,7 @@ DEFAULT_PROTOCOL = "https"
|
||||
PROTOCOL_CHOICES = ["https", "http"]
|
||||
|
||||
|
||||
class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
|
||||
"""Handle a Axis config flow."""
|
||||
|
||||
VERSION = 3
|
||||
@@ -146,7 +146,7 @@ class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
model = self.config[CONF_MODEL]
|
||||
same_model = [
|
||||
entry.data[CONF_NAME]
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN)
|
||||
for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN)
|
||||
if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model
|
||||
]
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from .const import DOMAIN, EVSE_ID, LOGGER, MODEL_TYPE
|
||||
|
||||
type BlueCurrentConfigEntry = ConfigEntry[Connector]
|
||||
|
||||
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
CHARGE_POINTS = "CHARGE_POINTS"
|
||||
DATA = "data"
|
||||
DELAY = 5
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
"""Support for Blue Current buttons."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from bluecurrent_api.client import Client
|
||||
|
||||
from homeassistant.components.button import (
|
||||
ButtonDeviceClass,
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BlueCurrentConfigEntry, Connector
|
||||
from .entity import ChargepointEntity
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class ChargePointButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes a Blue Current button entity."""
|
||||
|
||||
function: Callable[[Client, str], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
CHARGE_POINT_BUTTONS = (
|
||||
ChargePointButtonEntityDescription(
|
||||
key="reset",
|
||||
translation_key="reset",
|
||||
function=lambda client, evse_id: client.reset(evse_id),
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
),
|
||||
ChargePointButtonEntityDescription(
|
||||
key="reboot",
|
||||
translation_key="reboot",
|
||||
function=lambda client, evse_id: client.reboot(evse_id),
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
),
|
||||
ChargePointButtonEntityDescription(
|
||||
key="stop_charge_session",
|
||||
translation_key="stop_charge_session",
|
||||
function=lambda client, evse_id: client.stop_session(evse_id),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: BlueCurrentConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Blue Current buttons."""
|
||||
connector: Connector = entry.runtime_data
|
||||
async_add_entities(
|
||||
ChargePointButton(
|
||||
connector,
|
||||
button,
|
||||
evse_id,
|
||||
)
|
||||
for evse_id in connector.charge_points
|
||||
for button in CHARGE_POINT_BUTTONS
|
||||
)
|
||||
|
||||
|
||||
class ChargePointButton(ChargepointEntity, ButtonEntity):
|
||||
"""Define a charge point button."""
|
||||
|
||||
has_value = True
|
||||
entity_description: ChargePointButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
connector: Connector,
|
||||
description: ChargePointButtonEntityDescription,
|
||||
evse_id: str,
|
||||
) -> None:
|
||||
"""Initialize the button."""
|
||||
super().__init__(connector, evse_id)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{description.key}_{evse_id}"
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Handle the button press."""
|
||||
await self.entity_description.function(self.connector.client, self.evse_id)
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Entity representing a Blue Current charge point."""
|
||||
|
||||
from abc import abstractmethod
|
||||
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -15,12 +17,12 @@ class BlueCurrentEntity(Entity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
has_value = False
|
||||
|
||||
def __init__(self, connector: Connector, signal: str) -> None:
|
||||
"""Initialize the entity."""
|
||||
self.connector = connector
|
||||
self.signal = signal
|
||||
self.has_value = False
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
@@ -41,6 +43,7 @@ class BlueCurrentEntity(Entity):
|
||||
return self.connector.connected and self.has_value
|
||||
|
||||
@callback
|
||||
@abstractmethod
|
||||
def update_from_latest_data(self) -> None:
|
||||
"""Update the entity from the latest data."""
|
||||
|
||||
|
||||
@@ -19,17 +19,6 @@
|
||||
"current_left": {
|
||||
"default": "mdi:gauge"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"reset": {
|
||||
"default": "mdi:restart"
|
||||
},
|
||||
"reboot": {
|
||||
"default": "mdi:restart-alert"
|
||||
},
|
||||
"stop_charge_session": {
|
||||
"default": "mdi:stop"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,17 +113,6 @@
|
||||
"grid_max_current": {
|
||||
"name": "Max grid current"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"stop_charge_session": {
|
||||
"name": "Stop charge session"
|
||||
},
|
||||
"reboot": {
|
||||
"name": "Reboot"
|
||||
},
|
||||
"reset": {
|
||||
"name": "Reset"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,13 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
|
||||
from .const import (
|
||||
CONF_GCID,
|
||||
CONF_READ_ONLY,
|
||||
CONF_REFRESH_TOKEN,
|
||||
DOMAIN as BMW_DOMAIN,
|
||||
SCAN_INTERVALS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -57,7 +63,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"{DOMAIN}-{config_entry.data[CONF_USERNAME]}",
|
||||
name=f"{BMW_DOMAIN}-{config_entry.data[CONF_USERNAME]}",
|
||||
update_interval=timedelta(
|
||||
seconds=SCAN_INTERVALS[config_entry.data[CONF_REGION]]
|
||||
),
|
||||
@@ -75,26 +81,26 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
except MyBMWCaptchaMissingError as err:
|
||||
# If a captcha is required (user/password login flow), always trigger the reauth flow
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_domain=BMW_DOMAIN,
|
||||
translation_key="missing_captcha",
|
||||
) from err
|
||||
except MyBMWAuthError as err:
|
||||
# Allow one retry interval before raising AuthFailed to avoid flaky API issues
|
||||
if self.last_update_success:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_domain=BMW_DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"exception": str(err)},
|
||||
) from err
|
||||
# Clear refresh token and trigger reauth if previous update failed as well
|
||||
self._update_config_entry_refresh_token(None)
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_domain=BMW_DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
) from err
|
||||
except (MyBMWAPIError, RequestError) as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_domain=BMW_DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"exception": str(err)},
|
||||
) from err
|
||||
|
||||
@@ -7,17 +7,15 @@ from ssl import SSLError
|
||||
from bosch_alarm_mode2 import Panel
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PORT, Platform
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
|
||||
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
@@ -54,11 +52,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
mac = entry.data.get(CONF_MAC)
|
||||
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
|
||||
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
|
||||
name=f"Bosch {panel.model}",
|
||||
manufacturer="Bosch Security Systems",
|
||||
|
||||
@@ -34,9 +34,6 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
|
||||
"""An alarm control panel entity for a bosch alarm panel."""
|
||||
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
"""Support for Bosch Alarm Panel binary sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bosch_alarm_mode2 import Panel
|
||||
from bosch_alarm_mode2.const import ALARM_PANEL_FAULTS
|
||||
|
||||
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 . import BoschAlarmConfigEntry
|
||||
from .entity import BoschAlarmAreaEntity, BoschAlarmEntity, BoschAlarmPointEntity
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class BoschAlarmFaultEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes Bosch Alarm sensor entity."""
|
||||
|
||||
fault: int
|
||||
|
||||
|
||||
FAULT_TYPES = [
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_battery_low",
|
||||
entity_registry_enabled_default=True,
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
fault=ALARM_PANEL_FAULTS.BATTERY_LOW,
|
||||
),
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_battery_mising",
|
||||
translation_key="panel_fault_battery_mising",
|
||||
entity_registry_enabled_default=True,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
fault=ALARM_PANEL_FAULTS.BATTERY_MISING,
|
||||
),
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_ac_fail",
|
||||
translation_key="panel_fault_ac_fail",
|
||||
entity_registry_enabled_default=True,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
fault=ALARM_PANEL_FAULTS.AC_FAIL,
|
||||
),
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_phone_line_failure",
|
||||
translation_key="panel_fault_phone_line_failure",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
fault=ALARM_PANEL_FAULTS.PHONE_LINE_FAILURE,
|
||||
),
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_parameter_crc_fail_in_pif",
|
||||
translation_key="panel_fault_parameter_crc_fail_in_pif",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
fault=ALARM_PANEL_FAULTS.PARAMETER_CRC_FAIL_IN_PIF,
|
||||
),
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_communication_fail_since_rps_hang_up",
|
||||
translation_key="panel_fault_communication_fail_since_rps_hang_up",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
fault=ALARM_PANEL_FAULTS.COMMUNICATION_FAIL_SINCE_RPS_HANG_UP,
|
||||
),
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_sdi_fail_since_rps_hang_up",
|
||||
translation_key="panel_fault_sdi_fail_since_rps_hang_up",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
fault=ALARM_PANEL_FAULTS.SDI_FAIL_SINCE_RPS_HANG_UP,
|
||||
),
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_user_code_tamper_since_rps_hang_up",
|
||||
translation_key="panel_fault_user_code_tamper_since_rps_hang_up",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
fault=ALARM_PANEL_FAULTS.USER_CODE_TAMPER_SINCE_RPS_HANG_UP,
|
||||
),
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_fail_to_call_rps_since_rps_hang_up",
|
||||
translation_key="panel_fault_fail_to_call_rps_since_rps_hang_up",
|
||||
entity_registry_enabled_default=False,
|
||||
fault=ALARM_PANEL_FAULTS.FAIL_TO_CALL_RPS_SINCE_RPS_HANG_UP,
|
||||
),
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_point_bus_fail_since_rps_hang_up",
|
||||
translation_key="panel_fault_point_bus_fail_since_rps_hang_up",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
fault=ALARM_PANEL_FAULTS.POINT_BUS_FAIL_SINCE_RPS_HANG_UP,
|
||||
),
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_log_overflow",
|
||||
translation_key="panel_fault_log_overflow",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
fault=ALARM_PANEL_FAULTS.LOG_OVERFLOW,
|
||||
),
|
||||
BoschAlarmFaultEntityDescription(
|
||||
key="panel_fault_log_threshold",
|
||||
translation_key="panel_fault_log_threshold",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
fault=ALARM_PANEL_FAULTS.LOG_THRESHOLD,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: BoschAlarmConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up binary sensors for alarm points and the connection status."""
|
||||
panel = config_entry.runtime_data
|
||||
|
||||
entities: list[BinarySensorEntity] = [
|
||||
PointSensor(panel, point_id, config_entry.unique_id or config_entry.entry_id)
|
||||
for point_id in panel.points
|
||||
]
|
||||
|
||||
entities.extend(
|
||||
PanelFaultsSensor(
|
||||
panel,
|
||||
config_entry.unique_id or config_entry.entry_id,
|
||||
fault_type,
|
||||
)
|
||||
for fault_type in FAULT_TYPES
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
AreaReadyToArmSensor(
|
||||
panel, area_id, config_entry.unique_id or config_entry.entry_id, "away"
|
||||
)
|
||||
for area_id in panel.areas
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
AreaReadyToArmSensor(
|
||||
panel, area_id, config_entry.unique_id or config_entry.entry_id, "home"
|
||||
)
|
||||
for area_id in panel.areas
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
class PanelFaultsSensor(BoschAlarmEntity, BinarySensorEntity):
|
||||
"""A binary sensor entity for each fault type in a bosch alarm panel."""
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
entity_description: BoschAlarmFaultEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
panel: Panel,
|
||||
unique_id: str,
|
||||
entity_description: BoschAlarmFaultEntityDescription,
|
||||
) -> None:
|
||||
"""Set up a binary sensor entity for each fault type in a bosch alarm panel."""
|
||||
super().__init__(panel, unique_id, True)
|
||||
self.entity_description = entity_description
|
||||
self._fault_type = entity_description.fault
|
||||
self._attr_unique_id = f"{unique_id}_fault_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return if this fault has occurred."""
|
||||
return self._fault_type in self.panel.panel_faults_ids
|
||||
|
||||
|
||||
class AreaReadyToArmSensor(BoschAlarmAreaEntity, BinarySensorEntity):
|
||||
"""A binary sensor entity showing if a panel is ready to arm."""
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
def __init__(
|
||||
self, panel: Panel, area_id: int, unique_id: str, arm_type: str
|
||||
) -> None:
|
||||
"""Set up a binary sensor entity for the arming status in a bosch alarm panel."""
|
||||
super().__init__(panel, area_id, unique_id, False, False, True)
|
||||
self.panel = panel
|
||||
self._arm_type = arm_type
|
||||
self._attr_translation_key = f"area_ready_to_arm_{arm_type}"
|
||||
self._attr_unique_id = f"{self._area_unique_id}_ready_to_arm_{arm_type}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return if this panel is ready to arm."""
|
||||
if self._arm_type == "away":
|
||||
return self._area.all_ready
|
||||
if self._arm_type == "home":
|
||||
return self._area.all_ready or self._area.part_ready
|
||||
return False
|
||||
|
||||
|
||||
class PointSensor(BoschAlarmPointEntity, BinarySensorEntity):
|
||||
"""A binary sensor entity for a point in a bosch alarm panel."""
|
||||
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
|
||||
"""Set up a binary sensor entity for a point in a bosch alarm panel."""
|
||||
super().__init__(panel, point_id, unique_id)
|
||||
self._attr_unique_id = self._point_unique_id
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return if this point sensor is on."""
|
||||
return self._point.is_open()
|
||||
@@ -6,13 +6,12 @@ import asyncio
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
import ssl
|
||||
from typing import Any, Self
|
||||
from typing import Any
|
||||
|
||||
from bosch_alarm_mode2 import Panel
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_DHCP,
|
||||
SOURCE_RECONFIGURE,
|
||||
SOURCE_USER,
|
||||
ConfigFlow,
|
||||
@@ -21,14 +20,11 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import (
|
||||
CONF_CODE,
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_MODEL,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
)
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
|
||||
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
|
||||
|
||||
@@ -92,12 +88,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Init config flow."""
|
||||
|
||||
self._data: dict[str, Any] = {}
|
||||
self.mac: str | None = None
|
||||
self.host: str | None = None
|
||||
|
||||
def is_matching(self, other_flow: Self) -> bool:
|
||||
"""Return True if other_flow is matching this flow."""
|
||||
return self.mac == other_flow.mac or self.host == other_flow.host
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -106,12 +96,9 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
self.host = user_input[CONF_HOST]
|
||||
if self.source == SOURCE_USER:
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
try:
|
||||
# Use load_selector = 0 to fetch the panel model without authentication.
|
||||
(model, _) = await try_connect(user_input, 0)
|
||||
(model, serial) = await try_connect(user_input, 0)
|
||||
except (
|
||||
OSError,
|
||||
ConnectionRefusedError,
|
||||
@@ -142,55 +129,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_dhcp(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle DHCP discovery."""
|
||||
self.mac = format_mac(discovery_info.macaddress)
|
||||
self.host = discovery_info.ip
|
||||
if self.hass.config_entries.flow.async_has_matching_flow(self):
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
|
||||
for entry in self.hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.data[CONF_MAC] == self.mac:
|
||||
result = self.hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={
|
||||
**entry.data,
|
||||
CONF_HOST: discovery_info.ip,
|
||||
},
|
||||
)
|
||||
if result:
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
return self.async_abort(reason="already_configured")
|
||||
try:
|
||||
# Use load_selector = 0 to fetch the panel model without authentication.
|
||||
(model, _) = await try_connect(
|
||||
{CONF_HOST: discovery_info.ip, CONF_PORT: 7700}, 0
|
||||
)
|
||||
except (
|
||||
OSError,
|
||||
ConnectionRefusedError,
|
||||
ssl.SSLError,
|
||||
asyncio.exceptions.TimeoutError,
|
||||
):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
return self.async_abort(reason="unknown")
|
||||
self.context["title_placeholders"] = {
|
||||
"model": model,
|
||||
"host": discovery_info.ip,
|
||||
}
|
||||
self._data = {
|
||||
CONF_HOST: discovery_info.ip,
|
||||
CONF_MAC: self.mac,
|
||||
CONF_MODEL: model,
|
||||
CONF_PORT: 7700,
|
||||
}
|
||||
|
||||
return await self.async_step_auth()
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -234,7 +172,7 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
if serial_number:
|
||||
await self.async_set_unique_id(str(serial_number))
|
||||
if self.source in (SOURCE_USER, SOURCE_DHCP):
|
||||
if self.source == SOURCE_USER:
|
||||
if serial_number:
|
||||
self._abort_if_unique_id_configured()
|
||||
else:
|
||||
@@ -246,7 +184,6 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
if serial_number:
|
||||
self._abort_if_unique_id_mismatch(reason="device_mismatch")
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data=self._data,
|
||||
|
||||
@@ -17,13 +17,9 @@ class BoschAlarmEntity(Entity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, panel: Panel, unique_id: str, observe_faults: bool = False
|
||||
) -> None:
|
||||
def __init__(self, panel: Panel, unique_id: str) -> None:
|
||||
"""Set up a entity for a bosch alarm panel."""
|
||||
self.panel = panel
|
||||
self._observe_faults = observe_faults
|
||||
self._attr_should_poll = False
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
name=f"Bosch {panel.model}",
|
||||
@@ -38,14 +34,10 @@ class BoschAlarmEntity(Entity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Observe state changes."""
|
||||
self.panel.connection_status_observer.attach(self.schedule_update_ha_state)
|
||||
if self._observe_faults:
|
||||
self.panel.faults_observer.attach(self.schedule_update_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Stop observing state changes."""
|
||||
self.panel.connection_status_observer.detach(self.schedule_update_ha_state)
|
||||
if self._observe_faults:
|
||||
self.panel.faults_observer.attach(self.schedule_update_ha_state)
|
||||
|
||||
|
||||
class BoschAlarmAreaEntity(BoschAlarmEntity):
|
||||
@@ -96,33 +88,6 @@ class BoschAlarmAreaEntity(BoschAlarmEntity):
|
||||
self._area.status_observer.detach(self.schedule_update_ha_state)
|
||||
|
||||
|
||||
class BoschAlarmPointEntity(BoschAlarmEntity):
|
||||
"""A base entity for point related entities within a bosch alarm panel."""
|
||||
|
||||
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
|
||||
"""Set up a area related entity for a bosch alarm panel."""
|
||||
super().__init__(panel, unique_id)
|
||||
self._point_id = point_id
|
||||
self._point_unique_id = f"{unique_id}_point_{point_id}"
|
||||
self._point = panel.points[point_id]
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._point_unique_id)},
|
||||
name=self._point.name,
|
||||
manufacturer="Bosch Security Systems",
|
||||
via_device=(DOMAIN, unique_id),
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Observe state changes."""
|
||||
await super().async_added_to_hass()
|
||||
self._point.status_observer.attach(self.schedule_update_ha_state)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Stop observing state changes."""
|
||||
await super().async_added_to_hass()
|
||||
self._point.status_observer.detach(self.schedule_update_ha_state)
|
||||
|
||||
|
||||
class BoschAlarmDoorEntity(BoschAlarmEntity):
|
||||
"""A base entity for area related entities within a bosch alarm panel."""
|
||||
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"alarms_gas": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"alarms_fire": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"alarms_burglary": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"faulting_points": {
|
||||
"default": "mdi:alert-circle"
|
||||
}
|
||||
@@ -33,44 +24,6 @@
|
||||
"on": "mdi:lock-open"
|
||||
}
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"panel_fault_parameter_crc_fail_in_pif": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"panel_fault_phone_line_failure": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"panel_fault_sdi_fail_since_rps_hang_up": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"panel_fault_user_code_tamper_since_rps_hang_up": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"panel_fault_fail_to_call_rps_since_rps_hang_up": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"panel_fault_point_bus_fail_since_rps_hang_up": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"panel_fault_log_overflow": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"panel_fault_log_threshold": {
|
||||
"default": "mdi:alert-circle"
|
||||
},
|
||||
"area_ready_to_arm_away": {
|
||||
"default": "mdi:shield",
|
||||
"state": {
|
||||
"on": "mdi:shield-lock"
|
||||
}
|
||||
},
|
||||
"area_ready_to_arm_home": {
|
||||
"default": "mdi:shield",
|
||||
"state": {
|
||||
"on": "mdi:shield-home"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,6 @@
|
||||
"name": "Bosch Alarm",
|
||||
"codeowners": ["@mag1024", "@sanjay900"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
"macaddress": "000463*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bosch_alarm",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -39,15 +39,15 @@ rules:
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
|
||||
@@ -6,7 +6,6 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bosch_alarm_mode2 import Panel
|
||||
from bosch_alarm_mode2.const import ALARM_MEMORY_PRIORITIES
|
||||
from bosch_alarm_mode2.panel import Area
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
@@ -16,53 +15,18 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from . import BoschAlarmConfigEntry
|
||||
from .entity import BoschAlarmAreaEntity
|
||||
|
||||
ALARM_TYPES = {
|
||||
"burglary": {
|
||||
ALARM_MEMORY_PRIORITIES.BURGLARY_SUPERVISORY: "supervisory",
|
||||
ALARM_MEMORY_PRIORITIES.BURGLARY_TROUBLE: "trouble",
|
||||
ALARM_MEMORY_PRIORITIES.BURGLARY_ALARM: "alarm",
|
||||
},
|
||||
"gas": {
|
||||
ALARM_MEMORY_PRIORITIES.GAS_SUPERVISORY: "supervisory",
|
||||
ALARM_MEMORY_PRIORITIES.GAS_TROUBLE: "trouble",
|
||||
ALARM_MEMORY_PRIORITIES.GAS_ALARM: "alarm",
|
||||
},
|
||||
"fire": {
|
||||
ALARM_MEMORY_PRIORITIES.FIRE_SUPERVISORY: "supervisory",
|
||||
ALARM_MEMORY_PRIORITIES.FIRE_TROUBLE: "trouble",
|
||||
ALARM_MEMORY_PRIORITIES.FIRE_ALARM: "alarm",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class BoschAlarmSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Bosch Alarm sensor entity."""
|
||||
|
||||
value_fn: Callable[[Area], str | int]
|
||||
value_fn: Callable[[Area], int]
|
||||
observe_alarms: bool = False
|
||||
observe_ready: bool = False
|
||||
observe_status: bool = False
|
||||
|
||||
|
||||
def priority_value_fn(priority_info: dict[int, str]) -> Callable[[Area], str]:
|
||||
"""Build a value_fn for a given priority type."""
|
||||
return lambda area: next(
|
||||
(key for priority, key in priority_info.items() if priority in area.alarms_ids),
|
||||
"no_issues",
|
||||
)
|
||||
|
||||
|
||||
SENSOR_TYPES: list[BoschAlarmSensorEntityDescription] = [
|
||||
*[
|
||||
BoschAlarmSensorEntityDescription(
|
||||
key=f"alarms_{key}",
|
||||
translation_key=f"alarms_{key}",
|
||||
value_fn=priority_value_fn(priority_type),
|
||||
observe_alarms=True,
|
||||
)
|
||||
for key, priority_type in ALARM_TYPES.items()
|
||||
],
|
||||
BoschAlarmSensorEntityDescription(
|
||||
key="faulting_points",
|
||||
translation_key="faulting_points",
|
||||
@@ -117,6 +81,6 @@ class BoschAreaSensor(BoschAlarmAreaEntity, SensorEntity):
|
||||
self._attr_unique_id = f"{self._area_unique_id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | int:
|
||||
def native_value(self) -> int:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self._area)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{model} ({host})",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
@@ -43,7 +42,6 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
@@ -58,95 +56,22 @@
|
||||
"message": "Incorrect credentials for panel."
|
||||
},
|
||||
"incorrect_door_state": {
|
||||
"message": "Door cannot be manipulated while it is momentarily unlocked."
|
||||
"message": "Door cannot be manipulated while it is being cycled."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"panel_fault_battery_mising": {
|
||||
"name": "Battery missing"
|
||||
},
|
||||
"panel_fault_ac_fail": {
|
||||
"name": "AC Failure"
|
||||
},
|
||||
"panel_fault_parameter_crc_fail_in_pif": {
|
||||
"name": "CRC failure in panel configuration"
|
||||
},
|
||||
"panel_fault_phone_line_failure": {
|
||||
"name": "Phone line failure"
|
||||
},
|
||||
"panel_fault_sdi_fail_since_rps_hang_up": {
|
||||
"name": "SDI failure since RPS hang up"
|
||||
},
|
||||
"panel_fault_user_code_tamper_since_rps_hang_up": {
|
||||
"name": "User code tamper since RPS hang up"
|
||||
},
|
||||
"panel_fault_fail_to_call_rps_since_rps_hang_up": {
|
||||
"name": "Failure to call RPS since RPS hang up"
|
||||
},
|
||||
"panel_fault_point_bus_fail_since_rps_hang_up": {
|
||||
"name": "Point bus failure since RPS hang up"
|
||||
},
|
||||
"panel_fault_log_overflow": {
|
||||
"name": "Log overflow"
|
||||
},
|
||||
"panel_fault_log_threshold": {
|
||||
"name": "Log threshold reached"
|
||||
},
|
||||
"area_ready_to_arm_away": {
|
||||
"name": "Area ready to arm away",
|
||||
"state": {
|
||||
"on": "Ready",
|
||||
"off": "Not ready"
|
||||
}
|
||||
},
|
||||
"area_ready_to_arm_home": {
|
||||
"name": "Area ready to arm home",
|
||||
"state": {
|
||||
"on": "Ready",
|
||||
"off": "Not ready"
|
||||
}
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"secured": {
|
||||
"name": "Secured"
|
||||
},
|
||||
"cycling": {
|
||||
"name": "Momentarily unlocked"
|
||||
"name": "Cycling"
|
||||
},
|
||||
"locked": {
|
||||
"name": "Locked"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"alarms_gas": {
|
||||
"name": "Gas alarm issues",
|
||||
"state": {
|
||||
"supervisory": "Supervisory",
|
||||
"trouble": "Trouble",
|
||||
"alarm": "Alarm",
|
||||
"no_issues": "No issues"
|
||||
}
|
||||
},
|
||||
"alarms_fire": {
|
||||
"name": "Fire alarm issues",
|
||||
"state": {
|
||||
"supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]",
|
||||
"trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]",
|
||||
"alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]",
|
||||
"no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]"
|
||||
}
|
||||
},
|
||||
"alarms_burglary": {
|
||||
"name": "Burglary alarm issues",
|
||||
"state": {
|
||||
"supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]",
|
||||
"trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]",
|
||||
"alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]",
|
||||
"no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]"
|
||||
}
|
||||
},
|
||||
"faulting_points": {
|
||||
"name": "Faulting points",
|
||||
"unit_of_measurement": "points"
|
||||
|
||||
@@ -60,7 +60,7 @@ from .const import (
|
||||
ADDED_CAST_DEVICES_KEY,
|
||||
CAST_MULTIZONE_MANAGER_KEY,
|
||||
CONF_IGNORE_CEC,
|
||||
DOMAIN,
|
||||
DOMAIN as CAST_DOMAIN,
|
||||
SIGNAL_CAST_DISCOVERED,
|
||||
SIGNAL_CAST_REMOVED,
|
||||
SIGNAL_HASS_CAST_SHOW_VIEW,
|
||||
@@ -315,7 +315,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
self._cast_view_remove_handler: CALLBACK_TYPE | None = None
|
||||
self._attr_unique_id = str(cast_info.uuid)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, str(cast_info.uuid).replace("-", ""))},
|
||||
identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))},
|
||||
manufacturer=str(cast_info.cast_info.manufacturer),
|
||||
model=cast_info.cast_info.model_name,
|
||||
name=str(cast_info.friendly_name),
|
||||
@@ -591,7 +591,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
"""Generate root node."""
|
||||
children = []
|
||||
# Add media browsers
|
||||
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
|
||||
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
|
||||
children.extend(
|
||||
await platform.async_get_media_browser_root_object(
|
||||
self.hass, self._chromecast.cast_type
|
||||
@@ -650,7 +650,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
|
||||
platform: CastProtocol
|
||||
assert media_content_type is not None
|
||||
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
|
||||
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
|
||||
browse_media = await platform.async_browse_media(
|
||||
self.hass,
|
||||
media_content_type,
|
||||
@@ -680,7 +680,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
|
||||
|
||||
# Handle media supported by a known cast app
|
||||
if media_type == DOMAIN:
|
||||
if media_type == CAST_DOMAIN:
|
||||
try:
|
||||
app_data = json.loads(media_id)
|
||||
if metadata := extra.get("metadata"):
|
||||
@@ -712,7 +712,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
return
|
||||
|
||||
# Try the cast platforms
|
||||
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
|
||||
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
|
||||
result = await platform.async_play_media(
|
||||
self.hass, self.entity_id, chromecast, media_type, media_id
|
||||
)
|
||||
|
||||
@@ -43,7 +43,7 @@ from homeassistant.util.dt import utcnow
|
||||
from .const import (
|
||||
CONF_ENTITY_CONFIG,
|
||||
CONF_FILTER,
|
||||
DOMAIN,
|
||||
DOMAIN as CLOUD_DOMAIN,
|
||||
PREF_ALEXA_REPORT_STATE,
|
||||
PREF_ENABLE_ALEXA,
|
||||
PREF_SHOULD_EXPOSE,
|
||||
@@ -55,7 +55,7 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CLOUD_ALEXA = f"{DOMAIN}.{ALEXA_DOMAIN}"
|
||||
CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}"
|
||||
|
||||
# Time to wait when entity preferences have changed before syncing it to
|
||||
# the cloud.
|
||||
|
||||
@@ -41,7 +41,7 @@ from .const import (
|
||||
CONF_ENTITY_CONFIG,
|
||||
CONF_FILTER,
|
||||
DEFAULT_DISABLE_2FA,
|
||||
DOMAIN,
|
||||
DOMAIN as CLOUD_DOMAIN,
|
||||
PREF_DISABLE_2FA,
|
||||
PREF_SHOULD_EXPOSE,
|
||||
)
|
||||
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CLOUD_GOOGLE = f"{DOMAIN}.{GOOGLE_DOMAIN}"
|
||||
CLOUD_GOOGLE = f"{CLOUD_DOMAIN}.{GOOGLE_DOMAIN}"
|
||||
|
||||
|
||||
SUPPORTED_DOMAINS = {
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.101.0"],
|
||||
"requirements": ["hass-nabucasa==0.100.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -134,9 +134,11 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
|
||||
self._attr_current_temperature = values[0] / 10
|
||||
|
||||
self._attr_hvac_action = None
|
||||
if _mode == ClimaComelitMode.OFF:
|
||||
self._attr_hvac_action = HVACAction.OFF
|
||||
if not _active:
|
||||
self._attr_hvac_action = HVACAction.IDLE
|
||||
elif _mode in API_STATUS:
|
||||
if _mode in API_STATUS:
|
||||
self._attr_hvac_action = API_STATUS[_mode]["hvac_action"]
|
||||
|
||||
self._attr_hvac_mode = None
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any, cast
|
||||
from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -68,10 +68,16 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
def is_closed(self) -> bool | None:
|
||||
"""Return if the cover is closed."""
|
||||
|
||||
if self._last_state in [None, "unknown"]:
|
||||
return None
|
||||
|
||||
if self.device_status != STATE_COVER.index("stopped"):
|
||||
return False
|
||||
|
||||
if self._last_action:
|
||||
return self._last_action == STATE_COVER.index("closing")
|
||||
|
||||
return None
|
||||
return self._last_state == CoverState.CLOSED
|
||||
|
||||
@property
|
||||
def is_closing(self) -> bool:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiocomelit==0.12.3"]
|
||||
"requirements": ["aiocomelit==0.12.1"]
|
||||
}
|
||||
|
||||
@@ -55,8 +55,10 @@ rules:
|
||||
docs-known-limitations:
|
||||
status: exempt
|
||||
comment: no known limitations, yet
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-supported-devices:
|
||||
status: todo
|
||||
comment: review and complete missing ones
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
|
||||
@@ -44,6 +44,7 @@ def websocket_list_areas(
|
||||
vol.Optional("humidity_entity_id"): vol.Any(str, None),
|
||||
vol.Optional("icon"): str,
|
||||
vol.Optional("labels"): [str],
|
||||
vol.Optional("motion_entity_id"): vol.Any(str, None),
|
||||
vol.Required("name"): str,
|
||||
vol.Optional("picture"): vol.Any(str, None),
|
||||
vol.Optional("temperature_entity_id"): vol.Any(str, None),
|
||||
@@ -112,6 +113,7 @@ def websocket_delete_area(
|
||||
vol.Optional("humidity_entity_id"): vol.Any(str, None),
|
||||
vol.Optional("icon"): vol.Any(str, None),
|
||||
vol.Optional("labels"): [str],
|
||||
vol.Optional("motion_entity_id"): vol.Any(str, None),
|
||||
vol.Optional("name"): str,
|
||||
vol.Optional("picture"): vol.Any(str, None),
|
||||
vol.Optional("temperature_entity_id"): vol.Any(str, None),
|
||||
|
||||
@@ -17,7 +17,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from ..const import CONF_MASTER_GATEWAY, DOMAIN, HASSIO_CONFIGURATION_URL, PLATFORMS
|
||||
from ..const import (
|
||||
CONF_MASTER_GATEWAY,
|
||||
DOMAIN as DECONZ_DOMAIN,
|
||||
HASSIO_CONFIGURATION_URL,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .config import DeconzConfig
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -188,7 +193,7 @@ class DeconzHub:
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
configuration_url=configuration_url,
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, self.api.config.bridge_id)},
|
||||
identifiers={(DECONZ_DOMAIN, self.api.config.bridge_id)},
|
||||
manufacturer="Dresden Elektronik",
|
||||
model=self.api.config.model_id,
|
||||
name=self.api.config.name,
|
||||
|
||||
@@ -6,16 +6,12 @@ from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseMedia,
|
||||
MediaClass,
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
MediaType,
|
||||
RepeatMode,
|
||||
SearchMedia,
|
||||
SearchMediaQuery,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -411,18 +407,3 @@ class DemoSearchPlayer(AbstractDemoPlayer):
|
||||
"""A Demo media player that supports searching."""
|
||||
|
||||
_attr_supported_features = SEARCH_PLAYER_SUPPORT
|
||||
|
||||
async def async_search_media(self, query: SearchMediaQuery) -> SearchMedia:
|
||||
"""Demo implementation of search media."""
|
||||
return SearchMedia(
|
||||
result=[
|
||||
BrowseMedia(
|
||||
title="Search result",
|
||||
media_class=MediaClass.MOVIE,
|
||||
media_content_type=MediaType.MOVIE,
|
||||
media_content_id="search_result_id",
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/denonavr",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["denonavr"],
|
||||
"requirements": ["denonavr==1.1.1"],
|
||||
"requirements": ["denonavr==1.1.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Denon",
|
||||
|
||||
@@ -8,7 +8,11 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_DOMAIN
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
|
||||
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.trigger import (
|
||||
TriggerActionType,
|
||||
TriggerInfo,
|
||||
TriggerProtocol,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import (
|
||||
@@ -21,28 +25,13 @@ from .helpers import async_validate_device_automation_config
|
||||
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
class DeviceAutomationTriggerProtocol(Protocol):
|
||||
class DeviceAutomationTriggerProtocol(TriggerProtocol, Protocol):
|
||||
"""Define the format of device_trigger modules.
|
||||
|
||||
Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config.
|
||||
Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config
|
||||
from TriggerProtocol.
|
||||
"""
|
||||
|
||||
TRIGGER_SCHEMA: vol.Schema
|
||||
|
||||
async def async_validate_trigger_config(
|
||||
self, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
|
||||
async def async_attach_trigger(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
action: TriggerActionType,
|
||||
trigger_info: TriggerInfo,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
|
||||
async def async_get_trigger_capabilities(
|
||||
self, hass: HomeAssistant, config: ConfigType
|
||||
) -> dict[str, vol.Schema]:
|
||||
|
||||
@@ -7,7 +7,6 @@ from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
from fnmatch import translate
|
||||
from functools import lru_cache, partial
|
||||
from ipaddress import IPv4Address
|
||||
import itertools
|
||||
import logging
|
||||
import re
|
||||
@@ -23,7 +22,6 @@ from aiodiscover.discovery import (
|
||||
from cached_ipaddress import cached_ip_addresses
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components import network
|
||||
from homeassistant.components.device_tracker import (
|
||||
ATTR_HOST_NAME,
|
||||
ATTR_IP,
|
||||
@@ -423,33 +421,9 @@ class DHCPWatcher(WatcherBase):
|
||||
response.ip_address, response.hostname, response.mac_address
|
||||
)
|
||||
|
||||
async def async_get_adapter_indexes(self) -> list[int] | None:
|
||||
"""Get the adapter indexes."""
|
||||
adapters = await network.async_get_adapters(self.hass)
|
||||
if network.async_only_default_interface_enabled(adapters):
|
||||
return None
|
||||
return [
|
||||
adapter["index"]
|
||||
for adapter in adapters
|
||||
if (
|
||||
adapter["enabled"]
|
||||
and adapter["index"] is not None
|
||||
and adapter["ipv4"]
|
||||
and (
|
||||
addresses := [IPv4Address(ip["address"]) for ip in adapter["ipv4"]]
|
||||
)
|
||||
and any(
|
||||
ip for ip in addresses if not ip.is_loopback and not ip.is_global
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
async def async_start(self) -> None:
|
||||
"""Start watching for dhcp packets."""
|
||||
self._unsub = await aiodhcpwatcher.async_start(
|
||||
self._async_process_dhcp_request,
|
||||
await self.async_get_adapter_indexes(),
|
||||
)
|
||||
self._unsub = await aiodhcpwatcher.async_start(self._async_process_dhcp_request)
|
||||
|
||||
|
||||
class RediscoveryWatcher(WatcherBase):
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"domain": "dhcp",
|
||||
"name": "DHCP Discovery",
|
||||
"codeowners": ["@bdraco"],
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/dhcp",
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -2,13 +2,32 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
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
|
||||
|
||||
from .const import _LOGGER, CONF_DOWNLOAD_DIR
|
||||
from .services import register_services
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
ATTR_FILENAME,
|
||||
ATTR_OVERWRITE,
|
||||
ATTR_SUBDIR,
|
||||
ATTR_URL,
|
||||
CONF_DOWNLOAD_DIR,
|
||||
DOMAIN,
|
||||
DOWNLOAD_COMPLETED_EVENT,
|
||||
DOWNLOAD_FAILED_EVENT,
|
||||
SERVICE_DOWNLOAD_FILE,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -25,6 +44,127 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
return False
|
||||
|
||||
register_services(hass)
|
||||
def download_file(service: ServiceCall) -> None:
|
||||
"""Start thread to download file specified in the URL."""
|
||||
|
||||
def do_download() -> None:
|
||||
"""Download the file."""
|
||||
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:
|
||||
_LOGGER.warning(
|
||||
"Downloading '%s' failed, status_code=%d", url, req.status_code
|
||||
)
|
||||
hass.bus.fire(
|
||||
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
|
||||
{"url": url, "filename": filename},
|
||||
)
|
||||
|
||||
else:
|
||||
if filename is None and "content-disposition" in req.headers:
|
||||
match = re.findall(
|
||||
r"filename=(\S+)", req.headers["content-disposition"]
|
||||
)
|
||||
|
||||
if match:
|
||||
filename = match[0].strip("'\" ")
|
||||
|
||||
if not filename:
|
||||
filename = os.path.basename(url).strip()
|
||||
|
||||
if not filename:
|
||||
filename = "ha_download"
|
||||
|
||||
# Check the filename
|
||||
raise_if_invalid_filename(filename)
|
||||
|
||||
# Do we want to download to subdir, create if needed
|
||||
if subdir:
|
||||
subdir_path = os.path.join(download_path, subdir)
|
||||
|
||||
# Ensure subdir exist
|
||||
os.makedirs(subdir_path, exist_ok=True)
|
||||
|
||||
final_path = os.path.join(subdir_path, filename)
|
||||
|
||||
else:
|
||||
final_path = os.path.join(download_path, filename)
|
||||
|
||||
path, ext = os.path.splitext(final_path)
|
||||
|
||||
# If file exist append a number.
|
||||
# We test filename, filename_2..
|
||||
if not overwrite:
|
||||
tries = 1
|
||||
final_path = path + ext
|
||||
while os.path.isfile(final_path):
|
||||
tries += 1
|
||||
|
||||
final_path = f"{path}_{tries}.{ext}"
|
||||
|
||||
_LOGGER.debug("%s -> %s", url, final_path)
|
||||
|
||||
with open(final_path, "wb") as fil:
|
||||
for chunk in req.iter_content(1024):
|
||||
fil.write(chunk)
|
||||
|
||||
_LOGGER.debug("Downloading of %s done", url)
|
||||
hass.bus.fire(
|
||||
f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}",
|
||||
{"url": url, "filename": filename},
|
||||
)
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.exception("ConnectionError occurred for %s", url)
|
||||
hass.bus.fire(
|
||||
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
|
||||
{"url": url, "filename": filename},
|
||||
)
|
||||
|
||||
# Remove file if we started downloading but failed
|
||||
if final_path and os.path.isfile(final_path):
|
||||
os.remove(final_path)
|
||||
except ValueError:
|
||||
_LOGGER.exception("Invalid value")
|
||||
hass.bus.fire(
|
||||
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
|
||||
{"url": url, "filename": filename},
|
||||
)
|
||||
|
||||
# Remove file if we started downloading but failed
|
||||
if final_path and os.path.isfile(final_path):
|
||||
os.remove(final_path)
|
||||
|
||||
threading.Thread(target=do_download).start()
|
||||
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_DOWNLOAD_FILE,
|
||||
download_file,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_FILENAME): cv.string,
|
||||
vol.Optional(ATTR_SUBDIR): cv.string,
|
||||
vol.Required(ATTR_URL): cv.url,
|
||||
vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
"""Support for functionality to download files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
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
|
||||
|
||||
from .const import (
|
||||
_LOGGER,
|
||||
ATTR_FILENAME,
|
||||
ATTR_OVERWRITE,
|
||||
ATTR_SUBDIR,
|
||||
ATTR_URL,
|
||||
CONF_DOWNLOAD_DIR,
|
||||
DOMAIN,
|
||||
DOWNLOAD_COMPLETED_EVENT,
|
||||
DOWNLOAD_FAILED_EVENT,
|
||||
SERVICE_DOWNLOAD_FILE,
|
||||
)
|
||||
|
||||
|
||||
def download_file(service: ServiceCall) -> None:
|
||||
"""Start thread to download file specified in the URL."""
|
||||
|
||||
entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
|
||||
download_path = entry.data[CONF_DOWNLOAD_DIR]
|
||||
|
||||
def do_download() -> None:
|
||||
"""Download the file."""
|
||||
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:
|
||||
_LOGGER.warning(
|
||||
"Downloading '%s' failed, status_code=%d", url, req.status_code
|
||||
)
|
||||
service.hass.bus.fire(
|
||||
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
|
||||
{"url": url, "filename": filename},
|
||||
)
|
||||
|
||||
else:
|
||||
if filename is None and "content-disposition" in req.headers:
|
||||
match = re.findall(
|
||||
r"filename=(\S+)", req.headers["content-disposition"]
|
||||
)
|
||||
|
||||
if match:
|
||||
filename = match[0].strip("'\" ")
|
||||
|
||||
if not filename:
|
||||
filename = os.path.basename(url).strip()
|
||||
|
||||
if not filename:
|
||||
filename = "ha_download"
|
||||
|
||||
# Check the filename
|
||||
raise_if_invalid_filename(filename)
|
||||
|
||||
# Do we want to download to subdir, create if needed
|
||||
if subdir:
|
||||
subdir_path = os.path.join(download_path, subdir)
|
||||
|
||||
# Ensure subdir exist
|
||||
os.makedirs(subdir_path, exist_ok=True)
|
||||
|
||||
final_path = os.path.join(subdir_path, filename)
|
||||
|
||||
else:
|
||||
final_path = os.path.join(download_path, filename)
|
||||
|
||||
path, ext = os.path.splitext(final_path)
|
||||
|
||||
# If file exist append a number.
|
||||
# We test filename, filename_2..
|
||||
if not overwrite:
|
||||
tries = 1
|
||||
final_path = path + ext
|
||||
while os.path.isfile(final_path):
|
||||
tries += 1
|
||||
|
||||
final_path = f"{path}_{tries}.{ext}"
|
||||
|
||||
_LOGGER.debug("%s -> %s", url, final_path)
|
||||
|
||||
with open(final_path, "wb") as fil:
|
||||
for chunk in req.iter_content(1024):
|
||||
fil.write(chunk)
|
||||
|
||||
_LOGGER.debug("Downloading of %s done", url)
|
||||
service.hass.bus.fire(
|
||||
f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}",
|
||||
{"url": url, "filename": filename},
|
||||
)
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.exception("ConnectionError occurred for %s", url)
|
||||
service.hass.bus.fire(
|
||||
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
|
||||
{"url": url, "filename": filename},
|
||||
)
|
||||
|
||||
# Remove file if we started downloading but failed
|
||||
if final_path and os.path.isfile(final_path):
|
||||
os.remove(final_path)
|
||||
except ValueError:
|
||||
_LOGGER.exception("Invalid value")
|
||||
service.hass.bus.fire(
|
||||
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
|
||||
{"url": url, "filename": filename},
|
||||
)
|
||||
|
||||
# Remove file if we started downloading but failed
|
||||
if final_path and os.path.isfile(final_path):
|
||||
os.remove(final_path)
|
||||
|
||||
threading.Thread(target=do_download).start()
|
||||
|
||||
|
||||
def register_services(hass: HomeAssistant) -> None:
|
||||
"""Register the services for the downloader component."""
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_DOWNLOAD_FILE,
|
||||
download_file,
|
||||
schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(ATTR_FILENAME): cv.string,
|
||||
vol.Optional(ATTR_SUBDIR): cv.string,
|
||||
vol.Required(ATTR_URL): cv.url,
|
||||
vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean,
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -148,6 +148,11 @@ class EcoNetThermostat(EcoNetEntity[Thermostat], ClimateEntity):
|
||||
if target_temp_low or target_temp_high:
|
||||
self._econet.set_set_point(None, target_temp_high, target_temp_low)
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool:
|
||||
"""Return true if aux heater."""
|
||||
return self._econet.mode == ThermostatOperationMode.EMERGENCY_HEAT
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return hvac operation ie. heat, cool, mode.
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
|
||||
from .util import get_supported_entities
|
||||
from .util import get_supported_entitites
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
@@ -49,7 +49,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Add entities for passed config_entry in HA."""
|
||||
async_add_entities(
|
||||
get_supported_entities(
|
||||
get_supported_entitites(
|
||||
config_entry.runtime_data, EcovacsBinarySensor, ENTITY_DESCRIPTIONS
|
||||
)
|
||||
)
|
||||
|
||||
@@ -16,13 +16,13 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
from .const import SUPPORTED_LIFESPANS
|
||||
from .const import SUPPORTED_LIFESPANS, SUPPORTED_STATION_ACTIONS
|
||||
from .entity import (
|
||||
EcovacsCapabilityEntityDescription,
|
||||
EcovacsDescriptionEntity,
|
||||
EcovacsEntity,
|
||||
)
|
||||
from .util import get_supported_entities
|
||||
from .util import get_supported_entitites
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
@@ -62,7 +62,7 @@ STATION_ENTITY_DESCRIPTIONS = tuple(
|
||||
key=f"station_action_{action.name.lower()}",
|
||||
translation_key=f"station_action_{action.name.lower()}",
|
||||
)
|
||||
for action in StationAction
|
||||
for action in SUPPORTED_STATION_ACTIONS
|
||||
)
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Add entities for passed config_entry in HA."""
|
||||
controller = config_entry.runtime_data
|
||||
entities: list[EcovacsEntity] = get_supported_entities(
|
||||
entities: list[EcovacsEntity] = get_supported_entitites(
|
||||
controller, EcovacsButtonEntity, ENTITY_DESCRIPTIONS
|
||||
)
|
||||
entities.extend(
|
||||
|
||||
@@ -172,13 +172,7 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors = {}
|
||||
|
||||
if user_input:
|
||||
self._async_abort_entries_match(
|
||||
{
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_OVERRIDE_REST_URL: user_input.get(CONF_OVERRIDE_REST_URL),
|
||||
CONF_OVERRIDE_MQTT_URL: user_input.get(CONF_OVERRIDE_MQTT_URL),
|
||||
}
|
||||
)
|
||||
self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]})
|
||||
|
||||
errors = await _validate_input(self.hass, user_input)
|
||||
|
||||
@@ -220,9 +214,6 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
data_schema=vol.Schema(schema), suggested_values=user_input
|
||||
),
|
||||
description_placeholders={
|
||||
"account_name": "Ecovacs",
|
||||
},
|
||||
errors=errors,
|
||||
last_step=True,
|
||||
)
|
||||
|
||||
@@ -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.10", "deebot-client==13.2.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==13.1.0"]
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ from .entity import (
|
||||
EcovacsEntity,
|
||||
EventT,
|
||||
)
|
||||
from .util import get_supported_entities
|
||||
from .util import get_supported_entitites
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
@@ -87,7 +87,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Add entities for passed config_entry in HA."""
|
||||
controller = config_entry.runtime_data
|
||||
entities: list[EcovacsEntity] = get_supported_entities(
|
||||
entities: list[EcovacsEntity] = get_supported_entitites(
|
||||
controller, EcovacsNumberEntity, ENTITY_DESCRIPTIONS
|
||||
)
|
||||
if entities:
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
config-flow: done
|
||||
test-before-configure: done
|
||||
unique-config-entry: done
|
||||
config-flow-test-coverage: done
|
||||
runtime-data: done
|
||||
test-before-setup:
|
||||
status: todo
|
||||
comment: Legacy code will not raise on setup currently
|
||||
appropriate-polling:
|
||||
status: todo
|
||||
comment: |
|
||||
@mib1185 Please check legacy code.
|
||||
deebot-client pulls only once at beginning and afterwards is pushed based
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
entity-event-setup: done
|
||||
dependency-transparency:
|
||||
status: todo
|
||||
comment: Currently unsure if all dependencies need to validated or only direct ones.
|
||||
action-setup:
|
||||
status: done
|
||||
comment: "`raw_get_positions` is a entity service"
|
||||
common-modules: done
|
||||
docs-high-level-description: todo
|
||||
docs-installation-instructions: todo
|
||||
docs-removal-instructions: todo
|
||||
docs-actions: todo
|
||||
brands: done
|
||||
|
||||
# Silver
|
||||
config-entry-unloading: done
|
||||
log-when-unavailable: todo
|
||||
entity-unavailable: done
|
||||
action-exceptions: todo
|
||||
reauthentication-flow: todo
|
||||
parallel-updates:
|
||||
status: todo
|
||||
comment: |
|
||||
@mib1185 Please check legacy code.
|
||||
deebot-client uses internally semaphores to prevent to many parallel requests
|
||||
test-coverage: todo
|
||||
integration-owner: done
|
||||
docs-installation-parameters: todo
|
||||
docs-configuration-parameters: todo
|
||||
|
||||
# Gold
|
||||
entity-translations:
|
||||
status: todo
|
||||
comment: |
|
||||
@mib1185 Legacy entities are not translated
|
||||
entity-device-class: done
|
||||
devices: done
|
||||
entity-category: done
|
||||
entity-disabled-by-default: done
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: Not supported as we don't talk directly to the devices
|
||||
stale-devices: todo
|
||||
diagnostics: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
dynamic-devices:
|
||||
status: todo
|
||||
comment: New devices are discovered only on boot currently
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Not supported as we don't talk directly to the devices
|
||||
repair-issues: todo
|
||||
docs-use-cases: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-data-update: todo
|
||||
docs-known-limitations: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-examples: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency:
|
||||
status: todo
|
||||
comment: |
|
||||
@mib1185 Please check legacy code.
|
||||
deebot-client is async
|
||||
inject-websession:
|
||||
status: todo
|
||||
comment: |
|
||||
@mib1185 Please check legacy code.
|
||||
deebot-client uses the passed websession
|
||||
strict-typing:
|
||||
status: todo
|
||||
comment: |
|
||||
@mib1185 Please check legacy code.
|
||||
deebot-client is typed
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import EcovacsConfigEntry
|
||||
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
|
||||
from .util import get_name_key, get_supported_entities
|
||||
from .util import get_name_key, get_supported_entitites
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
@@ -59,7 +59,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Add entities for passed config_entry in HA."""
|
||||
controller = config_entry.runtime_data
|
||||
entities = get_supported_entities(
|
||||
entities = get_supported_entitites(
|
||||
controller, EcovacsSelectEntity, ENTITY_DESCRIPTIONS
|
||||
)
|
||||
if entities:
|
||||
|
||||
@@ -6,8 +6,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Generic
|
||||
|
||||
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
|
||||
from deebot_client.device import Device
|
||||
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan
|
||||
from deebot_client.events import (
|
||||
BatteryEvent,
|
||||
ErrorEvent,
|
||||
@@ -35,7 +34,7 @@ from homeassistant.const import (
|
||||
UnitOfArea,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
@@ -48,7 +47,7 @@ from .entity import (
|
||||
EcovacsLegacyEntity,
|
||||
EventT,
|
||||
)
|
||||
from .util import get_name_key, get_options, get_supported_entities
|
||||
from .util import get_name_key, get_options, get_supported_entitites
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
@@ -60,15 +59,6 @@ class EcovacsSensorEntityDescription(
|
||||
"""Ecovacs sensor entity description."""
|
||||
|
||||
value_fn: Callable[[EventT], StateType]
|
||||
native_unit_of_measurement_fn: Callable[[DeviceType], str | None] | None = None
|
||||
|
||||
|
||||
@callback
|
||||
def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
|
||||
"""Get the area native unit of measurement based on device type."""
|
||||
if device_type is DeviceType.MOWER:
|
||||
return UnitOfArea.SQUARE_CENTIMETERS
|
||||
return UnitOfArea.SQUARE_METERS
|
||||
|
||||
|
||||
ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
||||
@@ -78,9 +68,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
||||
capability_fn=lambda caps: caps.stats.clean,
|
||||
value_fn=lambda e: e.area,
|
||||
translation_key="stats_area",
|
||||
device_class=SensorDeviceClass.AREA,
|
||||
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
|
||||
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
|
||||
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
|
||||
),
|
||||
EcovacsSensorEntityDescription[StatsEvent](
|
||||
key="stats_time",
|
||||
@@ -97,10 +85,8 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
|
||||
value_fn=lambda e: e.area,
|
||||
key="total_stats_area",
|
||||
translation_key="total_stats_area",
|
||||
device_class=SensorDeviceClass.AREA,
|
||||
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
|
||||
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
|
||||
),
|
||||
EcovacsSensorEntityDescription[TotalStatsEvent](
|
||||
capability_fn=lambda caps: caps.stats.total,
|
||||
@@ -211,7 +197,7 @@ async def async_setup_entry(
|
||||
"""Add entities for passed config_entry in HA."""
|
||||
controller = config_entry.runtime_data
|
||||
|
||||
entities: list[EcovacsEntity] = get_supported_entities(
|
||||
entities: list[EcovacsEntity] = get_supported_entitites(
|
||||
controller, EcovacsSensor, ENTITY_DESCRIPTIONS
|
||||
)
|
||||
entities.extend(
|
||||
@@ -263,27 +249,6 @@ class EcovacsSensor(
|
||||
|
||||
entity_description: EcovacsSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: Device,
|
||||
capability: CapabilityEvent,
|
||||
entity_description: EcovacsSensorEntityDescription,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize entity."""
|
||||
super().__init__(device, capability, entity_description, **kwargs)
|
||||
if (
|
||||
entity_description.native_unit_of_measurement_fn
|
||||
and (
|
||||
native_unit_of_measurement
|
||||
:= entity_description.native_unit_of_measurement_fn(
|
||||
device.capabilities.device_type
|
||||
)
|
||||
)
|
||||
is not None
|
||||
):
|
||||
self._attr_native_unit_of_measurement = native_unit_of_measurement
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Set up the event listeners now that hass is ready."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@@ -22,12 +22,8 @@
|
||||
"verify_mqtt_certificate": "Verify MQTT SSL certificate"
|
||||
},
|
||||
"data_description": {
|
||||
"country": "The country of your {account_name} account.",
|
||||
"override_rest_url": "Enter the REST URL of your self-hosted instance including the scheme (http/https).",
|
||||
"override_mqtt_url": "Enter the MQTT URL of your self-hosted instance including the scheme (mqtt/mqtts).",
|
||||
"password": "[%key:common::config_flow::data_description::password%]",
|
||||
"username": "[%key:common::config_flow::data_description::username%]",
|
||||
"verify_mqtt_certificate": "Should SSL certificates be verified? Uncheck this checkbox only if you are using a self-signed certificate."
|
||||
"override_mqtt_url": "Enter the MQTT URL of your self-hosted instance including the scheme (mqtt/mqtts)."
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -17,7 +17,7 @@ from .entity import (
|
||||
EcovacsDescriptionEntity,
|
||||
EcovacsEntity,
|
||||
)
|
||||
from .util import get_supported_entities
|
||||
from .util import get_supported_entitites
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
@@ -109,7 +109,7 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Add entities for passed config_entry in HA."""
|
||||
controller = config_entry.runtime_data
|
||||
entities: list[EcovacsEntity] = get_supported_entities(
|
||||
entities: list[EcovacsEntity] = get_supported_entitites(
|
||||
controller, EcovacsSwitchEntity, ENTITY_DESCRIPTIONS
|
||||
)
|
||||
if entities:
|
||||
|
||||
@@ -32,7 +32,7 @@ def get_client_device_id(hass: HomeAssistant, self_hosted: bool) -> str:
|
||||
)
|
||||
|
||||
|
||||
def get_supported_entities(
|
||||
def get_supported_entitites(
|
||||
controller: EcovacsController,
|
||||
entity_class: type[EcovacsDescriptionEntity],
|
||||
descriptions: tuple[EcovacsCapabilityEntityDescription, ...],
|
||||
|
||||
@@ -13,7 +13,6 @@ PLATFORMS = [
|
||||
Platform.CLIMATE,
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.TIME,
|
||||
|
||||
@@ -15,12 +15,6 @@
|
||||
},
|
||||
"night_temperature_offset": {
|
||||
"default": "mdi:thermometer"
|
||||
},
|
||||
"system_led": {
|
||||
"default": "mdi:led-on",
|
||||
"state": {
|
||||
"0": "mdi:led-off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -109,20 +109,6 @@ HEATER_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalHeater], ..
|
||||
),
|
||||
)
|
||||
|
||||
GENERAL_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalDevice], ...] = (
|
||||
EheimDigitalNumberDescription[EheimDigitalDevice](
|
||||
key="system_led",
|
||||
translation_key="system_led",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
native_min_value=0,
|
||||
native_max_value=100,
|
||||
native_step=PRECISION_WHOLE,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda device: device.sys_led,
|
||||
set_value_fn=lambda device, value: device.set_sys_led(int(value)),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -152,10 +138,6 @@ async def async_setup_entry(
|
||||
)
|
||||
for description in HEATER_DESCRIPTIONS
|
||||
)
|
||||
entities.extend(
|
||||
EheimDigitalNumber[EheimDigitalDevice](coordinator, device, description)
|
||||
for description in GENERAL_DESCRIPTIONS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
"""EHEIM Digital select entities."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.types import FilterMode
|
||||
|
||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
from .entity import EheimDigitalEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EheimDigitalSelectDescription(SelectEntityDescription, Generic[_DeviceT_co]):
|
||||
"""Class describing EHEIM Digital select entities."""
|
||||
|
||||
value_fn: Callable[[_DeviceT_co], str | None]
|
||||
set_value_fn: Callable[[_DeviceT_co, str], Awaitable[None]]
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
EheimDigitalSelectDescription[EheimDigitalClassicVario], ...
|
||||
] = (
|
||||
EheimDigitalSelectDescription[EheimDigitalClassicVario](
|
||||
key="filter_mode",
|
||||
translation_key="filter_mode",
|
||||
value_fn=(
|
||||
lambda device: device.filter_mode.name.lower()
|
||||
if device.filter_mode is not None
|
||||
else None
|
||||
),
|
||||
set_value_fn=(
|
||||
lambda device, value: device.set_filter_mode(FilterMode[value.upper()])
|
||||
),
|
||||
options=[name.lower() for name in FilterMode.__members__],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: EheimDigitalConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the callbacks for the coordinator so select entities can be added as devices are found."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
def async_setup_device_entities(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the number entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalSelect[EheimDigitalDevice]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities.extend(
|
||||
EheimDigitalSelect[EheimDigitalClassicVario](
|
||||
coordinator, device, description
|
||||
)
|
||||
for description in CLASSICVARIO_DESCRIPTIONS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
coordinator.add_platform_callback(async_setup_device_entities)
|
||||
async_setup_device_entities(coordinator.hub.devices)
|
||||
|
||||
|
||||
class EheimDigitalSelect(
|
||||
EheimDigitalEntity[_DeviceT_co], SelectEntity, Generic[_DeviceT_co]
|
||||
):
|
||||
"""Represent an EHEIM Digital select entity."""
|
||||
|
||||
entity_description: EheimDigitalSelectDescription[_DeviceT_co]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: _DeviceT_co,
|
||||
description: EheimDigitalSelectDescription[_DeviceT_co],
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital select entity."""
|
||||
super().__init__(coordinator, device)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{self._device_address}_{description.key}"
|
||||
|
||||
@override
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
return await self.entity_description.set_value_fn(self._device, option)
|
||||
|
||||
@override
|
||||
def _async_update_attrs(self) -> None:
|
||||
self._attr_current_option = self.entity_description.value_fn(self._device)
|
||||
@@ -62,19 +62,6 @@
|
||||
},
|
||||
"night_temperature_offset": {
|
||||
"name": "Night temperature offset"
|
||||
},
|
||||
"system_led": {
|
||||
"name": "System LED brightness"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"filter_mode": {
|
||||
"name": "Filter mode",
|
||||
"state": {
|
||||
"manual": "Manual",
|
||||
"pulse": "Pulse",
|
||||
"bio": "Bio"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -20,8 +20,10 @@ from homeassistant.components.climate import (
|
||||
from homeassistant.const import PRECISION_WHOLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
from . import ElkM1ConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .entity import ElkEntity, create_elk_entities
|
||||
|
||||
SUPPORT_HVAC = [
|
||||
@@ -76,6 +78,7 @@ class ElkThermostat(ElkEntity, ClimateEntity):
|
||||
_attr_precision = PRECISION_WHOLE
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.FAN_MODE
|
||||
| ClimateEntityFeature.AUX_HEAT
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
@@ -125,6 +128,11 @@ class ElkThermostat(ElkEntity, ClimateEntity):
|
||||
"""Return the current humidity."""
|
||||
return self._element.humidity
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool:
|
||||
"""Return if aux heater is on."""
|
||||
return self._element.mode == ThermostatMode.EMERGENCY_HEAT
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return the fan setting."""
|
||||
@@ -143,6 +151,34 @@ class ElkThermostat(ElkEntity, ClimateEntity):
|
||||
thermostat_mode, fan_mode = HASS_TO_ELK_HVAC_MODES[hvac_mode]
|
||||
self._elk_set(thermostat_mode, fan_mode)
|
||||
|
||||
async def async_turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"migrate_aux_heat",
|
||||
breaks_in_ha_version="2025.4.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
translation_key="migrate_aux_heat",
|
||||
severity=IssueSeverity.WARNING,
|
||||
)
|
||||
self._elk_set(ThermostatMode.EMERGENCY_HEAT, None)
|
||||
|
||||
async def async_turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"migrate_aux_heat",
|
||||
breaks_in_ha_version="2025.4.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
translation_key="migrate_aux_heat",
|
||||
severity=IssueSeverity.WARNING,
|
||||
)
|
||||
self._elk_set(ThermostatMode.HEAT, None)
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new target fan mode."""
|
||||
thermostat_mode, elk_fan_mode = HASS_TO_ELK_FAN_MODES[fan_mode]
|
||||
|
||||
@@ -189,5 +189,18 @@
|
||||
"name": "Sensor zone trigger",
|
||||
"description": "Triggers zone."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"migrate_aux_heat": {
|
||||
"title": "Migration of Elk-M1 set_aux_heat action",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "The Elk-M1 `set_aux_heat` action has been migrated. A new emergency heat switch entity is available for each thermostat.\n\nUpdate any automations to use the new emergency heat switch entity. When this is done, select **Submit** to fix this issue.",
|
||||
"title": "[%key:component::elkm1::issues::migrate_aux_heat::title%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sense_energy"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["sense-energy==0.13.8"]
|
||||
"requirements": ["sense-energy==0.13.7"]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ from pyephember2.pyephember2 import (
|
||||
ZoneMode,
|
||||
zone_current_temperature,
|
||||
zone_is_active,
|
||||
zone_is_boost_active,
|
||||
zone_is_hotwater,
|
||||
zone_mode,
|
||||
zone_name,
|
||||
@@ -101,6 +102,7 @@ class EphEmberThermostat(ClimateEntity):
|
||||
self._attr_name = self._zone_name
|
||||
|
||||
if self._hot_water:
|
||||
self._attr_supported_features = ClimateEntityFeature.AUX_HEAT
|
||||
self._attr_target_temperature_step = None
|
||||
else:
|
||||
self._attr_target_temperature_step = 0.5
|
||||
@@ -142,6 +144,22 @@ class EphEmberThermostat(ClimateEntity):
|
||||
else:
|
||||
_LOGGER.error("Invalid operation mode provided %s", hvac_mode)
|
||||
|
||||
@property
|
||||
def is_aux_heat(self) -> bool:
|
||||
"""Return true if aux heater."""
|
||||
|
||||
return zone_is_boost_active(self._zone)
|
||||
|
||||
def turn_aux_heat_on(self) -> None:
|
||||
"""Turn auxiliary heater on."""
|
||||
self._ember.activate_boost_by_name(
|
||||
self._zone_name, zone_target_temperature(self._zone)
|
||||
)
|
||||
|
||||
def turn_aux_heat_off(self) -> None:
|
||||
"""Turn auxiliary heater off."""
|
||||
self._ember.deactivate_boost_by_name(self._zone_name)
|
||||
|
||||
def set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
|
||||
@@ -239,6 +239,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
self._states = cast(dict[int, _StateT], entry_data.state[state_type])
|
||||
assert entry_data.device_info is not None
|
||||
device_info = entry_data.device_info
|
||||
self._device_info = device_info
|
||||
self._on_entry_data_changed()
|
||||
self._key = entity_info.key
|
||||
self._state_type = state_type
|
||||
@@ -326,11 +327,6 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
@callback
|
||||
def _on_entry_data_changed(self) -> None:
|
||||
entry_data = self._entry_data
|
||||
# Update the device info since it can change
|
||||
# when the device is reconnected
|
||||
if TYPE_CHECKING:
|
||||
assert entry_data.device_info is not None
|
||||
self._device_info = entry_data.device_info
|
||||
self._api_version = entry_data.api_version
|
||||
self._client = entry_data.client
|
||||
if self._device_info.has_deep_sleep:
|
||||
|
||||
@@ -63,7 +63,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
|
||||
if self._supports_speed_levels:
|
||||
data["speed_level"] = math.ceil(
|
||||
percentage_to_ranged_value(
|
||||
(1, self._static_info.supported_speed_count), percentage
|
||||
(1, self._static_info.supported_speed_levels), percentage
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -121,7 +121,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
|
||||
)
|
||||
|
||||
return ranged_value_to_percentage(
|
||||
(1, self._static_info.supported_speed_count), self._state.speed_level
|
||||
(1, self._static_info.supported_speed_levels), self._state.speed_level
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -164,7 +164,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
|
||||
if not supports_speed_levels:
|
||||
self._attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS)
|
||||
else:
|
||||
self._attr_speed_count = static_info.supported_speed_count
|
||||
self._attr_speed_count = static_info.supported_speed_levels
|
||||
|
||||
|
||||
async_setup_entry = partial(
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from aioesphomeapi import (
|
||||
APIVersion,
|
||||
ColorMode as ESPHomeColorMode,
|
||||
EntityInfo,
|
||||
LightColorCapability,
|
||||
LightInfo,
|
||||
@@ -107,15 +106,15 @@ def _mired_to_kelvin(mired_temperature: float) -> int:
|
||||
|
||||
|
||||
@lru_cache
|
||||
def _color_mode_to_ha(mode: ESPHomeColorMode) -> ColorMode:
|
||||
def _color_mode_to_ha(mode: int) -> str:
|
||||
"""Convert an esphome color mode to a HA color mode constant.
|
||||
|
||||
Choose the color mode that best matches the feature-set.
|
||||
"""
|
||||
candidates: list[tuple[ColorMode, LightColorCapability]] = []
|
||||
candidates = []
|
||||
for ha_mode, cap_lists in _COLOR_MODE_MAPPING.items():
|
||||
for caps in cap_lists:
|
||||
if caps.value == mode:
|
||||
if caps == mode:
|
||||
# exact match
|
||||
return ha_mode
|
||||
if (mode & caps) == caps:
|
||||
@@ -132,8 +131,8 @@ def _color_mode_to_ha(mode: ESPHomeColorMode) -> ColorMode:
|
||||
|
||||
@lru_cache
|
||||
def _filter_color_modes(
|
||||
supported: list[ESPHomeColorMode], features: LightColorCapability
|
||||
) -> tuple[ESPHomeColorMode, ...]:
|
||||
supported: list[int], features: LightColorCapability
|
||||
) -> tuple[int, ...]:
|
||||
"""Filter the given supported color modes.
|
||||
|
||||
Excluding all values that don't have the requested features.
|
||||
@@ -157,7 +156,7 @@ def _least_complex_color_mode(color_modes: tuple[int, ...]) -> int:
|
||||
class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
|
||||
"""A light implementation for ESPHome."""
|
||||
|
||||
_native_supported_color_modes: tuple[ESPHomeColorMode, ...]
|
||||
_native_supported_color_modes: tuple[int, ...]
|
||||
_supports_color_mode = False
|
||||
|
||||
@property
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==31.1.0",
|
||||
"aioesphomeapi==30.2.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.15.1"
|
||||
],
|
||||
|
||||
@@ -88,9 +88,9 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
||||
return
|
||||
if (
|
||||
state_class == EsphomeSensorStateClass.MEASUREMENT
|
||||
and static_info.legacy_last_reset_type == LastResetType.AUTO
|
||||
and static_info.last_reset_type == LastResetType.AUTO
|
||||
):
|
||||
# Legacy, legacy_last_reset_type auto was the equivalent to the
|
||||
# Legacy, last_reset_type auto was the equivalent to the
|
||||
# TOTAL_INCREASING state class
|
||||
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
else:
|
||||
|
||||
@@ -35,7 +35,7 @@ async def validate_host(
|
||||
hass: HomeAssistant, host: str
|
||||
) -> tuple[str, FroniusConfigEntryData]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
fronius = Fronius(async_get_clientsession(hass, verify_ssl=False), host)
|
||||
fronius = Fronius(async_get_clientsession(hass), host)
|
||||
|
||||
try:
|
||||
datalogger_info: dict[str, Any]
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
"current_dc": {
|
||||
"default": "mdi:current-dc"
|
||||
},
|
||||
"current_dc_mppt_no": {
|
||||
"current_dc_2": {
|
||||
"default": "mdi:current-dc"
|
||||
},
|
||||
"voltage_dc": {
|
||||
"default": "mdi:current-dc"
|
||||
},
|
||||
"voltage_dc_mppt_no": {
|
||||
"voltage_dc_2": {
|
||||
"default": "mdi:current-dc"
|
||||
},
|
||||
"co2_factor": {
|
||||
|
||||
@@ -168,26 +168,6 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="current_dc_mppt_no",
|
||||
translation_placeholders={"mppt_no": "2"},
|
||||
),
|
||||
FroniusSensorEntityDescription(
|
||||
key="current_dc_3",
|
||||
default_value=0,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="current_dc_mppt_no",
|
||||
translation_placeholders={"mppt_no": "3"},
|
||||
),
|
||||
FroniusSensorEntityDescription(
|
||||
key="current_dc_4",
|
||||
default_value=0,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="current_dc_mppt_no",
|
||||
translation_placeholders={"mppt_no": "4"},
|
||||
),
|
||||
FroniusSensorEntityDescription(
|
||||
key="power_ac",
|
||||
@@ -217,26 +197,6 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="voltage_dc_mppt_no",
|
||||
translation_placeholders={"mppt_no": "2"},
|
||||
),
|
||||
FroniusSensorEntityDescription(
|
||||
key="voltage_dc_3",
|
||||
default_value=0,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="voltage_dc_mppt_no",
|
||||
translation_placeholders={"mppt_no": "3"},
|
||||
),
|
||||
FroniusSensorEntityDescription(
|
||||
key="voltage_dc_4",
|
||||
default_value=0,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
translation_key="voltage_dc_mppt_no",
|
||||
translation_placeholders={"mppt_no": "4"},
|
||||
),
|
||||
# device status entities
|
||||
FroniusSensorEntityDescription(
|
||||
@@ -767,7 +727,7 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
|
||||
self.response_key = description.response_key or description.key
|
||||
self.solar_net_id = solar_net_id
|
||||
self._attr_native_value = self._get_entity_value()
|
||||
self._attr_translation_key = description.translation_key or description.key
|
||||
self._attr_translation_key = description.key
|
||||
|
||||
def _device_data(self) -> dict[str, Any]:
|
||||
"""Extract information for SolarNet device from coordinator data."""
|
||||
|
||||
@@ -52,8 +52,8 @@
|
||||
"current_dc": {
|
||||
"name": "DC current"
|
||||
},
|
||||
"current_dc_mppt_no": {
|
||||
"name": "DC current {mppt_no}"
|
||||
"current_dc_2": {
|
||||
"name": "DC current 2"
|
||||
},
|
||||
"power_ac": {
|
||||
"name": "AC power"
|
||||
@@ -64,8 +64,8 @@
|
||||
"voltage_dc": {
|
||||
"name": "DC voltage"
|
||||
},
|
||||
"voltage_dc_mppt_no": {
|
||||
"name": "DC voltage {mppt_no}"
|
||||
"voltage_dc_2": {
|
||||
"name": "DC voltage 2"
|
||||
},
|
||||
"inverter_state": {
|
||||
"name": "Inverter state"
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250516.0"]
|
||||
"requirements": ["home-assistant-frontend==20250509.0"]
|
||||
}
|
||||
|
||||
@@ -20,12 +20,9 @@ from homeassistant.helpers import config_entry_flow, config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
type GeofencyConfigEntry = ConfigEntry[set[str]]
|
||||
|
||||
PLATFORMS = [Platform.DEVICE_TRACKER]
|
||||
|
||||
CONF_MOBILE_BEACONS = "mobile_beacons"
|
||||
@@ -78,13 +75,16 @@ WEBHOOK_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
_DATA_GEOFENCY: HassKey[list[str]] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
|
||||
"""Set up the Geofency component."""
|
||||
mobile_beacons = hass_config.get(DOMAIN, {}).get(CONF_MOBILE_BEACONS, [])
|
||||
hass.data[_DATA_GEOFENCY] = [slugify(beacon) for beacon in mobile_beacons]
|
||||
config = hass_config.get(DOMAIN, {})
|
||||
mobile_beacons = config.get(CONF_MOBILE_BEACONS, [])
|
||||
hass.data[DOMAIN] = {
|
||||
"beacons": [slugify(beacon) for beacon in mobile_beacons],
|
||||
"devices": set(),
|
||||
"unsub_device_tracker": {},
|
||||
}
|
||||
return True
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ async def handle_webhook(
|
||||
text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY
|
||||
)
|
||||
|
||||
if _is_mobile_beacon(data, hass.data[_DATA_GEOFENCY]):
|
||||
if _is_mobile_beacon(data, hass.data[DOMAIN]["beacons"]):
|
||||
return _set_location(hass, data, None)
|
||||
if data["entry"] == LOCATION_ENTRY:
|
||||
location_name = data["name"]
|
||||
@@ -140,9 +140,8 @@ def _set_location(hass, data, location_name):
|
||||
return web.Response(text=f"Setting location for {device}")
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Configure based on config entry."""
|
||||
entry.runtime_data = set()
|
||||
webhook.async_register(
|
||||
hass, DOMAIN, "Geofency", entry.data[CONF_WEBHOOK_ID], handle_webhook
|
||||
)
|
||||
@@ -151,9 +150,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for the Geofency device tracker platform."""
|
||||
|
||||
from homeassistant.components.device_tracker import TrackerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@@ -9,13 +10,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import TRACKER_UPDATE, GeofencyConfigEntry
|
||||
from .const import DOMAIN
|
||||
from . import DOMAIN, TRACKER_UPDATE
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: GeofencyConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Geofency config entry."""
|
||||
@@ -23,16 +23,14 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def _receive_data(device, gps, location_name, attributes):
|
||||
"""Fire HA event to set location."""
|
||||
if device in config_entry.runtime_data:
|
||||
if device in hass.data[DOMAIN]["devices"]:
|
||||
return
|
||||
|
||||
config_entry.runtime_data.add(device)
|
||||
hass.data[DOMAIN]["devices"].add(device)
|
||||
|
||||
async_add_entities(
|
||||
[GeofencyEntity(config_entry, device, gps, location_name, attributes)]
|
||||
)
|
||||
async_add_entities([GeofencyEntity(device, gps, location_name, attributes)])
|
||||
|
||||
config_entry.async_on_unload(
|
||||
hass.data[DOMAIN]["unsub_device_tracker"][config_entry.entry_id] = (
|
||||
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
|
||||
)
|
||||
|
||||
@@ -47,8 +45,8 @@ async def async_setup_entry(
|
||||
}
|
||||
|
||||
if dev_ids:
|
||||
config_entry.runtime_data.update(dev_ids)
|
||||
async_add_entities(GeofencyEntity(config_entry, dev_id) for dev_id in dev_ids)
|
||||
hass.data[DOMAIN]["devices"].update(dev_ids)
|
||||
async_add_entities(GeofencyEntity(dev_id) for dev_id in dev_ids)
|
||||
|
||||
|
||||
class GeofencyEntity(TrackerEntity, RestoreEntity):
|
||||
@@ -57,9 +55,8 @@ class GeofencyEntity(TrackerEntity, RestoreEntity):
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, entry, device, gps=None, location_name=None, attributes=None):
|
||||
def __init__(self, device, gps=None, location_name=None, attributes=None):
|
||||
"""Set up Geofency entity."""
|
||||
self._entry = entry
|
||||
self._attr_extra_state_attributes = attributes or {}
|
||||
self._name = device
|
||||
self._attr_location_name = location_name
|
||||
@@ -96,7 +93,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity):
|
||||
"""Clean up after entity before removal."""
|
||||
await super().async_will_remove_from_hass()
|
||||
self._unsub_dispatcher()
|
||||
self._entry.runtime_data.remove(self.unique_id)
|
||||
self.hass.data[DOMAIN]["devices"].remove(self.unique_id)
|
||||
|
||||
@callback
|
||||
def _async_receive_data(self, device, gps, location_name, attributes):
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["go2rtc-client==0.1.3b0"],
|
||||
"requirements": ["go2rtc-client==0.1.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.4"]
|
||||
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_travel_time",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google", "homeassistant.helpers.location"],
|
||||
"requirements": ["google-maps-routing==0.6.15"]
|
||||
"requirements": ["google-maps-routing==0.6.14"]
|
||||
}
|
||||
|
||||
@@ -24,8 +24,6 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
type GPSLoggerConfigEntry = ConfigEntry[set[str]]
|
||||
|
||||
PLATFORMS = [Platform.DEVICE_TRACKER]
|
||||
|
||||
TRACKER_UPDATE = f"{DOMAIN}_tracker_update"
|
||||
@@ -90,9 +88,9 @@ async def handle_webhook(
|
||||
return web.Response(text=f"Setting location for {device}")
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: GPSLoggerConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Configure based on config entry."""
|
||||
entry.runtime_data = set()
|
||||
hass.data.setdefault(DOMAIN, {"devices": set(), "unsub_device_tracker": {}})
|
||||
webhook.async_register(
|
||||
hass, DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook
|
||||
)
|
||||
@@ -105,6 +103,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: GPSLoggerConfigEntry) ->
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
|
||||
hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for the GPSLogger device tracking."""
|
||||
|
||||
from homeassistant.components.device_tracker import TrackerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_BATTERY_LEVEL,
|
||||
ATTR_GPS_ACCURACY,
|
||||
@@ -14,20 +15,19 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import TRACKER_UPDATE, GPSLoggerConfigEntry
|
||||
from . import DOMAIN, TRACKER_UPDATE
|
||||
from .const import (
|
||||
ATTR_ACTIVITY,
|
||||
ATTR_ALTITUDE,
|
||||
ATTR_DIRECTION,
|
||||
ATTR_PROVIDER,
|
||||
ATTR_SPEED,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: GPSLoggerConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Configure a dispatcher connection based on a config entry."""
|
||||
@@ -35,14 +35,16 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def _receive_data(device, gps, battery, accuracy, attrs):
|
||||
"""Receive set location."""
|
||||
if device in entry.runtime_data:
|
||||
if device in hass.data[DOMAIN]["devices"]:
|
||||
return
|
||||
|
||||
entry.runtime_data.add(device)
|
||||
hass.data[DOMAIN]["devices"].add(device)
|
||||
|
||||
async_add_entities([GPSLoggerEntity(device, gps, battery, accuracy, attrs)])
|
||||
|
||||
entry.async_on_unload(async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data))
|
||||
hass.data[DOMAIN]["unsub_device_tracker"][entry.entry_id] = (
|
||||
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
|
||||
)
|
||||
|
||||
# Restore previously loaded devices
|
||||
dev_reg = dr.async_get(hass)
|
||||
@@ -56,7 +58,7 @@ async def async_setup_entry(
|
||||
|
||||
entities = []
|
||||
for dev_id in dev_ids:
|
||||
entry.runtime_data.add(dev_id)
|
||||
hass.data[DOMAIN]["devices"].add(dev_id)
|
||||
entity = GPSLoggerEntity(dev_id, None, None, None, None)
|
||||
entities.append(entity)
|
||||
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
"""The Gree Climate integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from .const import DISCOVERY_SCAN_INTERVAL
|
||||
from .coordinator import DiscoveryService, GreeConfigEntry, GreeRuntimeData
|
||||
from .const import (
|
||||
COORDINATORS,
|
||||
DATA_DISCOVERY_SERVICE,
|
||||
DISCOVERY_SCAN_INTERVAL,
|
||||
DISPATCHERS,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import DiscoveryService
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Gree Climate from a config entry."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
gree_discovery = DiscoveryService(hass, entry)
|
||||
entry.runtime_data = GreeRuntimeData(
|
||||
discovery_service=gree_discovery, coordinators=[]
|
||||
)
|
||||
hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery
|
||||
|
||||
async def _async_scan_update(_=None):
|
||||
bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass))
|
||||
@@ -43,6 +47,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if hass.data.get(DATA_DISCOVERY_SERVICE) is not None:
|
||||
hass.data.pop(DATA_DISCOVERY_SERVICE)
|
||||
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(COORDINATORS, None)
|
||||
hass.data[DOMAIN].pop(DISPATCHERS, None)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -36,18 +36,21 @@ from homeassistant.components.climate import (
|
||||
ClimateEntityFeature,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
COORDINATORS,
|
||||
DISPATCH_DEVICE_DISCOVERED,
|
||||
DOMAIN,
|
||||
FAN_MEDIUM_HIGH,
|
||||
FAN_MEDIUM_LOW,
|
||||
TARGET_TEMPERATURE_STEP,
|
||||
)
|
||||
from .coordinator import DeviceDataUpdateCoordinator, GreeConfigEntry
|
||||
from .coordinator import DeviceDataUpdateCoordinator
|
||||
from .entity import GreeEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -84,17 +87,17 @@ SWING_MODES = [SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH]
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: GreeConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Gree HVAC device from a config entry."""
|
||||
|
||||
@callback
|
||||
def init_device(coordinator: DeviceDataUpdateCoordinator) -> None:
|
||||
def init_device(coordinator):
|
||||
"""Register the device."""
|
||||
async_add_entities([GreeClimateEntity(coordinator)])
|
||||
|
||||
for coordinator in entry.runtime_data.coordinators:
|
||||
for coordinator in hass.data[DOMAIN][COORDINATORS]:
|
||||
init_device(coordinator)
|
||||
|
||||
entry.async_on_unload(
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
"""Constants for the Gree Climate integration."""
|
||||
|
||||
COORDINATORS = "coordinators"
|
||||
|
||||
DATA_DISCOVERY_SERVICE = "gree_discovery"
|
||||
|
||||
DISCOVERY_SCAN_INTERVAL = 300
|
||||
DISCOVERY_TIMEOUT = 8
|
||||
DISPATCH_DEVICE_DISCOVERED = "gree_device_discovered"
|
||||
DISPATCHERS = "dispatchers"
|
||||
|
||||
DOMAIN = "gree"
|
||||
COORDINATOR = "coordinator"
|
||||
|
||||
FAN_MEDIUM_LOW = "medium low"
|
||||
FAN_MEDIUM_HIGH = "medium high"
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
@@ -21,6 +20,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import (
|
||||
COORDINATORS,
|
||||
DISCOVERY_TIMEOUT,
|
||||
DISPATCH_DEVICE_DISCOVERED,
|
||||
DOMAIN,
|
||||
@@ -31,24 +31,14 @@ from .const import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type GreeConfigEntry = ConfigEntry[GreeRuntimeData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class GreeRuntimeData:
|
||||
"""RUntime data for Gree Climate integration."""
|
||||
|
||||
discovery_service: DiscoveryService
|
||||
coordinators: list[DeviceDataUpdateCoordinator]
|
||||
|
||||
|
||||
class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Manages polling for state changes from the device."""
|
||||
|
||||
config_entry: GreeConfigEntry
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: GreeConfigEntry, device: Device
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, device: Device
|
||||
) -> None:
|
||||
"""Initialize the data update coordinator."""
|
||||
super().__init__(
|
||||
@@ -138,7 +128,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
class DiscoveryService(Listener):
|
||||
"""Discovery event handler for gree devices."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: GreeConfigEntry) -> None:
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize discovery service."""
|
||||
super().__init__()
|
||||
self.hass = hass
|
||||
@@ -147,6 +137,8 @@ class DiscoveryService(Listener):
|
||||
self.discovery = Discovery(DISCOVERY_TIMEOUT)
|
||||
self.discovery.add_listener(self)
|
||||
|
||||
hass.data[DOMAIN].setdefault(COORDINATORS, [])
|
||||
|
||||
async def device_found(self, device_info: DeviceInfo) -> None:
|
||||
"""Handle new device found on the network."""
|
||||
|
||||
@@ -165,14 +157,14 @@ class DiscoveryService(Listener):
|
||||
device.device_info.port,
|
||||
)
|
||||
coordo = DeviceDataUpdateCoordinator(self.hass, self.entry, device)
|
||||
self.entry.runtime_data.coordinators.append(coordo)
|
||||
self.hass.data[DOMAIN][COORDINATORS].append(coordo)
|
||||
await coordo.async_refresh()
|
||||
|
||||
async_dispatcher_send(self.hass, DISPATCH_DEVICE_DISCOVERED, coordo)
|
||||
|
||||
async def device_update(self, device_info: DeviceInfo) -> None:
|
||||
"""Handle updates in device information, update if ip has changed."""
|
||||
for coordinator in self.entry.runtime_data.coordinators:
|
||||
for coordinator in self.hass.data[DOMAIN][COORDINATORS]:
|
||||
if coordinator.device.device_info.mac == device_info.mac:
|
||||
coordinator.device.device_info.ip = device_info.ip
|
||||
await coordinator.async_refresh()
|
||||
|
||||
@@ -13,13 +13,13 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DISPATCH_DEVICE_DISCOVERED
|
||||
from .coordinator import GreeConfigEntry
|
||||
from .entity import DeviceDataUpdateCoordinator, GreeEntity
|
||||
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DOMAIN
|
||||
from .entity import GreeEntity
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
@@ -92,13 +92,13 @@ GREE_SWITCHES: tuple[GreeSwitchEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: GreeConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Gree HVAC device from a config entry."""
|
||||
|
||||
@callback
|
||||
def init_device(coordinator: DeviceDataUpdateCoordinator) -> None:
|
||||
def init_device(coordinator):
|
||||
"""Register the device."""
|
||||
|
||||
async_add_entities(
|
||||
@@ -106,7 +106,7 @@ async def async_setup_entry(
|
||||
for description in GREE_SWITCHES
|
||||
)
|
||||
|
||||
for coordinator in entry.runtime_data.coordinators:
|
||||
for coordinator in hass.data[DOMAIN][COORDINATORS]:
|
||||
init_device(coordinator)
|
||||
|
||||
entry.async_on_unload(
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
"""Shared constants for the greeneye_monitor integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from greeneye import Monitors
|
||||
|
||||
CONF_CHANNELS = "channels"
|
||||
CONF_COUNTED_QUANTITY = "counted_quantity"
|
||||
CONF_COUNTED_QUANTITY_PER_PULSE = "counted_quantity_per_pulse"
|
||||
@@ -22,8 +13,8 @@ CONF_TEMPERATURE_SENSORS = "temperature_sensors"
|
||||
CONF_TIME_UNIT = "time_unit"
|
||||
CONF_VOLTAGE_SENSORS = "voltage"
|
||||
|
||||
DATA_GREENEYE_MONITOR = "greeneye_monitor"
|
||||
DOMAIN = "greeneye_monitor"
|
||||
DATA_GREENEYE_MONITOR: HassKey[Monitors] = HassKey(DOMAIN)
|
||||
|
||||
SENSOR_TYPE_CURRENT = "current_sensor"
|
||||
SENSOR_TYPE_PULSE_COUNTER = "pulse_counter"
|
||||
|
||||
@@ -109,7 +109,7 @@ async def async_setup_platform(
|
||||
if len(monitor_configs) == 0:
|
||||
monitors.remove_listener(on_new_monitor)
|
||||
|
||||
monitors = hass.data[DATA_GREENEYE_MONITOR]
|
||||
monitors: greeneye.Monitors = hass.data[DATA_GREENEYE_MONITOR]
|
||||
monitors.add_listener(on_new_monitor)
|
||||
for monitor in monitors.monitors.values():
|
||||
on_new_monitor(monitor)
|
||||
|
||||
@@ -52,10 +52,10 @@ type HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator]
|
||||
class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
|
||||
"""Habitica Data Update Coordinator."""
|
||||
|
||||
config_entry: HabiticaConfigEntry
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: HabiticaConfigEntry, habitica: Habitica
|
||||
self, hass: HomeAssistant, config_entry: ConfigEntry, habitica: Habitica
|
||||
) -> None:
|
||||
"""Initialize the Habitica data coordinator."""
|
||||
super().__init__(
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
"consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye",
|
||||
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye",
|
||||
"consumer_products_coffee_maker_program_beverage_hot_water": "Hot water",
|
||||
"dishcare_dishwasher_program_pre_rinse": "Pre-rinse",
|
||||
"dishcare_dishwasher_program_pre_rinse": "Pre_rinse",
|
||||
"dishcare_dishwasher_program_auto_1": "Auto 1",
|
||||
"dishcare_dishwasher_program_auto_2": "Auto 2",
|
||||
"dishcare_dishwasher_program_auto_3": "Auto 3",
|
||||
@@ -252,7 +252,7 @@
|
||||
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
|
||||
"dishcare_dishwasher_program_magic_daily": "Magic daily",
|
||||
"dishcare_dishwasher_program_super_60": "Super 60ºC",
|
||||
"dishcare_dishwasher_program_kurz_60": "Speed 60ºC",
|
||||
"dishcare_dishwasher_program_kurz_60": "Kurz 60ºC",
|
||||
"dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC",
|
||||
"dishcare_dishwasher_program_machine_care": "Machine care",
|
||||
"dishcare_dishwasher_program_steam_fresh": "Steam fresh",
|
||||
|
||||
@@ -90,17 +90,16 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
minor_version=2,
|
||||
)
|
||||
|
||||
if config_entry.minor_version <= 3:
|
||||
# Add a `firmware_version` key if it doesn't exist to handle entries created
|
||||
# with minor version 1.3 where the firmware version was not set.
|
||||
if config_entry.minor_version == 2:
|
||||
# Add a `firmware_version` key
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data={
|
||||
**config_entry.data,
|
||||
FIRMWARE_VERSION: config_entry.data.get(FIRMWARE_VERSION),
|
||||
FIRMWARE_VERSION: None,
|
||||
},
|
||||
version=1,
|
||||
minor_version=4,
|
||||
minor_version=3,
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -62,7 +62,7 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Home Assistant Yellow."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 4
|
||||
MINOR_VERSION = 3
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
"""Instantiate config flow."""
|
||||
@@ -116,11 +116,6 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
|
||||
if self._probed_firmware_info is not None
|
||||
else ApplicationType.EZSP
|
||||
).value,
|
||||
FIRMWARE_VERSION: (
|
||||
self._probed_firmware_info.firmware_version
|
||||
if self._probed_firmware_info is not None
|
||||
else None
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.ALARM_CONTROL_PANEL,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.CLIMATE,
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
"""The Homee alarm control panel platform."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pyHomee.const import AttributeChangedBy, AttributeType
|
||||
from pyHomee.model import HomeeAttribute
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntity,
|
||||
AlarmControlPanelEntityDescription,
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DOMAIN, HomeeConfigEntry
|
||||
from .entity import HomeeEntity
|
||||
from .helpers import get_name_for_enum
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HomeeAlarmControlPanelEntityDescription(AlarmControlPanelEntityDescription):
|
||||
"""A class that describes Homee alarm control panel entities."""
|
||||
|
||||
code_arm_required: bool = False
|
||||
state_list: list[AlarmControlPanelState]
|
||||
|
||||
|
||||
ALARM_DESCRIPTIONS = {
|
||||
AttributeType.HOMEE_MODE: HomeeAlarmControlPanelEntityDescription(
|
||||
key="homee_mode",
|
||||
code_arm_required=False,
|
||||
state_list=[
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
AlarmControlPanelState.ARMED_NIGHT,
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
AlarmControlPanelState.ARMED_VACATION,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def get_supported_features(
|
||||
state_list: list[AlarmControlPanelState],
|
||||
) -> AlarmControlPanelEntityFeature:
|
||||
"""Return supported features based on the state list."""
|
||||
supported_features = AlarmControlPanelEntityFeature(0)
|
||||
if AlarmControlPanelState.ARMED_HOME in state_list:
|
||||
supported_features |= AlarmControlPanelEntityFeature.ARM_HOME
|
||||
if AlarmControlPanelState.ARMED_AWAY in state_list:
|
||||
supported_features |= AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
if AlarmControlPanelState.ARMED_NIGHT in state_list:
|
||||
supported_features |= AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
if AlarmControlPanelState.ARMED_VACATION in state_list:
|
||||
supported_features |= AlarmControlPanelEntityFeature.ARM_VACATION
|
||||
return supported_features
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomeeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add the Homee platform for the alarm control panel component."""
|
||||
|
||||
async_add_entities(
|
||||
HomeeAlarmPanel(attribute, config_entry, ALARM_DESCRIPTIONS[attribute.type])
|
||||
for node in config_entry.runtime_data.nodes
|
||||
for attribute in node.attributes
|
||||
if attribute.type in ALARM_DESCRIPTIONS and attribute.editable
|
||||
)
|
||||
|
||||
|
||||
class HomeeAlarmPanel(HomeeEntity, AlarmControlPanelEntity):
|
||||
"""Representation of a Homee alarm control panel."""
|
||||
|
||||
entity_description: HomeeAlarmControlPanelEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attribute: HomeeAttribute,
|
||||
entry: HomeeConfigEntry,
|
||||
description: HomeeAlarmControlPanelEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a Homee alarm control panel entity."""
|
||||
super().__init__(attribute, entry)
|
||||
self.entity_description = description
|
||||
self._attr_code_arm_required = description.code_arm_required
|
||||
self._attr_supported_features = get_supported_features(description.state_list)
|
||||
self._attr_translation_key = description.key
|
||||
|
||||
@property
|
||||
def alarm_state(self) -> AlarmControlPanelState:
|
||||
"""Return current state."""
|
||||
return self.entity_description.state_list[int(self._attribute.current_value)]
|
||||
|
||||
@property
|
||||
def changed_by(self) -> str:
|
||||
"""Return by whom or what the entity was last changed."""
|
||||
changed_by_name = get_name_for_enum(
|
||||
AttributeChangedBy, self._attribute.changed_by
|
||||
)
|
||||
return f"{changed_by_name} - {self._attribute.changed_by_id}"
|
||||
|
||||
async def _async_set_alarm_state(self, state: AlarmControlPanelState) -> None:
|
||||
"""Set the alarm state."""
|
||||
if state in self.entity_description.state_list:
|
||||
await self.async_set_homee_value(
|
||||
self.entity_description.state_list.index(state)
|
||||
)
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
# Since disarm is always present in the UI, we raise an error.
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="disarm_not_supported",
|
||||
)
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_HOME)
|
||||
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_NIGHT)
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_AWAY)
|
||||
|
||||
async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
|
||||
"""Send arm vacation command."""
|
||||
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_VACATION)
|
||||
@@ -27,20 +27,14 @@ class HomeeEntity(Entity):
|
||||
)
|
||||
self._entry = entry
|
||||
node = entry.runtime_data.get_node_by_id(attribute.node_id)
|
||||
# Homee hub itself has node-id -1
|
||||
if node.id == -1:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.runtime_data.settings.uid)},
|
||||
)
|
||||
else:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}")
|
||||
},
|
||||
name=node.name,
|
||||
model=get_name_for_enum(NodeProfile, node.profile),
|
||||
via_device=(DOMAIN, entry.runtime_data.settings.uid),
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}")
|
||||
},
|
||||
name=node.name,
|
||||
model=get_name_for_enum(NodeProfile, node.profile),
|
||||
via_device=(DOMAIN, entry.runtime_data.settings.uid),
|
||||
)
|
||||
|
||||
self._host_connected = entry.runtime_data.connected
|
||||
|
||||
|
||||
@@ -26,11 +26,6 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"alarm_control_panel": {
|
||||
"homee_mode": {
|
||||
"name": "Status"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"blackout_alarm": {
|
||||
"name": "Blackout"
|
||||
@@ -375,9 +370,6 @@
|
||||
"connection_closed": {
|
||||
"message": "Could not connect to homee while setting attribute."
|
||||
},
|
||||
"disarm_not_supported": {
|
||||
"message": "Disarm is not supported by homee."
|
||||
},
|
||||
"invalid_preset_mode": {
|
||||
"message": "Invalid preset mode: {preset_mode}. Turning on is only supported with preset mode 'Manual'."
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import (
|
||||
@@ -20,7 +21,7 @@ from .const import (
|
||||
HMIPC_HAPID,
|
||||
HMIPC_NAME,
|
||||
)
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .hap import HomematicipHAP
|
||||
from .services import async_setup_services, async_unload_services
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
@@ -44,6 +45,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the HomematicIP Cloud component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
accesspoints = config.get(DOMAIN, [])
|
||||
|
||||
for conf in accesspoints:
|
||||
@@ -66,7 +69,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up an access point from a config entry."""
|
||||
|
||||
# 0.104 introduced config entry unique id, this makes upgrading possible
|
||||
@@ -78,8 +81,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
|
||||
)
|
||||
|
||||
hap = HomematicipHAP(hass, entry)
|
||||
hass.data[DOMAIN][entry.unique_id] = hap
|
||||
|
||||
entry.runtime_data = hap
|
||||
if not await hap.async_setup():
|
||||
return False
|
||||
|
||||
@@ -107,12 +110,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: HomematicIPConfigEntry
|
||||
) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
hap = entry.runtime_data
|
||||
assert hap.reset_connection_listener is not None
|
||||
hap = hass.data[DOMAIN].pop(entry.unique_id)
|
||||
hap.reset_connection_listener()
|
||||
|
||||
await async_unload_services(hass)
|
||||
@@ -122,7 +122,7 @@ async def async_unload_entry(
|
||||
|
||||
@callback
|
||||
def _async_remove_obsolete_entities(
|
||||
hass: HomeAssistant, entry: HomematicIPConfigEntry, hap: HomematicipHAP
|
||||
hass: HomeAssistant, entry: ConfigEntry, hap: HomematicipHAP
|
||||
):
|
||||
"""Remove obsolete entities from entity registry."""
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .hap import AsyncHome, HomematicIPConfigEntry, HomematicipHAP
|
||||
from .hap import AsyncHome, HomematicipHAP
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,11 +26,11 @@ CONST_ALARM_CONTROL_PANEL_NAME = "HmIP Alarm Control Panel"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomematicIPConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the HomematicIP alrm control panel from a config entry."""
|
||||
hap = config_entry.runtime_data
|
||||
hap = hass.data[DOMAIN][config_entry.unique_id]
|
||||
async_add_entities([HomematicipAlarmControlPanelEntity(hap)])
|
||||
|
||||
|
||||
|
||||
@@ -34,13 +34,14 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import HomematicipGenericEntity
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .hap import HomematicipHAP
|
||||
|
||||
ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode"
|
||||
ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position"
|
||||
@@ -74,11 +75,11 @@ SAM_DEVICE_ATTRIBUTES = {
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomematicIPConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
|
||||
hap = config_entry.runtime_data
|
||||
hap = hass.data[DOMAIN][config_entry.unique_id]
|
||||
entities: list[HomematicipGenericEntity] = [HomematicipCloudConnectionSensor(hap)]
|
||||
for device in hap.home.devices:
|
||||
if isinstance(device, AccelerationSensor):
|
||||
|
||||
@@ -5,20 +5,22 @@ from __future__ import annotations
|
||||
from homematicip.device import WallMountedGarageDoorController
|
||||
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import HomematicipGenericEntity
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .hap import HomematicipHAP
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomematicIPConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the HomematicIP button from a config entry."""
|
||||
hap = config_entry.runtime_data
|
||||
hap = hass.data[DOMAIN][config_entry.unique_id]
|
||||
|
||||
async_add_entities(
|
||||
HomematicipGarageDoorControllerButton(hap, device)
|
||||
|
||||
@@ -24,6 +24,7 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -31,7 +32,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import HomematicipGenericEntity
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .hap import HomematicipHAP
|
||||
|
||||
HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2}
|
||||
COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5}
|
||||
@@ -54,11 +55,11 @@ HMIP_ECO_CM = "ECO"
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomematicIPConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the HomematicIP climate from a config entry."""
|
||||
hap = config_entry.runtime_data
|
||||
hap = hass.data[DOMAIN][config_entry.unique_id]
|
||||
|
||||
async_add_entities(
|
||||
HomematicipHeatingGroup(hap, device)
|
||||
|
||||
@@ -21,11 +21,13 @@ from homeassistant.components.cover import (
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import HomematicipGenericEntity
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .hap import HomematicipHAP
|
||||
|
||||
HMIP_COVER_OPEN = 0
|
||||
HMIP_COVER_CLOSED = 1
|
||||
@@ -35,11 +37,11 @@ HMIP_SLATS_CLOSED = 1
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomematicIPConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the HomematicIP cover from a config entry."""
|
||||
hap = config_entry.runtime_data
|
||||
hap = hass.data[DOMAIN][config_entry.unique_id]
|
||||
entities: list[HomematicipGenericEntity] = [
|
||||
HomematicipCoverShutterGroup(hap, group)
|
||||
for group in hap.home.groups
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"""Support for HomematicIP Cloud events."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homematicip.base.channel_event import ChannelEvent
|
||||
from homematicip.base.functionalChannels import FunctionalChannel
|
||||
from homematicip.device import Device
|
||||
|
||||
from homeassistant.components.event import (
|
||||
@@ -13,20 +10,19 @@ from homeassistant.components.event import (
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import HomematicipGenericEntity
|
||||
from .hap import HomematicIPConfigEntry, HomematicipHAP
|
||||
from .hap import HomematicipHAP
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HmipEventEntityDescription(EventEntityDescription):
|
||||
"""Description of a HomematicIP Cloud event."""
|
||||
|
||||
channel_event_types: list[str] | None = None
|
||||
channel_selector_fn: Callable[[FunctionalChannel], bool] | None = None
|
||||
|
||||
|
||||
EVENT_DESCRIPTIONS = {
|
||||
"doorbell": HmipEventEntityDescription(
|
||||
@@ -34,42 +30,35 @@ EVENT_DESCRIPTIONS = {
|
||||
translation_key="doorbell",
|
||||
device_class=EventDeviceClass.DOORBELL,
|
||||
event_types=["ring"],
|
||||
channel_event_types=["DOOR_BELL_SENSOR_EVENT"],
|
||||
channel_selector_fn=lambda channel: channel.channelRole == "DOOR_BELL_INPUT",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HomematicIPConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the HomematicIP cover from a config entry."""
|
||||
hap = config_entry.runtime_data
|
||||
entities: list[HomematicipGenericEntity] = []
|
||||
hap = hass.data[DOMAIN][config_entry.unique_id]
|
||||
|
||||
entities.extend(
|
||||
async_add_entities(
|
||||
HomematicipDoorBellEvent(
|
||||
hap,
|
||||
device,
|
||||
channel.index,
|
||||
description,
|
||||
EVENT_DESCRIPTIONS["doorbell"],
|
||||
)
|
||||
for description in EVENT_DESCRIPTIONS.values()
|
||||
for device in hap.home.devices
|
||||
for channel in device.functionalChannels
|
||||
if description.channel_selector_fn and description.channel_selector_fn(channel)
|
||||
if channel.channelRole == "DOOR_BELL_INPUT"
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
|
||||
"""Event class for HomematicIP doorbell events."""
|
||||
|
||||
_attr_device_class = EventDeviceClass.DOORBELL
|
||||
entity_description: HmipEventEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -97,27 +86,9 @@ class HomematicipDoorBellEvent(HomematicipGenericEntity, EventEntity):
|
||||
@callback
|
||||
def _async_handle_event(self, *args, **kwargs) -> None:
|
||||
"""Handle the event fired by the functional channel."""
|
||||
raised_channel_event = self._get_channel_event_from_args(*args)
|
||||
|
||||
if not self._should_raise(raised_channel_event):
|
||||
return
|
||||
|
||||
event_types = self.entity_description.event_types
|
||||
if TYPE_CHECKING:
|
||||
assert event_types is not None
|
||||
|
||||
self._trigger_event(event_type=event_types[0])
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _should_raise(self, event_type: str) -> bool:
|
||||
"""Check if the event should be raised."""
|
||||
if self.entity_description.channel_event_types is None:
|
||||
return False
|
||||
return event_type in self.entity_description.channel_event_types
|
||||
|
||||
def _get_channel_event_from_args(self, *args) -> str:
|
||||
"""Get the channel event."""
|
||||
if isinstance(args[0], ChannelEvent):
|
||||
return args[0].channelEventType
|
||||
|
||||
return ""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user