forked from home-assistant/core
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7411e54b2c | |||
| ed9503324d | |||
| 22a06a6c2e | |||
| 3b611b9b03 | |||
| 79cc3bffc6 | |||
| 5c455304a5 | |||
| 058f860be7 | |||
| ef319c966d | |||
| adc4e9fdc1 | |||
| 40a00fb790 | |||
| 0926b16095 | |||
| 308c89af4a | |||
| b0c2a47288 | |||
| c446cce2cc | |||
| e02267ad89 | |||
| 36381e6753 | |||
| 6533562f4e | |||
| 1bc6ea98ce | |||
| bab34b844b | |||
| ad3dac0373 | |||
| c5d93e5456 | |||
| ef9b46dce5 | |||
| 6f3ceb83c2 | |||
| 589577a04c | |||
| cb21bb6542 | |||
| ad64139b8e | |||
| 9ae0cfc7e5 | |||
| dffaf49eca | |||
| 4add783108 | |||
| 421251308f | |||
| cce878213f | |||
| 664441eaec | |||
| d4686a3cce | |||
| 6e92247799 | |||
| f5355c833e | |||
| add9f4c5ab | |||
| 38973fe64a | |||
| d657964729 | |||
| 25c408484c | |||
| c335b5b37c | |||
| 61b00892c3 | |||
| e47e2c92fe | |||
| 3283965b45 | |||
| 4a9cbc79f2 | |||
| 33978ce59e | |||
| d5262231a1 | |||
| b563f9078a | |||
| e8667dfbe0 | |||
| 8d4f5d78ff | |||
| e354a850c9 | |||
| 5ea026d369 | |||
| ddfe17d0a4 | |||
| 85aa7bef1e | |||
| 8498928e47 |
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/intents-package
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfApparentPower,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
@@ -35,6 +36,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"alarmdel": SensorEntityDescription(
|
||||
key="alarmdel",
|
||||
translation_key="alarm_delay",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"ambtemp": SensorEntityDescription(
|
||||
key="ambtemp",
|
||||
@@ -47,15 +49,18 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="apc",
|
||||
translation_key="apc_status",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"apcmodel": SensorEntityDescription(
|
||||
key="apcmodel",
|
||||
translation_key="apc_model",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"badbatts": SensorEntityDescription(
|
||||
key="badbatts",
|
||||
translation_key="bad_batteries",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"battdate": SensorEntityDescription(
|
||||
key="battdate",
|
||||
@@ -82,6 +87,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="cable",
|
||||
translation_key="cable_type",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"cumonbatt": SensorEntityDescription(
|
||||
key="cumonbatt",
|
||||
@@ -94,52 +100,63 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="date",
|
||||
translation_key="date",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"dipsw": SensorEntityDescription(
|
||||
key="dipsw",
|
||||
translation_key="dip_switch_settings",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"dlowbatt": SensorEntityDescription(
|
||||
key="dlowbatt",
|
||||
translation_key="low_battery_signal",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"driver": SensorEntityDescription(
|
||||
key="driver",
|
||||
translation_key="driver",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"dshutd": SensorEntityDescription(
|
||||
key="dshutd",
|
||||
translation_key="shutdown_delay",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"dwake": SensorEntityDescription(
|
||||
key="dwake",
|
||||
translation_key="wake_delay",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"end apc": SensorEntityDescription(
|
||||
key="end apc",
|
||||
translation_key="date_and_time",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"extbatts": SensorEntityDescription(
|
||||
key="extbatts",
|
||||
translation_key="external_batteries",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"firmware": SensorEntityDescription(
|
||||
key="firmware",
|
||||
translation_key="firmware_version",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"hitrans": SensorEntityDescription(
|
||||
key="hitrans",
|
||||
translation_key="transfer_high",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"hostname": SensorEntityDescription(
|
||||
key="hostname",
|
||||
translation_key="hostname",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"humidity": SensorEntityDescription(
|
||||
key="humidity",
|
||||
@@ -163,10 +180,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="lastxfer",
|
||||
translation_key="last_transfer",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"linefail": SensorEntityDescription(
|
||||
key="linefail",
|
||||
translation_key="line_failure",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"linefreq": SensorEntityDescription(
|
||||
key="linefreq",
|
||||
@@ -198,15 +217,18 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
translation_key="transfer_low",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"mandate": SensorEntityDescription(
|
||||
key="mandate",
|
||||
translation_key="manufacture_date",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"masterupd": SensorEntityDescription(
|
||||
key="masterupd",
|
||||
translation_key="master_update",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"maxlinev": SensorEntityDescription(
|
||||
key="maxlinev",
|
||||
@@ -217,11 +239,13 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"maxtime": SensorEntityDescription(
|
||||
key="maxtime",
|
||||
translation_key="max_time",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"mbattchg": SensorEntityDescription(
|
||||
key="mbattchg",
|
||||
translation_key="max_battery_charge",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"minlinev": SensorEntityDescription(
|
||||
key="minlinev",
|
||||
@@ -232,41 +256,48 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"mintimel": SensorEntityDescription(
|
||||
key="mintimel",
|
||||
translation_key="min_time",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"model": SensorEntityDescription(
|
||||
key="model",
|
||||
translation_key="model",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nombattv": SensorEntityDescription(
|
||||
key="nombattv",
|
||||
translation_key="battery_nominal_voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nominv": SensorEntityDescription(
|
||||
key="nominv",
|
||||
translation_key="nominal_input_voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nomoutv": SensorEntityDescription(
|
||||
key="nomoutv",
|
||||
translation_key="nominal_output_voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nompower": SensorEntityDescription(
|
||||
key="nompower",
|
||||
translation_key="nominal_output_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nomapnt": SensorEntityDescription(
|
||||
key="nomapnt",
|
||||
translation_key="nominal_apparent_power",
|
||||
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
|
||||
device_class=SensorDeviceClass.APPARENT_POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"numxfers": SensorEntityDescription(
|
||||
key="numxfers",
|
||||
@@ -291,21 +322,25 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="reg1",
|
||||
translation_key="register_1_fault",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"reg2": SensorEntityDescription(
|
||||
key="reg2",
|
||||
translation_key="register_2_fault",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"reg3": SensorEntityDescription(
|
||||
key="reg3",
|
||||
translation_key="register_3_fault",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"retpct": SensorEntityDescription(
|
||||
key="retpct",
|
||||
translation_key="restore_capacity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"selftest": SensorEntityDescription(
|
||||
key="selftest",
|
||||
@@ -315,20 +350,24 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="sense",
|
||||
translation_key="sensitivity",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"serialno": SensorEntityDescription(
|
||||
key="serialno",
|
||||
translation_key="serial_number",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"starttime": SensorEntityDescription(
|
||||
key="starttime",
|
||||
translation_key="startup_time",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"statflag": SensorEntityDescription(
|
||||
key="statflag",
|
||||
translation_key="online_status",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"status": SensorEntityDescription(
|
||||
key="status",
|
||||
@@ -337,6 +376,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"stesti": SensorEntityDescription(
|
||||
key="stesti",
|
||||
translation_key="self_test_interval",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"timeleft": SensorEntityDescription(
|
||||
key="timeleft",
|
||||
@@ -360,23 +400,28 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="upsname",
|
||||
translation_key="ups_name",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"version": SensorEntityDescription(
|
||||
key="version",
|
||||
translation_key="version",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"xoffbat": SensorEntityDescription(
|
||||
key="xoffbat",
|
||||
translation_key="transfer_from_battery",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"xoffbatt": SensorEntityDescription(
|
||||
key="xoffbatt",
|
||||
translation_key="transfer_from_battery",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"xonbatt": SensorEntityDescription(
|
||||
key="xonbatt",
|
||||
translation_key="transfer_to_battery",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==3.12.4"]
|
||||
"requirements": ["bthome-ble==3.13.1"]
|
||||
}
|
||||
|
||||
@@ -300,10 +300,6 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
def supported_features(self) -> CoverEntityFeature:
|
||||
"""Flag supported features."""
|
||||
if (features := self._attr_supported_features) is not None:
|
||||
if type(features) is int:
|
||||
new_features = CoverEntityFeature(features)
|
||||
self._report_deprecated_supported_features_values(new_features)
|
||||
return new_features
|
||||
return features
|
||||
|
||||
supported_features = (
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["discord"],
|
||||
"requirements": ["nextcord==2.6.0"]
|
||||
"requirements": ["nextcord==3.1.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sml"],
|
||||
"requirements": ["pysml==0.0.12"]
|
||||
"requirements": ["pysml==0.1.5"]
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="timestamp",
|
||||
translation_key="timestamp",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"]
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.4"]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -133,8 +133,8 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
def _parse_routing_response(self, response: dict[str, Any]) -> HERETravelTimeData:
|
||||
"""Parse the routing response dict to a HERETravelTimeData."""
|
||||
distance: float = 0.0
|
||||
duration: float = 0.0
|
||||
duration_in_traffic: float = 0.0
|
||||
duration: int = 0
|
||||
duration_in_traffic: int = 0
|
||||
|
||||
for section in response["routes"][0]["sections"]:
|
||||
distance += DistanceConverter.convert(
|
||||
@@ -167,8 +167,8 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
destination_name = names[0]["value"]
|
||||
return HERETravelTimeData(
|
||||
attribution=None,
|
||||
duration=round(duration / 60),
|
||||
duration_in_traffic=round(duration_in_traffic / 60),
|
||||
duration=duration,
|
||||
duration_in_traffic=duration_in_traffic,
|
||||
distance=distance,
|
||||
origin=f"{mapped_origin_lat},{mapped_origin_lon}",
|
||||
destination=f"{mapped_destination_lat},{mapped_destination_lon}",
|
||||
@@ -271,13 +271,13 @@ class HERETransitDataUpdateCoordinator(
|
||||
UnitOfLength.METERS,
|
||||
UnitOfLength.KILOMETERS,
|
||||
)
|
||||
duration: float = sum(
|
||||
duration: int = sum(
|
||||
section["travelSummary"]["duration"] for section in sections
|
||||
)
|
||||
return HERETravelTimeData(
|
||||
attribution=attribution,
|
||||
duration=round(duration / 60),
|
||||
duration_in_traffic=round(duration / 60),
|
||||
duration=duration,
|
||||
duration_in_traffic=duration,
|
||||
distance=distance,
|
||||
origin=f"{mapped_origin_lat},{mapped_origin_lon}",
|
||||
destination=f"{mapped_destination_lat},{mapped_destination_lon}",
|
||||
|
||||
@@ -55,14 +55,18 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...]
|
||||
icon=ICONS.get(travel_mode, ICON_CAR),
|
||||
key=ATTR_DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
translation_key="duration_in_traffic",
|
||||
icon=ICONS.get(travel_mode, ICON_CAR),
|
||||
key=ATTR_DURATION_IN_TRAFFIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
translation_key="distance",
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling:
|
||||
status: done
|
||||
comment: |
|
||||
Full polling is performed at the configuration entry setup and
|
||||
device polling is performed when a CONNECTED or a PAIRED event is received.
|
||||
If many CONNECTED or PAIRED events are received for a device within a short time span,
|
||||
the integration will stop polling for that device and will create a repair issue.
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: done
|
||||
comment: |
|
||||
Event entities are disabled by default to prevent user confusion regarding
|
||||
which events are supported by its appliance.
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration doesn't have settings in its configuration flow.
|
||||
repair-issues: done
|
||||
stale-devices: done
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -23,9 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -
|
||||
|
||||
api: HomeWizardEnergy
|
||||
|
||||
is_battery = entry.unique_id.startswith("HWE-BAT") if entry.unique_id else False
|
||||
|
||||
if (token := entry.data.get(CONF_TOKEN)) and is_battery:
|
||||
if token := entry.data.get(CONF_TOKEN):
|
||||
api = HomeWizardEnergyV2(
|
||||
entry.data[CONF_IP_ADDRESS],
|
||||
token=token,
|
||||
@@ -37,8 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -
|
||||
clientsession=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
if is_battery:
|
||||
await async_check_v2_support_and_create_issue(hass, entry)
|
||||
await async_check_v2_support_and_create_issue(hass, entry)
|
||||
|
||||
coordinator = HWEnergyDeviceUpdateCoordinator(hass, entry, api)
|
||||
try:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2025.5.1"]
|
||||
"requirements": ["aioautomower==2025.6.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==10.0.4"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==10.0.4"]
|
||||
}
|
||||
|
||||
@@ -1,93 +1,28 @@
|
||||
"""The Meater Temperature Probe integration."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from meater import (
|
||||
AuthenticationError,
|
||||
MeaterApi,
|
||||
ServiceUnavailableError,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
from meater.MeaterApi import MeaterProbe
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MeaterConfigEntry, MeaterCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MeaterConfigEntry) -> bool:
|
||||
"""Set up Meater Temperature Probe from a config entry."""
|
||||
# Store an API object to access
|
||||
session = async_get_clientsession(hass)
|
||||
meater_api = MeaterApi(session)
|
||||
|
||||
# Add the credentials
|
||||
try:
|
||||
_LOGGER.debug("Authenticating with the Meater API")
|
||||
await meater_api.authenticate(
|
||||
entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
|
||||
)
|
||||
except (ServiceUnavailableError, TooManyRequestsError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
except AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Unable to authenticate with the Meater API: {err}"
|
||||
) from err
|
||||
|
||||
async def async_update_data() -> dict[str, MeaterProbe]:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
# Note: TimeoutError and aiohttp.ClientError are already
|
||||
# handled by the data update coordinator.
|
||||
async with asyncio.timeout(10):
|
||||
devices: list[MeaterProbe] = await meater_api.get_all_devices()
|
||||
except AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed("The API call wasn't authenticated") from err
|
||||
except TooManyRequestsError as err:
|
||||
raise UpdateFailed(
|
||||
"Too many requests have been made to the API, rate limiting is in place"
|
||||
) from err
|
||||
|
||||
return {device.id: device for device in devices}
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
# Name of the data. For logging purposes.
|
||||
name="meater_api",
|
||||
update_method=async_update_data,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
coordinator = MeaterCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN].setdefault("known_probes", set())
|
||||
hass.data.setdefault(DOMAIN, {}).setdefault("known_probes", set())
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
"api": meater_api,
|
||||
"coordinator": coordinator,
|
||||
}
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MeaterConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Meater Coordinator."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from meater.MeaterApi import (
|
||||
AuthenticationError,
|
||||
MeaterApi,
|
||||
MeaterProbe,
|
||||
ServiceUnavailableError,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MeaterConfigEntry = ConfigEntry[MeaterCoordinator]
|
||||
|
||||
|
||||
class MeaterCoordinator(DataUpdateCoordinator[dict[str, MeaterProbe]]):
|
||||
"""Meater Coordinator."""
|
||||
|
||||
config_entry: MeaterConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: MeaterConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the Meater Coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"Meater {entry.title}",
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
session = async_get_clientsession(hass)
|
||||
self.client = MeaterApi(session)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the Meater Coordinator."""
|
||||
try:
|
||||
_LOGGER.debug("Authenticating with the Meater API")
|
||||
await self.client.authenticate(
|
||||
self.config_entry.data[CONF_USERNAME],
|
||||
self.config_entry.data[CONF_PASSWORD],
|
||||
)
|
||||
except (ServiceUnavailableError, TooManyRequestsError) as err:
|
||||
raise UpdateFailed from err
|
||||
except AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Unable to authenticate with the Meater API: {err}"
|
||||
) from err
|
||||
|
||||
async def _async_update_data(self) -> dict[str, MeaterProbe]:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
# Note: TimeoutError and aiohttp.ClientError are already
|
||||
# handled by the data update coordinator.
|
||||
async with asyncio.timeout(10):
|
||||
devices: list[MeaterProbe] = await self.client.get_all_devices()
|
||||
except AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed("The API call wasn't authenticated") from err
|
||||
except TooManyRequestsError as err:
|
||||
raise UpdateFailed(
|
||||
"Too many requests have been made to the API, rate limiting is in place"
|
||||
) from err
|
||||
|
||||
return {device.id: device for device in devices}
|
||||
@@ -0,0 +1,55 @@
|
||||
"""Diagnostics support for the Meater integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import MeaterConfigEntry
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: MeaterConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
return {
|
||||
identifier: {
|
||||
"id": probe.id,
|
||||
"internal_temperature": probe.internal_temperature,
|
||||
"ambient_temperature": probe.ambient_temperature,
|
||||
"time_updated": probe.time_updated.isoformat(),
|
||||
"cook": (
|
||||
{
|
||||
"id": probe.cook.id,
|
||||
"name": probe.cook.name,
|
||||
"state": probe.cook.state,
|
||||
"target_temperature": (
|
||||
probe.cook.target_temperature
|
||||
if hasattr(probe.cook, "target_temperature")
|
||||
else None
|
||||
),
|
||||
"peak_temperature": (
|
||||
probe.cook.peak_temperature
|
||||
if hasattr(probe.cook, "peak_temperature")
|
||||
else None
|
||||
),
|
||||
"time_remaining": (
|
||||
probe.cook.time_remaining
|
||||
if hasattr(probe.cook, "time_remaining")
|
||||
else None
|
||||
),
|
||||
"time_elapsed": (
|
||||
probe.cook.time_elapsed
|
||||
if hasattr(probe.cook, "time_elapsed")
|
||||
else None
|
||||
),
|
||||
}
|
||||
if probe.cook
|
||||
else None
|
||||
),
|
||||
}
|
||||
for identifier, probe in coordinator.data.items()
|
||||
}
|
||||
@@ -14,18 +14,28 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import MeaterCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MeaterConfigEntry
|
||||
|
||||
COOK_STATES = {
|
||||
"Not Started": "not_started",
|
||||
"Configured": "configured",
|
||||
"Started": "started",
|
||||
"Ready For Resting": "ready_for_resting",
|
||||
"Resting": "resting",
|
||||
"Slightly Underdone": "slightly_underdone",
|
||||
"Finished": "finished",
|
||||
"Slightly Overdone": "slightly_overdone",
|
||||
"OVERCOOK!": "overcooked",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -82,13 +92,13 @@ SENSOR_TYPES = (
|
||||
available=lambda probe: probe is not None and probe.cook is not None,
|
||||
value=lambda probe: probe.cook.name if probe.cook else None,
|
||||
),
|
||||
# One of Not Started, Configured, Started, Ready For Resting, Resting,
|
||||
# Slightly Underdone, Finished, Slightly Overdone, OVERCOOK!. Not translated.
|
||||
MeaterSensorEntityDescription(
|
||||
key="cook_state",
|
||||
translation_key="cook_state",
|
||||
available=lambda probe: probe is not None and probe.cook is not None,
|
||||
value=lambda probe: probe.cook.state if probe.cook else None,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=list(COOK_STATES.values()),
|
||||
value=lambda probe: COOK_STATES.get(probe.cook.state) if probe.cook else None,
|
||||
),
|
||||
# Target temperature
|
||||
MeaterSensorEntityDescription(
|
||||
@@ -137,13 +147,11 @@ SENSOR_TYPES = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MeaterConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the entry."""
|
||||
coordinator: DataUpdateCoordinator[dict[str, MeaterProbe]] = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]["coordinator"]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
@callback
|
||||
def async_update_data():
|
||||
@@ -174,11 +182,10 @@ async def async_setup_entry(
|
||||
|
||||
# Add a subscriber to the coordinator to discover new temperature probes
|
||||
coordinator.async_add_listener(async_update_data)
|
||||
async_update_data()
|
||||
|
||||
|
||||
class MeaterProbeTemperature(
|
||||
SensorEntity, CoordinatorEntity[DataUpdateCoordinator[dict[str, MeaterProbe]]]
|
||||
):
|
||||
class MeaterProbeTemperature(SensorEntity, CoordinatorEntity[MeaterCoordinator]):
|
||||
"""Meater Temperature Sensor Entity."""
|
||||
|
||||
entity_description: MeaterSensorEntityDescription
|
||||
|
||||
@@ -40,7 +40,18 @@
|
||||
"name": "Cooking"
|
||||
},
|
||||
"cook_state": {
|
||||
"name": "Cook state"
|
||||
"name": "Cook state",
|
||||
"state": {
|
||||
"not_started": "Not started",
|
||||
"configured": "Configured",
|
||||
"started": "Started",
|
||||
"ready_for_resting": "Ready for resting",
|
||||
"resting": "Resting",
|
||||
"slightly_underdone": "Slightly underdone",
|
||||
"finished": "Finished",
|
||||
"slightly_overdone": "Slightly overdone",
|
||||
"overcooked": "Overcooked"
|
||||
}
|
||||
},
|
||||
"cook_target_temp": {
|
||||
"name": "Target temperature"
|
||||
|
||||
@@ -814,19 +814,6 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Flag media player features that are supported."""
|
||||
return self._attr_supported_features
|
||||
|
||||
@property
|
||||
def supported_features_compat(self) -> MediaPlayerEntityFeature:
|
||||
"""Return the supported features as MediaPlayerEntityFeature.
|
||||
|
||||
Remove this compatibility shim in 2025.1 or later.
|
||||
"""
|
||||
features = self.supported_features
|
||||
if type(features) is int:
|
||||
new_features = MediaPlayerEntityFeature(features)
|
||||
self._report_deprecated_supported_features_values(new_features)
|
||||
return new_features
|
||||
return features
|
||||
|
||||
def turn_on(self) -> None:
|
||||
"""Turn the media player on."""
|
||||
raise NotImplementedError
|
||||
@@ -966,87 +953,85 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
@property
|
||||
def support_play(self) -> bool:
|
||||
"""Boolean if play is supported."""
|
||||
return MediaPlayerEntityFeature.PLAY in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PLAY in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_pause(self) -> bool:
|
||||
"""Boolean if pause is supported."""
|
||||
return MediaPlayerEntityFeature.PAUSE in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PAUSE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_stop(self) -> bool:
|
||||
"""Boolean if stop is supported."""
|
||||
return MediaPlayerEntityFeature.STOP in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.STOP in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_seek(self) -> bool:
|
||||
"""Boolean if seek is supported."""
|
||||
return MediaPlayerEntityFeature.SEEK in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.SEEK in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_volume_set(self) -> bool:
|
||||
"""Boolean if setting volume is supported."""
|
||||
return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_volume_mute(self) -> bool:
|
||||
"""Boolean if muting volume is supported."""
|
||||
return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_previous_track(self) -> bool:
|
||||
"""Boolean if previous track command supported."""
|
||||
return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_next_track(self) -> bool:
|
||||
"""Boolean if next track command supported."""
|
||||
return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_play_media(self) -> bool:
|
||||
"""Boolean if play media command supported."""
|
||||
return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_select_source(self) -> bool:
|
||||
"""Boolean if select source command supported."""
|
||||
return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_select_sound_mode(self) -> bool:
|
||||
"""Boolean if select sound mode command supported."""
|
||||
return (
|
||||
MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features_compat
|
||||
)
|
||||
return MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_clear_playlist(self) -> bool:
|
||||
"""Boolean if clear playlist command supported."""
|
||||
return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_shuffle_set(self) -> bool:
|
||||
"""Boolean if shuffle is supported."""
|
||||
return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_grouping(self) -> bool:
|
||||
"""Boolean if player grouping is supported."""
|
||||
return MediaPlayerEntityFeature.GROUPING in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.GROUPING in self.supported_features
|
||||
|
||||
async def async_toggle(self) -> None:
|
||||
"""Toggle the power on the media player."""
|
||||
@@ -1074,7 +1059,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if (
|
||||
self.volume_level is not None
|
||||
and self.volume_level < 1
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
|
||||
):
|
||||
await self.async_set_volume_level(
|
||||
min(1, self.volume_level + self.volume_step)
|
||||
@@ -1092,7 +1077,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if (
|
||||
self.volume_level is not None
|
||||
and self.volume_level > 0
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
|
||||
):
|
||||
await self.async_set_volume_level(
|
||||
max(0, self.volume_level - self.volume_step)
|
||||
@@ -1135,7 +1120,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
def capability_attributes(self) -> dict[str, Any]:
|
||||
"""Return capability attributes."""
|
||||
data: dict[str, Any] = {}
|
||||
supported_features = self.supported_features_compat
|
||||
supported_features = self.supported_features
|
||||
|
||||
if (
|
||||
source_list := self.source_list
|
||||
@@ -1364,7 +1349,7 @@ async def websocket_browse_media(
|
||||
connection.send_error(msg["id"], "entity_not_found", "Entity not found")
|
||||
return
|
||||
|
||||
if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features_compat:
|
||||
if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features:
|
||||
connection.send_message(
|
||||
websocket_api.error_message(
|
||||
msg["id"], ERR_NOT_SUPPORTED, "Player does not support browsing media"
|
||||
@@ -1447,7 +1432,7 @@ async def websocket_search_media(
|
||||
connection.send_error(msg["id"], "entity_not_found", "Entity not found")
|
||||
return
|
||||
|
||||
if MediaPlayerEntityFeature.SEARCH_MEDIA not in player.supported_features_compat:
|
||||
if MediaPlayerEntityFeature.SEARCH_MEDIA not in player.supported_features:
|
||||
connection.send_message(
|
||||
websocket_api.error_message(
|
||||
msg["id"], ERR_NOT_SUPPORTED, "Player does not support searching media"
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import dns.asyncresolver
|
||||
import dns.rdata
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
@@ -22,20 +23,23 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_dnspython_rdata_classes() -> None:
|
||||
"""Load dnspython rdata classes used by mcstatus."""
|
||||
def prevent_dnspython_blocking_operations() -> None:
|
||||
"""Prevent dnspython blocking operations by pre-loading required data."""
|
||||
|
||||
# Blocking import: https://github.com/rthalley/dnspython/issues/1083
|
||||
for rdtype in dns.rdatatype.RdataType:
|
||||
if not dns.rdatatype.is_metatype(rdtype) or rdtype == dns.rdatatype.OPT:
|
||||
dns.rdata.get_rdata_class(dns.rdataclass.IN, rdtype) # type: ignore[no-untyped-call]
|
||||
|
||||
# Blocking open: https://github.com/rthalley/dnspython/issues/1200
|
||||
dns.asyncresolver.get_default_resolver()
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: MinecraftServerConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Minecraft Server from a config entry."""
|
||||
|
||||
# Workaround to avoid blocking imports from dnspython (https://github.com/rthalley/dnspython/issues/1083)
|
||||
await hass.async_add_executor_job(load_dnspython_rdata_classes)
|
||||
await hass.async_add_executor_job(prevent_dnspython_blocking_operations)
|
||||
|
||||
# Create coordinator instance and store it.
|
||||
coordinator = MinecraftServerCoordinator(hass, entry)
|
||||
|
||||
@@ -138,12 +138,9 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
|
||||
device_class in DEVICE_CLASS_UNITS
|
||||
and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class]
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"The unit of measurement `%s` is not valid "
|
||||
"together with device class `%s`. "
|
||||
"this will stop working in HA Core 2025.7.0",
|
||||
unit_of_measurement,
|
||||
device_class,
|
||||
raise vol.Invalid(
|
||||
f"The unit of measurement '{unit_of_measurement}' is not valid "
|
||||
f"together with device class '{device_class}'"
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mysensors",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["mysensors"],
|
||||
"requirements": ["pymysensors==0.24.0"]
|
||||
"requirements": ["pymysensors==0.25.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["nessclient"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["nessclient==1.1.2"]
|
||||
"requirements": ["nessclient==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -13,14 +13,13 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NextDnsConfigEntry
|
||||
from .coordinator import NextDnsUpdateCoordinator
|
||||
from .entity import NextDnsEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -61,30 +60,14 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class NextDnsBinarySensor(
|
||||
CoordinatorEntity[NextDnsUpdateCoordinator[ConnectionStatus]], BinarySensorEntity
|
||||
):
|
||||
class NextDnsBinarySensor(NextDnsEntity, BinarySensorEntity):
|
||||
"""Define an NextDNS binary sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: NextDnsBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NextDnsUpdateCoordinator[ConnectionStatus],
|
||||
description: NextDnsBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = coordinator.device_info
|
||||
self._attr_unique_id = f"{coordinator.profile_id}_{description.key}"
|
||||
self._attr_is_on = description.state(coordinator.data, coordinator.profile_id)
|
||||
self.entity_description = description
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._attr_is_on = self.entity_description.state(
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self.entity_description.state(
|
||||
self.coordinator.data, self.coordinator.profile_id
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -4,21 +4,21 @@ from __future__ import annotations
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from nextdns import AnalyticsStatus, ApiError, InvalidApiKeyError
|
||||
from nextdns import ApiError, InvalidApiKeyError
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NextDnsConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NextDnsUpdateCoordinator
|
||||
from .entity import NextDnsEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
CLEAR_LOGS_BUTTON = ButtonEntityDescription(
|
||||
key="clear_logs",
|
||||
translation_key="clear_logs",
|
||||
@@ -37,24 +37,9 @@ async def async_setup_entry(
|
||||
async_add_entities([NextDnsButton(coordinator, CLEAR_LOGS_BUTTON)])
|
||||
|
||||
|
||||
class NextDnsButton(
|
||||
CoordinatorEntity[NextDnsUpdateCoordinator[AnalyticsStatus]], ButtonEntity
|
||||
):
|
||||
class NextDnsButton(NextDnsEntity, ButtonEntity):
|
||||
"""Define an NextDNS button."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NextDnsUpdateCoordinator[AnalyticsStatus],
|
||||
description: ButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = coordinator.device_info
|
||||
self._attr_unique_id = f"{coordinator.profile_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Trigger cleaning logs."""
|
||||
try:
|
||||
|
||||
@@ -24,7 +24,6 @@ from tenacity import RetryError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -53,14 +52,6 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]):
|
||||
"""Initialize."""
|
||||
self.nextdns = nextdns
|
||||
self.profile_id = profile_id
|
||||
self.profile_name = nextdns.get_profile_name(profile_id)
|
||||
self.device_info = DeviceInfo(
|
||||
configuration_url=f"https://my.nextdns.io/{profile_id}/setup",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, str(profile_id))},
|
||||
manufacturer="NextDNS Inc.",
|
||||
name=self.profile_name,
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Define NextDNS entities."""
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CoordinatorDataT, NextDnsUpdateCoordinator
|
||||
|
||||
|
||||
class NextDnsEntity(CoordinatorEntity[NextDnsUpdateCoordinator[CoordinatorDataT]]):
|
||||
"""Define NextDNS entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NextDnsUpdateCoordinator[CoordinatorDataT],
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=f"https://my.nextdns.io/{coordinator.profile_id}/setup",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, str(coordinator.profile_id))},
|
||||
manufacturer="NextDNS Inc.",
|
||||
name=coordinator.nextdns.get_profile_name(coordinator.profile_id),
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.profile_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
@@ -20,10 +20,9 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NextDnsConfigEntry
|
||||
from .const import (
|
||||
@@ -33,9 +32,10 @@ from .const import (
|
||||
ATTR_PROTOCOLS,
|
||||
ATTR_STATUS,
|
||||
)
|
||||
from .coordinator import CoordinatorDataT, NextDnsUpdateCoordinator
|
||||
from .coordinator import CoordinatorDataT
|
||||
from .entity import NextDnsEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -297,27 +297,12 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class NextDnsSensor(
|
||||
CoordinatorEntity[NextDnsUpdateCoordinator[CoordinatorDataT]], SensorEntity
|
||||
):
|
||||
class NextDnsSensor(NextDnsEntity, SensorEntity):
|
||||
"""Define an NextDNS sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: NextDnsSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NextDnsUpdateCoordinator[CoordinatorDataT],
|
||||
description: NextDnsSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = coordinator.device_info
|
||||
self._attr_unique_id = f"{coordinator.profile_id}_{description.key}"
|
||||
self._attr_native_value = description.value(coordinator.data)
|
||||
self.entity_description: NextDnsSensorEntityDescription = description
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._attr_native_value = self.entity_description.value(self.coordinator.data)
|
||||
self.async_write_ha_state()
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value(self.coordinator.data)
|
||||
|
||||
@@ -4,16 +4,25 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "The API key for your NextDNS account"
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"data": {
|
||||
"profile": "Profile"
|
||||
"profile_name": "Profile"
|
||||
},
|
||||
"data_description": {
|
||||
"profile_name": "The NextDNS configuration profile you want to integrate"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::nextdns::config::step::user::data_description::api_key%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,11 +15,11 @@ from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NextDnsConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NextDnsUpdateCoordinator
|
||||
from .entity import NextDnsEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -536,12 +536,9 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class NextDnsSwitch(
|
||||
CoordinatorEntity[NextDnsUpdateCoordinator[Settings]], SwitchEntity
|
||||
):
|
||||
class NextDnsSwitch(NextDnsEntity, SwitchEntity):
|
||||
"""Define an NextDNS switch."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: NextDnsSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
@@ -550,11 +547,8 @@ class NextDnsSwitch(
|
||||
description: NextDnsSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = coordinator.device_info
|
||||
self._attr_unique_id = f"{coordinator.profile_id}_{description.key}"
|
||||
super().__init__(coordinator, description)
|
||||
self._attr_is_on = description.state(coordinator.data)
|
||||
self.entity_description = description
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
|
||||
@@ -66,6 +66,7 @@ class OneDriveUpdateCoordinator(DataUpdateCoordinator[Drive]):
|
||||
translation_domain=DOMAIN, translation_key="authentication_failed"
|
||||
) from err
|
||||
except OneDriveException as err:
|
||||
_LOGGER.debug("Failed to fetch drive data: %s")
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN, translation_key="update_failed"
|
||||
) from err
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/osoenergy",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyosoenergyapi==1.1.4"]
|
||||
"requirements": ["pyosoenergyapi==1.1.5"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ from pypaperless.exceptions import (
|
||||
PaperlessInvalidTokenError,
|
||||
)
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, Platform
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
@@ -69,7 +69,7 @@ async def _get_paperless_api(
|
||||
api = Paperless(
|
||||
entry.data[CONF_URL],
|
||||
entry.data[CONF_API_KEY],
|
||||
session=async_get_clientsession(hass),
|
||||
session=async_get_clientsession(hass, entry.data.get(CONF_VERIFY_SSL, True)),
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -16,7 +16,7 @@ from pypaperless.exceptions import (
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
@@ -25,6 +25,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_URL): str,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_VERIFY_SSL, default=True): bool,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -78,15 +79,19 @@ class PaperlessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(entry, data=user_input)
|
||||
|
||||
if user_input is not None:
|
||||
suggested_values = user_input
|
||||
else:
|
||||
suggested_values = {
|
||||
CONF_URL: entry.data[CONF_URL],
|
||||
CONF_VERIFY_SSL: entry.data.get(CONF_VERIFY_SSL, True),
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
suggested_values={
|
||||
CONF_URL: user_input[CONF_URL]
|
||||
if user_input is not None
|
||||
else entry.data[CONF_URL],
|
||||
},
|
||||
suggested_values=suggested_values,
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
@@ -122,13 +127,15 @@ class PaperlessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def _validate_input(self, user_input: dict[str, str]) -> dict[str, str]:
|
||||
async def _validate_input(self, user_input: dict[str, Any]) -> dict[str, str]:
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
client = Paperless(
|
||||
user_input[CONF_URL],
|
||||
user_input[CONF_API_KEY],
|
||||
session=async_get_clientsession(self.hass),
|
||||
session=async_get_clientsession(
|
||||
self.hass, user_input.get(CONF_VERIFY_SSL, True)
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"url": "URL to connect to the Paperless-ngx instance",
|
||||
"api_key": "API key to connect to the Paperless-ngx API"
|
||||
"api_key": "API key to connect to the Paperless-ngx API",
|
||||
"verify_ssl": "Verify the SSL certificate of the Paperless-ngx instance. Disable this option if you’re using a self-signed certificate."
|
||||
},
|
||||
"title": "Add Paperless-ngx instance"
|
||||
},
|
||||
@@ -24,11 +26,13 @@
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"url": "[%key:component::paperless_ngx::config::step::user::data_description::url%]",
|
||||
"api_key": "[%key:component::paperless_ngx::config::step::user::data_description::api_key%]"
|
||||
"api_key": "[%key:component::paperless_ngx::config::step::user::data_description::api_key%]",
|
||||
"verify_ssl": "[%key:component::paperless_ngx::config::step::user::data_description::verify_ssl%]"
|
||||
},
|
||||
"title": "Reconfigure Paperless-ngx instance"
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==10.0.4"]
|
||||
}
|
||||
|
||||
@@ -491,6 +491,12 @@
|
||||
"state": {
|
||||
"on": "mdi:eye-off"
|
||||
}
|
||||
},
|
||||
"privacy_mask": {
|
||||
"default": "mdi:eye",
|
||||
"state": {
|
||||
"on": "mdi:eye-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -960,6 +960,9 @@
|
||||
},
|
||||
"privacy_mode": {
|
||||
"name": "Privacy mode"
|
||||
},
|
||||
"privacy_mask": {
|
||||
"name": "Privacy mask"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +216,15 @@ SWITCH_ENTITIES = (
|
||||
value=lambda api, ch: api.baichuan.privacy_mode(ch),
|
||||
method=lambda api, ch, value: api.baichuan.set_privacy_mode(ch, value),
|
||||
),
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="privacy_mask",
|
||||
cmd_key="GetMask",
|
||||
translation_key="privacy_mask",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda api, ch: api.supported(ch, "privacy_mask"),
|
||||
value=lambda api, ch: api.privacy_mask_enabled(ch),
|
||||
method=lambda api, ch, value: api.set_privacy_mask(ch, enable=value),
|
||||
),
|
||||
ReolinkSwitchEntityDescription(
|
||||
key="hardwired_chime_enabled",
|
||||
cmd_key="483",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "assumed_state",
|
||||
"loggers": ["rflink"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["rflink==0.0.66"]
|
||||
"requirements": ["rflink==0.0.67"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiorussound==4.5.2"],
|
||||
"requirements": ["aiorussound==4.6.0"],
|
||||
"zeroconf": ["_rio._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -22,12 +22,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -
|
||||
|
||||
federwiege = Federwiege(hass.loop, connection)
|
||||
federwiege.register()
|
||||
federwiege.connect()
|
||||
|
||||
entry.runtime_data = federwiege
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
federwiege.connect()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmarlaapi", "pysignalr"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmarlaapi==0.8.2"]
|
||||
"requirements": ["pysmarlaapi==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -53,9 +53,10 @@ class SmarlaNumber(SmarlaBaseEntity, NumberEntity):
|
||||
_property: Property[int]
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
return self._property.get()
|
||||
v = self._property.get()
|
||||
return float(v) if v is not None else None
|
||||
|
||||
def set_native_value(self, value: float) -> None:
|
||||
"""Update to the smarla device."""
|
||||
|
||||
@@ -52,7 +52,7 @@ class SmarlaSwitch(SmarlaBaseEntity, SwitchEntity):
|
||||
_property: Property[bool]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
return self._property.get()
|
||||
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.2.4"]
|
||||
"requirements": ["pysmartthings==3.2.5"]
|
||||
}
|
||||
|
||||
@@ -605,7 +605,10 @@
|
||||
"name": "Wrinkle prevent"
|
||||
},
|
||||
"ice_maker": {
|
||||
"name": "Ice maker"
|
||||
"name": "Ice cubes"
|
||||
},
|
||||
"ice_maker_2": {
|
||||
"name": "Ice bites"
|
||||
},
|
||||
"sabbath_mode": {
|
||||
"name": "Sabbath mode"
|
||||
|
||||
@@ -95,6 +95,7 @@ CAPABILITY_TO_SWITCHES: dict[Capability | str, SmartThingsSwitchEntityDescriptio
|
||||
status_attribute=Attribute.SWITCH,
|
||||
component_translation_key={
|
||||
"icemaker": "ice_maker",
|
||||
"icemaker-02": "ice_maker_2",
|
||||
},
|
||||
),
|
||||
Capability.SAMSUNG_CE_SABBATH_MODE: SmartThingsSwitchEntityDescription(
|
||||
|
||||
@@ -46,6 +46,7 @@ from .const import (
|
||||
ATTR_DISABLE_WEB_PREV,
|
||||
ATTR_FILE,
|
||||
ATTR_IS_ANONYMOUS,
|
||||
ATTR_IS_BIG,
|
||||
ATTR_KEYBOARD,
|
||||
ATTR_KEYBOARD_INLINE,
|
||||
ATTR_MESSAGE,
|
||||
@@ -58,6 +59,7 @@ from .const import (
|
||||
ATTR_PARSER,
|
||||
ATTR_PASSWORD,
|
||||
ATTR_QUESTION,
|
||||
ATTR_REACTION,
|
||||
ATTR_RESIZE_KEYBOARD,
|
||||
ATTR_SHOW_ALERT,
|
||||
ATTR_STICKER_ID,
|
||||
@@ -94,6 +96,7 @@ from .const import (
|
||||
SERVICE_SEND_STICKER,
|
||||
SERVICE_SEND_VIDEO,
|
||||
SERVICE_SEND_VOICE,
|
||||
SERVICE_SET_MESSAGE_REACTION,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -250,6 +253,19 @@ SERVICE_SCHEMA_LEAVE_CHAT = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_SCHEMA_SET_MESSAGE_REACTION = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CONFIG_ENTRY_ID): cv.string,
|
||||
vol.Required(ATTR_MESSAGEID): vol.Any(
|
||||
cv.positive_int, vol.All(cv.string, "last")
|
||||
),
|
||||
vol.Required(ATTR_CHAT_ID): vol.Coerce(int),
|
||||
vol.Required(ATTR_REACTION): cv.string,
|
||||
vol.Optional(ATTR_IS_BIG, default=False): cv.boolean,
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
SERVICE_MAP = {
|
||||
SERVICE_SEND_MESSAGE: SERVICE_SCHEMA_SEND_MESSAGE,
|
||||
SERVICE_SEND_PHOTO: SERVICE_SCHEMA_SEND_FILE,
|
||||
@@ -266,6 +282,7 @@ SERVICE_MAP = {
|
||||
SERVICE_ANSWER_CALLBACK_QUERY: SERVICE_SCHEMA_ANSWER_CALLBACK_QUERY,
|
||||
SERVICE_DELETE_MESSAGE: SERVICE_SCHEMA_DELETE_MESSAGE,
|
||||
SERVICE_LEAVE_CHAT: SERVICE_SCHEMA_LEAVE_CHAT,
|
||||
SERVICE_SET_MESSAGE_REACTION: SERVICE_SCHEMA_SET_MESSAGE_REACTION,
|
||||
}
|
||||
|
||||
|
||||
@@ -378,6 +395,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
messages = await notify_service.leave_chat(
|
||||
context=service.context, **kwargs
|
||||
)
|
||||
elif msgtype == SERVICE_SET_MESSAGE_REACTION:
|
||||
await notify_service.set_message_reaction(context=service.context, **kwargs)
|
||||
else:
|
||||
await notify_service.edit_message(
|
||||
msgtype, context=service.context, **kwargs
|
||||
|
||||
@@ -786,6 +786,39 @@ class TelegramNotificationService:
|
||||
self.bot.leave_chat, "Error leaving chat", None, chat_id, context=context
|
||||
)
|
||||
|
||||
async def set_message_reaction(
|
||||
self,
|
||||
chat_id: int,
|
||||
reaction: str,
|
||||
is_big: bool = False,
|
||||
context: Context | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Set the bot's reaction for a given message."""
|
||||
chat_id = self._get_target_chat_ids(chat_id)[0]
|
||||
message_id, _ = self._get_msg_ids(kwargs, chat_id)
|
||||
params = self._get_msg_kwargs(kwargs)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Set reaction to message %s in chat ID %s to %s with params: %s",
|
||||
message_id,
|
||||
chat_id,
|
||||
reaction,
|
||||
params,
|
||||
)
|
||||
|
||||
await self._send_msg(
|
||||
self.bot.set_message_reaction,
|
||||
"Error setting message reaction",
|
||||
params[ATTR_MESSAGE_TAG],
|
||||
chat_id,
|
||||
message_id,
|
||||
reaction=reaction,
|
||||
is_big=is_big,
|
||||
read_timeout=params[ATTR_TIMEOUT],
|
||||
context=context,
|
||||
)
|
||||
|
||||
|
||||
def initialize_bot(hass: HomeAssistant, p_config: MappingProxyType[str, Any]) -> Bot:
|
||||
"""Initialize telegram bot with proxy support."""
|
||||
|
||||
@@ -43,6 +43,7 @@ SERVICE_SEND_VOICE = "send_voice"
|
||||
SERVICE_SEND_DOCUMENT = "send_document"
|
||||
SERVICE_SEND_LOCATION = "send_location"
|
||||
SERVICE_SEND_POLL = "send_poll"
|
||||
SERVICE_SET_MESSAGE_REACTION = "set_message_reaction"
|
||||
SERVICE_EDIT_MESSAGE = "edit_message"
|
||||
SERVICE_EDIT_CAPTION = "edit_caption"
|
||||
SERVICE_EDIT_REPLYMARKUP = "edit_replymarkup"
|
||||
@@ -87,6 +88,8 @@ ATTR_MSG = "message"
|
||||
ATTR_MSGID = "id"
|
||||
ATTR_PARSER = "parse_mode"
|
||||
ATTR_PASSWORD = "password"
|
||||
ATTR_REACTION = "reaction"
|
||||
ATTR_IS_BIG = "is_big"
|
||||
ATTR_REPLY_TO_MSGID = "reply_to_message_id"
|
||||
ATTR_REPLYMARKUP = "reply_markup"
|
||||
ATTR_SHOW_ALERT = "show_alert"
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
},
|
||||
"leave_chat": {
|
||||
"service": "mdi:exit-run"
|
||||
},
|
||||
"set_message_reaction": {
|
||||
"service": "mdi:emoticon-happy"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,3 +787,29 @@ leave_chat:
|
||||
example: 12345
|
||||
selector:
|
||||
text:
|
||||
|
||||
set_message_reaction:
|
||||
fields:
|
||||
config_entry_id:
|
||||
selector:
|
||||
config_entry:
|
||||
integration: telegram_bot
|
||||
message_id:
|
||||
required: true
|
||||
example: 54321
|
||||
selector:
|
||||
text:
|
||||
chat_id:
|
||||
required: true
|
||||
example: 12345
|
||||
selector:
|
||||
text:
|
||||
reaction:
|
||||
required: true
|
||||
example: 👍
|
||||
selector:
|
||||
text:
|
||||
is_big:
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
@@ -857,6 +857,32 @@
|
||||
"description": "Chat ID of the group from which the bot should be removed."
|
||||
}
|
||||
}
|
||||
},
|
||||
"set_message_reaction": {
|
||||
"name": "Set message reaction",
|
||||
"description": "Sets the bot's reaction for a given message.",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"name": "[%key:component::telegram_bot::services::send_message::fields::config_entry_id::name%]",
|
||||
"description": "The config entry representing the Telegram bot to set the message reaction."
|
||||
},
|
||||
"message_id": {
|
||||
"name": "[%key:component::telegram_bot::services::edit_message::fields::message_id::name%]",
|
||||
"description": "ID of the message to react to."
|
||||
},
|
||||
"chat_id": {
|
||||
"name": "[%key:component::telegram_bot::services::edit_message::fields::chat_id::name%]",
|
||||
"description": "ID of the chat containing the message."
|
||||
},
|
||||
"reaction": {
|
||||
"name": "Reaction",
|
||||
"description": "Emoji reaction to use."
|
||||
},
|
||||
"is_big": {
|
||||
"name": "Large animation",
|
||||
"description": "Whether the reaction animation should be large."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -4,14 +4,30 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
import re
|
||||
from typing import Any, cast
|
||||
|
||||
import jwt
|
||||
from tesla_fleet_api import TeslaFleetApi
|
||||
from tesla_fleet_api.const import SERVERS
|
||||
from tesla_fleet_api.exceptions import (
|
||||
InvalidResponse,
|
||||
PreconditionFailed,
|
||||
TeslaFleetError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
QrCodeSelector,
|
||||
QrCodeSelectorConfig,
|
||||
QrErrorCorrectionLevel,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import CONF_DOMAIN, DOMAIN, LOGGER
|
||||
from .oauth import TeslaUserImplementation
|
||||
|
||||
|
||||
class OAuth2FlowHandler(
|
||||
@@ -21,36 +37,173 @@ class OAuth2FlowHandler(
|
||||
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize config flow."""
|
||||
super().__init__()
|
||||
self.domain: str | None = None
|
||||
self.registration_status: dict[str, bool] = {}
|
||||
self.tesla_apis: dict[str, TeslaFleetApi] = {}
|
||||
self.failed_regions: list[str] = []
|
||||
self.data: dict[str, Any] = {}
|
||||
self.uid: str | None = None
|
||||
self.api: TeslaFleetApi | None = None
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
return LOGGER
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow start."""
|
||||
return await super().async_step_user()
|
||||
|
||||
async def async_oauth_create_entry(
|
||||
self,
|
||||
data: dict[str, Any],
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
|
||||
"""Handle OAuth completion and proceed to domain registration."""
|
||||
token = jwt.decode(
|
||||
data["token"]["access_token"], options={"verify_signature": False}
|
||||
)
|
||||
uid = token["sub"]
|
||||
|
||||
await self.async_set_unique_id(uid)
|
||||
self.data = data
|
||||
self.uid = token["sub"]
|
||||
server = SERVERS[token["ou_code"].lower()]
|
||||
|
||||
await self.async_set_unique_id(self.uid)
|
||||
if self.source == SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_mismatch(reason="reauth_account_mismatch")
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data=data
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=uid, data=data)
|
||||
|
||||
# OAuth done, setup a Partner API connection
|
||||
implementation = cast(TeslaUserImplementation, self.flow_impl)
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
self.api = TeslaFleetApi(
|
||||
session=session,
|
||||
server=server,
|
||||
partner_scope=True,
|
||||
charging_scope=False,
|
||||
energy_scope=False,
|
||||
user_scope=False,
|
||||
vehicle_scope=False,
|
||||
)
|
||||
await self.api.get_private_key(self.hass.config.path("tesla_fleet.key"))
|
||||
await self.api.partner_login(
|
||||
implementation.client_id, implementation.client_secret
|
||||
)
|
||||
|
||||
return await self.async_step_domain_input()
|
||||
|
||||
async def async_step_domain_input(
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
errors: dict[str, str] | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle domain input step."""
|
||||
|
||||
errors = errors or {}
|
||||
|
||||
if user_input is not None:
|
||||
domain = user_input[CONF_DOMAIN].strip().lower()
|
||||
|
||||
# Validate domain format
|
||||
if not self._is_valid_domain(domain):
|
||||
errors[CONF_DOMAIN] = "invalid_domain"
|
||||
else:
|
||||
self.domain = domain
|
||||
return await self.async_step_domain_registration()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="domain_input",
|
||||
description_placeholders={
|
||||
"dashboard": "https://developer.tesla.com/en_AU/dashboard/"
|
||||
},
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DOMAIN): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_domain_registration(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle domain registration for both regions."""
|
||||
|
||||
assert self.api
|
||||
assert self.api.private_key
|
||||
assert self.domain
|
||||
|
||||
errors = {}
|
||||
description_placeholders = {
|
||||
"public_key_url": f"https://{self.domain}/.well-known/appspecific/com.tesla.3p.public-key.pem",
|
||||
"pem": self.api.public_pem,
|
||||
}
|
||||
|
||||
try:
|
||||
register_response = await self.api.partner.register(self.domain)
|
||||
except PreconditionFailed:
|
||||
return await self.async_step_domain_input(
|
||||
errors={CONF_DOMAIN: "precondition_failed"}
|
||||
)
|
||||
except InvalidResponse:
|
||||
errors["base"] = "invalid_response"
|
||||
except TeslaFleetError as e:
|
||||
errors["base"] = "unknown_error"
|
||||
description_placeholders["error"] = e.message
|
||||
else:
|
||||
# Get public key from response
|
||||
registered_public_key = register_response.get("response", {}).get(
|
||||
"public_key"
|
||||
)
|
||||
|
||||
if not registered_public_key:
|
||||
errors["base"] = "public_key_not_found"
|
||||
elif (
|
||||
registered_public_key.lower()
|
||||
!= self.api.public_uncompressed_point.lower()
|
||||
):
|
||||
errors["base"] = "public_key_mismatch"
|
||||
else:
|
||||
return await self.async_step_registration_complete()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="domain_registration",
|
||||
description_placeholders=description_placeholders,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_registration_complete(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Show completion and virtual key installation."""
|
||||
if user_input is not None and self.uid and self.data:
|
||||
return self.async_create_entry(title=self.uid, data=self.data)
|
||||
|
||||
if not self.domain:
|
||||
return await self.async_step_domain_input()
|
||||
|
||||
virtual_key_url = f"https://www.tesla.com/_ak/{self.domain}"
|
||||
data_schema = vol.Schema({}).extend(
|
||||
{
|
||||
vol.Optional("qr_code"): QrCodeSelector(
|
||||
config=QrCodeSelectorConfig(
|
||||
data=virtual_key_url,
|
||||
scale=6,
|
||||
error_correction_level=QrErrorCorrectionLevel.QUARTILE,
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="registration_complete",
|
||||
data_schema=data_schema,
|
||||
description_placeholders={
|
||||
"virtual_key_url": virtual_key_url,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
@@ -67,4 +220,11 @@ class OAuth2FlowHandler(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={"name": "Tesla Fleet"},
|
||||
)
|
||||
return await self.async_step_user()
|
||||
# For reauth, skip domain registration and go straight to OAuth
|
||||
return await super().async_step_user()
|
||||
|
||||
def _is_valid_domain(self, domain: str) -> bool:
|
||||
"""Validate domain format."""
|
||||
# Basic domain validation regex
|
||||
domain_pattern = re.compile(r"^(?:[a-zA-Z0-9]+\.)+[a-zA-Z0-9-]+$")
|
||||
return bool(domain_pattern.match(domain))
|
||||
|
||||
@@ -9,6 +9,7 @@ from tesla_fleet_api.const import Scope
|
||||
|
||||
DOMAIN = "tesla_fleet"
|
||||
|
||||
CONF_DOMAIN = "domain"
|
||||
CONF_REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.1.3"]
|
||||
"requirements": ["tesla-fleet-api==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"already_configured": "Configuration updated for profile.",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
@@ -13,7 +14,12 @@
|
||||
"reauth_account_mismatch": "The reauthentication account does not match the original account"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
"invalid_domain": "Invalid domain format. Please enter a valid domain name.",
|
||||
"public_key_not_found": "Public key not found.",
|
||||
"public_key_mismatch": "The public key hosted at your domain does not match the expected key. Please ensure the correct public key is hosted at the specified location.",
|
||||
"precondition_failed": "The domain does not match the application's allowed origins.",
|
||||
"invalid_response": "The registration was rejected by Tesla",
|
||||
"unknown_error": "An unknown error occurred: {error}"
|
||||
},
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
@@ -25,6 +31,21 @@
|
||||
"implementation": "[%key:common::config_flow::description::implementation%]"
|
||||
}
|
||||
},
|
||||
"domain_input": {
|
||||
"title": "Tesla Fleet domain registration",
|
||||
"description": "Enter the domain that will host your public key. This is typically the domain of the origin you specified during registration at {dashboard}.",
|
||||
"data": {
|
||||
"domain": "Domain"
|
||||
}
|
||||
},
|
||||
"domain_registration": {
|
||||
"title": "Registering public key",
|
||||
"description": "You must host the public key at:\n\n{public_key_url}\n\n```\n{pem}\n```"
|
||||
},
|
||||
"registration_complete": {
|
||||
"title": "Command signing",
|
||||
"description": "To enable command signing, you must open the Tesla app, select your vehicle, and then visit the following URL to set up a virtual key. You must repeat this process for each vehicle.\n\n{virtual_key_url}"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The {name} integration needs to re-authenticate your account"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.1.3", "teslemetry-stream==0.7.9"]
|
||||
"requirements": ["tesla-fleet-api==1.2.0", "teslemetry-stream==0.7.9"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tessie",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tessie", "tesla-fleet-api"],
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.1.3"]
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ class StateVacuumEntity(
|
||||
@property
|
||||
def capability_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return capability attributes."""
|
||||
if VacuumEntityFeature.FAN_SPEED in self.supported_features_compat:
|
||||
if VacuumEntityFeature.FAN_SPEED in self.supported_features:
|
||||
return {ATTR_FAN_SPEED_LIST: self.fan_speed_list}
|
||||
return None
|
||||
|
||||
@@ -330,7 +330,7 @@ class StateVacuumEntity(
|
||||
def state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of the vacuum cleaner."""
|
||||
data: dict[str, Any] = {}
|
||||
supported_features = self.supported_features_compat
|
||||
supported_features = self.supported_features
|
||||
|
||||
if VacuumEntityFeature.BATTERY in supported_features:
|
||||
data[ATTR_BATTERY_LEVEL] = self.battery_level
|
||||
@@ -369,19 +369,6 @@ class StateVacuumEntity(
|
||||
"""Flag vacuum cleaner features that are supported."""
|
||||
return self._attr_supported_features
|
||||
|
||||
@property
|
||||
def supported_features_compat(self) -> VacuumEntityFeature:
|
||||
"""Return the supported features as VacuumEntityFeature.
|
||||
|
||||
Remove this compatibility shim in 2025.1 or later.
|
||||
"""
|
||||
features = self.supported_features
|
||||
if type(features) is int:
|
||||
new_features = VacuumEntityFeature(features)
|
||||
self._report_deprecated_supported_features_values(new_features)
|
||||
return new_features
|
||||
return features
|
||||
|
||||
def stop(self, **kwargs: Any) -> None:
|
||||
"""Stop the vacuum cleaner."""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -74,3 +74,4 @@ class EcoSmartMode(StrEnum):
|
||||
OFF = "off"
|
||||
ECO_MODE = "eco_mode"
|
||||
FULL_SOLAR = "full_solar"
|
||||
DISABLED = "disabled"
|
||||
|
||||
@@ -166,13 +166,20 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
)
|
||||
|
||||
# Set current solar charging mode
|
||||
eco_smart_enabled = data[CHARGER_DATA_KEY][CHARGER_ECO_SMART_KEY][
|
||||
CHARGER_ECO_SMART_STATUS_KEY
|
||||
]
|
||||
eco_smart_mode = data[CHARGER_DATA_KEY][CHARGER_ECO_SMART_KEY][
|
||||
CHARGER_ECO_SMART_MODE_KEY
|
||||
]
|
||||
if eco_smart_enabled is False:
|
||||
eco_smart_enabled = (
|
||||
data[CHARGER_DATA_KEY]
|
||||
.get(CHARGER_ECO_SMART_KEY, {})
|
||||
.get(CHARGER_ECO_SMART_STATUS_KEY)
|
||||
)
|
||||
|
||||
eco_smart_mode = (
|
||||
data[CHARGER_DATA_KEY]
|
||||
.get(CHARGER_ECO_SMART_KEY, {})
|
||||
.get(CHARGER_ECO_SMART_MODE_KEY)
|
||||
)
|
||||
if eco_smart_mode is None:
|
||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.DISABLED
|
||||
elif eco_smart_enabled is False:
|
||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF
|
||||
elif eco_smart_mode == 0:
|
||||
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE
|
||||
|
||||
@@ -63,15 +63,15 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Create wallbox select entities in HASS."""
|
||||
coordinator: WallboxCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
WallboxSelect(coordinator, description)
|
||||
for ent in coordinator.data
|
||||
if (
|
||||
(description := SELECT_TYPES.get(ent))
|
||||
and description.supported_fn(coordinator)
|
||||
if coordinator.data[CHARGER_ECO_SMART_KEY] != EcoSmartMode.DISABLED:
|
||||
async_add_entities(
|
||||
WallboxSelect(coordinator, description)
|
||||
for ent in coordinator.data
|
||||
if (
|
||||
(description := SELECT_TYPES.get(ent))
|
||||
and description.supported_fn(coordinator)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class WallboxSelect(WallboxEntity, SelectEntity):
|
||||
|
||||
@@ -6,6 +6,7 @@ import asyncio
|
||||
from collections.abc import AsyncGenerator
|
||||
import io
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Final
|
||||
import wave
|
||||
|
||||
@@ -36,6 +37,7 @@ from homeassistant.components.assist_satellite import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.ulid import ulid_now
|
||||
|
||||
from .const import DOMAIN, SAMPLE_CHANNELS, SAMPLE_WIDTH
|
||||
from .data import WyomingService
|
||||
@@ -53,6 +55,7 @@ _PING_SEND_DELAY: Final = 2
|
||||
_PIPELINE_FINISH_TIMEOUT: Final = 1
|
||||
_TTS_SAMPLE_RATE: Final = 22050
|
||||
_ANNOUNCE_CHUNK_BYTES: Final = 2048 # 1024 samples
|
||||
_TTS_TIMEOUT_EXTRA: Final = 1.0
|
||||
|
||||
# Wyoming stage -> Assist stage
|
||||
_STAGES: dict[PipelineStage, assist_pipeline.PipelineStage] = {
|
||||
@@ -125,6 +128,10 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
self._ffmpeg_manager: ffmpeg.FFmpegManager | None = None
|
||||
self._played_event_received: asyncio.Event | None = None
|
||||
|
||||
# Randomly set on each pipeline loop run.
|
||||
# Used to ensure TTS timeout is acted on correctly.
|
||||
self._run_loop_id: str | None = None
|
||||
|
||||
@property
|
||||
def pipeline_entity_id(self) -> str | None:
|
||||
"""Return the entity ID of the pipeline to use for the next conversation."""
|
||||
@@ -511,6 +518,7 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
wake_word_phrase: str | None = None
|
||||
run_pipeline: RunPipeline | None = None
|
||||
send_ping = True
|
||||
self._run_loop_id = ulid_now()
|
||||
|
||||
# Read events and check for pipeline end in parallel
|
||||
pipeline_ended_task = self.config_entry.async_create_background_task(
|
||||
@@ -698,38 +706,52 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
f"Cannot stream audio format to satellite: {tts_result.extension}"
|
||||
)
|
||||
|
||||
data = b"".join([chunk async for chunk in tts_result.async_stream_result()])
|
||||
# Track the total duration of TTS audio for response timeout
|
||||
total_seconds = 0.0
|
||||
start_time = time.monotonic()
|
||||
|
||||
with io.BytesIO(data) as wav_io, wave.open(wav_io, "rb") as wav_file:
|
||||
sample_rate = wav_file.getframerate()
|
||||
sample_width = wav_file.getsampwidth()
|
||||
sample_channels = wav_file.getnchannels()
|
||||
_LOGGER.debug("Streaming %s TTS sample(s)", wav_file.getnframes())
|
||||
try:
|
||||
data = b"".join([chunk async for chunk in tts_result.async_stream_result()])
|
||||
|
||||
timestamp = 0
|
||||
await self._client.write_event(
|
||||
AudioStart(
|
||||
rate=sample_rate,
|
||||
width=sample_width,
|
||||
channels=sample_channels,
|
||||
timestamp=timestamp,
|
||||
).event()
|
||||
)
|
||||
with io.BytesIO(data) as wav_io, wave.open(wav_io, "rb") as wav_file:
|
||||
sample_rate = wav_file.getframerate()
|
||||
sample_width = wav_file.getsampwidth()
|
||||
sample_channels = wav_file.getnchannels()
|
||||
_LOGGER.debug("Streaming %s TTS sample(s)", wav_file.getnframes())
|
||||
|
||||
# Stream audio chunks
|
||||
while audio_bytes := wav_file.readframes(_SAMPLES_PER_CHUNK):
|
||||
chunk = AudioChunk(
|
||||
rate=sample_rate,
|
||||
width=sample_width,
|
||||
channels=sample_channels,
|
||||
audio=audio_bytes,
|
||||
timestamp=timestamp,
|
||||
timestamp = 0
|
||||
await self._client.write_event(
|
||||
AudioStart(
|
||||
rate=sample_rate,
|
||||
width=sample_width,
|
||||
channels=sample_channels,
|
||||
timestamp=timestamp,
|
||||
).event()
|
||||
)
|
||||
await self._client.write_event(chunk.event())
|
||||
timestamp += chunk.seconds
|
||||
|
||||
await self._client.write_event(AudioStop(timestamp=timestamp).event())
|
||||
_LOGGER.debug("TTS streaming complete")
|
||||
# Stream audio chunks
|
||||
while audio_bytes := wav_file.readframes(_SAMPLES_PER_CHUNK):
|
||||
chunk = AudioChunk(
|
||||
rate=sample_rate,
|
||||
width=sample_width,
|
||||
channels=sample_channels,
|
||||
audio=audio_bytes,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
await self._client.write_event(chunk.event())
|
||||
timestamp += chunk.seconds
|
||||
total_seconds += chunk.seconds
|
||||
|
||||
await self._client.write_event(AudioStop(timestamp=timestamp).event())
|
||||
_LOGGER.debug("TTS streaming complete")
|
||||
finally:
|
||||
send_duration = time.monotonic() - start_time
|
||||
timeout_seconds = max(0, total_seconds - send_duration + _TTS_TIMEOUT_EXTRA)
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._tts_timeout(timeout_seconds, self._run_loop_id),
|
||||
name="wyoming TTS timeout",
|
||||
)
|
||||
|
||||
async def _stt_stream(self) -> AsyncGenerator[bytes]:
|
||||
"""Yield audio chunks from a queue."""
|
||||
@@ -744,6 +766,18 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
|
||||
yield chunk
|
||||
|
||||
async def _tts_timeout(
|
||||
self, timeout_seconds: float, run_loop_id: str | None
|
||||
) -> None:
|
||||
"""Force state change to IDLE in case TTS played event isn't received."""
|
||||
await asyncio.sleep(timeout_seconds + _TTS_TIMEOUT_EXTRA)
|
||||
|
||||
if run_loop_id != self._run_loop_id:
|
||||
# On a different pipeline run now
|
||||
return
|
||||
|
||||
self.tts_response_finished()
|
||||
|
||||
@callback
|
||||
def _handle_timer(
|
||||
self, event_type: intent.TimerEventType, timer: intent.TimerInfo
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "xiaomi_miio",
|
||||
"name": "Xiaomi Miio",
|
||||
"name": "Xiaomi Home",
|
||||
"codeowners": ["@rytilahti", "@syssi", "@starkillerOG"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_miio",
|
||||
|
||||
@@ -5,37 +5,37 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"incomplete_info": "Incomplete information to set up device, no host or token supplied.",
|
||||
"not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Miio.",
|
||||
"not_xiaomi_miio": "Device is not (yet) supported by Xiaomi Home integration.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"wrong_token": "Checksum error, wrong token",
|
||||
"unknown_device": "The device model is not known, not able to set up the device using config flow.",
|
||||
"cloud_no_devices": "No devices found in this Xiaomi Miio cloud account.",
|
||||
"cloud_credentials_incomplete": "Cloud credentials incomplete, please fill in username, password and country",
|
||||
"cloud_login_error": "Could not log in to Xiaomi Miio Cloud, check the credentials."
|
||||
"cloud_no_devices": "No devices found in this Xiaomi Home account.",
|
||||
"cloud_credentials_incomplete": "Credentials incomplete, please fill in username, password and server region",
|
||||
"cloud_login_error": "Could not log in to Xiaomi Home, check the credentials."
|
||||
},
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"description": "The Xiaomi Miio integration needs to re-authenticate your account in order to update the tokens or add missing cloud credentials.",
|
||||
"description": "The Xiaomi Home integration needs to re-authenticate your account in order to update the tokens or add missing credentials.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
},
|
||||
"cloud": {
|
||||
"data": {
|
||||
"cloud_username": "Cloud username",
|
||||
"cloud_password": "Cloud password",
|
||||
"cloud_country": "Cloud server country",
|
||||
"cloud_username": "[%key:common::config_flow::data::username%]",
|
||||
"cloud_password": "[%key:common::config_flow::data::password%]",
|
||||
"cloud_country": "Server region",
|
||||
"manual": "Configure manually (not recommended)"
|
||||
},
|
||||
"description": "Log in to the Xiaomi Miio cloud, see https://www.openhab.org/addons/bindings/miio/#country-servers for the cloud server to use."
|
||||
"description": "Log in to Xiaomi Home, see https://www.openhab.org/addons/bindings/miio/#country-servers for the server region to use."
|
||||
},
|
||||
"select": {
|
||||
"data": {
|
||||
"select_device": "Miio device"
|
||||
"select_device": "[%key:common::config_flow::data::device%]"
|
||||
},
|
||||
"description": "Select the Xiaomi Miio device to set up."
|
||||
"description": "Select the Xiaomi Home device to set up."
|
||||
},
|
||||
"manual": {
|
||||
"data": {
|
||||
@@ -58,7 +58,7 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"cloud_subdevices": "Use cloud to get connected subdevices"
|
||||
"cloud_subdevices": "Use Xiaomi Home service to get connected subdevices"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,7 +331,7 @@
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity ID",
|
||||
"description": "Name of the Xiaomi Miio entity."
|
||||
"description": "Name of the Xiaomi Home entity."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -896,6 +896,7 @@ DISCOVERY_SCHEMAS = [
|
||||
writeable=False,
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# generic text sensors
|
||||
ZWaveDiscoverySchema(
|
||||
@@ -932,6 +933,7 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
data_template=NumericSensorDataTemplate(),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# Meter sensors for Meter CC
|
||||
ZWaveDiscoverySchema(
|
||||
@@ -957,6 +959,7 @@ DISCOVERY_SCHEMAS = [
|
||||
writeable=True,
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# button for Indicator CC
|
||||
ZWaveDiscoverySchema(
|
||||
@@ -980,6 +983,7 @@ DISCOVERY_SCHEMAS = [
|
||||
writeable=True,
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# binary switch
|
||||
# barrier operator signaling states
|
||||
|
||||
@@ -7475,7 +7475,7 @@
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Xiaomi Miio"
|
||||
"name": "Xiaomi Home"
|
||||
},
|
||||
"xiaomi_tv": {
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -682,9 +682,12 @@ def _load_services_file(hass: HomeAssistant, integration: Integration) -> JSON_T
|
||||
|
||||
def _load_services_files(
|
||||
hass: HomeAssistant, integrations: Iterable[Integration]
|
||||
) -> list[JSON_TYPE]:
|
||||
) -> dict[str, JSON_TYPE]:
|
||||
"""Load service files for multiple integrations."""
|
||||
return [_load_services_file(hass, integration) for integration in integrations]
|
||||
return {
|
||||
integration.domain: _load_services_file(hass, integration)
|
||||
for integration in integrations
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
@@ -744,10 +747,9 @@ async def async_get_all_descriptions(
|
||||
_LOGGER.error("Failed to load integration: %s", domain, exc_info=int_or_exc)
|
||||
|
||||
if integrations:
|
||||
contents = await hass.async_add_executor_job(
|
||||
loaded = await hass.async_add_executor_job(
|
||||
_load_services_files, hass, integrations
|
||||
)
|
||||
loaded = dict(zip(domains_with_missing_services, contents, strict=False))
|
||||
|
||||
# Load translations for all service domains
|
||||
translations = await translation.async_get_translations(
|
||||
|
||||
Generated
+13
-13
@@ -201,7 +201,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2025.5.1
|
||||
aioautomower==2025.6.0
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.1
|
||||
@@ -369,7 +369,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.42
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==4.5.2
|
||||
aiorussound==4.6.0
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@@ -683,7 +683,7 @@ brunt==1.2.0
|
||||
bt-proximity==0.2.1
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==3.12.4
|
||||
bthome-ble==3.13.1
|
||||
|
||||
# homeassistant.components.bt_home_hub_5
|
||||
bthomehub5-devicelist==0.1.1
|
||||
@@ -1203,7 +1203,7 @@ ibmiotf==0.3.4
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==10.0.0
|
||||
ical==10.0.4
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@@ -1484,7 +1484,7 @@ nad-receiver==0.3.0
|
||||
ndms2-client==0.1.2
|
||||
|
||||
# homeassistant.components.ness_alarm
|
||||
nessclient==1.1.2
|
||||
nessclient==1.2.0
|
||||
|
||||
# homeassistant.components.netdata
|
||||
netdata==1.3.0
|
||||
@@ -1505,7 +1505,7 @@ nexia==2.10.0
|
||||
nextcloudmonitor==1.5.1
|
||||
|
||||
# homeassistant.components.discord
|
||||
nextcord==2.6.0
|
||||
nextcord==3.1.0
|
||||
|
||||
# homeassistant.components.nextdns
|
||||
nextdns==4.0.0
|
||||
@@ -2156,7 +2156,7 @@ pymonoprice==0.4
|
||||
pymsteams==0.1.12
|
||||
|
||||
# homeassistant.components.mysensors
|
||||
pymysensors==0.24.0
|
||||
pymysensors==0.25.0
|
||||
|
||||
# homeassistant.components.iron_os
|
||||
pynecil==4.1.0
|
||||
@@ -2210,7 +2210,7 @@ pyopnsense==0.4.0
|
||||
pyoppleio-legacy==1.0.8
|
||||
|
||||
# homeassistant.components.osoenergy
|
||||
pyosoenergyapi==1.1.4
|
||||
pyosoenergyapi==1.1.5
|
||||
|
||||
# homeassistant.components.opentherm_gw
|
||||
pyotgw==2.2.2
|
||||
@@ -2338,10 +2338,10 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smarla
|
||||
pysmarlaapi==0.8.2
|
||||
pysmarlaapi==0.9.0
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.2.4
|
||||
pysmartthings==3.2.5
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
@@ -2350,7 +2350,7 @@ pysmarty2==0.10.2
|
||||
pysmhi==1.0.2
|
||||
|
||||
# homeassistant.components.edl21
|
||||
pysml==0.0.12
|
||||
pysml==0.1.5
|
||||
|
||||
# homeassistant.components.smlight
|
||||
pysmlight==0.2.6
|
||||
@@ -2658,7 +2658,7 @@ reolink-aio==0.14.1
|
||||
rfk101py==0.0.1
|
||||
|
||||
# homeassistant.components.rflink
|
||||
rflink==0.0.66
|
||||
rflink==0.0.67
|
||||
|
||||
# homeassistant.components.ring
|
||||
ring-doorbell==0.9.13
|
||||
@@ -2900,7 +2900,7 @@ temperusb==1.6.1
|
||||
# homeassistant.components.tesla_fleet
|
||||
# homeassistant.components.teslemetry
|
||||
# homeassistant.components.tessie
|
||||
tesla-fleet-api==1.1.3
|
||||
tesla-fleet-api==1.2.0
|
||||
|
||||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.5.2
|
||||
|
||||
Generated
+13
-13
@@ -189,7 +189,7 @@ aioaseko==1.0.0
|
||||
aioasuswrt==1.4.0
|
||||
|
||||
# homeassistant.components.husqvarna_automower
|
||||
aioautomower==2025.5.1
|
||||
aioautomower==2025.6.0
|
||||
|
||||
# homeassistant.components.azure_devops
|
||||
aioazuredevops==2.2.1
|
||||
@@ -351,7 +351,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.42
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==4.5.2
|
||||
aiorussound==4.6.0
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@@ -607,7 +607,7 @@ brottsplatskartan==1.0.5
|
||||
brunt==1.2.0
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==3.12.4
|
||||
bthome-ble==3.13.1
|
||||
|
||||
# homeassistant.components.buienradar
|
||||
buienradar==1.0.6
|
||||
@@ -1040,7 +1040,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==10.0.0
|
||||
ical==10.0.4
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.1.0
|
||||
@@ -1270,7 +1270,7 @@ myuplink==0.7.0
|
||||
ndms2-client==0.1.2
|
||||
|
||||
# homeassistant.components.ness_alarm
|
||||
nessclient==1.1.2
|
||||
nessclient==1.2.0
|
||||
|
||||
# homeassistant.components.nmap_tracker
|
||||
netmap==0.7.0.2
|
||||
@@ -1285,7 +1285,7 @@ nexia==2.10.0
|
||||
nextcloudmonitor==1.5.1
|
||||
|
||||
# homeassistant.components.discord
|
||||
nextcord==2.6.0
|
||||
nextcord==3.1.0
|
||||
|
||||
# homeassistant.components.nextdns
|
||||
nextdns==4.0.0
|
||||
@@ -1789,7 +1789,7 @@ pymodbus==3.9.2
|
||||
pymonoprice==0.4
|
||||
|
||||
# homeassistant.components.mysensors
|
||||
pymysensors==0.24.0
|
||||
pymysensors==0.25.0
|
||||
|
||||
# homeassistant.components.iron_os
|
||||
pynecil==4.1.0
|
||||
@@ -1834,7 +1834,7 @@ pyopenweathermap==0.2.2
|
||||
pyopnsense==0.4.0
|
||||
|
||||
# homeassistant.components.osoenergy
|
||||
pyosoenergyapi==1.1.4
|
||||
pyosoenergyapi==1.1.5
|
||||
|
||||
# homeassistant.components.opentherm_gw
|
||||
pyotgw==2.2.2
|
||||
@@ -1938,10 +1938,10 @@ pysma==0.7.5
|
||||
pysmappee==0.2.29
|
||||
|
||||
# homeassistant.components.smarla
|
||||
pysmarlaapi==0.8.2
|
||||
pysmarlaapi==0.9.0
|
||||
|
||||
# homeassistant.components.smartthings
|
||||
pysmartthings==3.2.4
|
||||
pysmartthings==3.2.5
|
||||
|
||||
# homeassistant.components.smarty
|
||||
pysmarty2==0.10.2
|
||||
@@ -1950,7 +1950,7 @@ pysmarty2==0.10.2
|
||||
pysmhi==1.0.2
|
||||
|
||||
# homeassistant.components.edl21
|
||||
pysml==0.0.12
|
||||
pysml==0.1.5
|
||||
|
||||
# homeassistant.components.smlight
|
||||
pysmlight==0.2.6
|
||||
@@ -2198,7 +2198,7 @@ renson-endura-delta==1.7.2
|
||||
reolink-aio==0.14.1
|
||||
|
||||
# homeassistant.components.rflink
|
||||
rflink==0.0.66
|
||||
rflink==0.0.67
|
||||
|
||||
# homeassistant.components.ring
|
||||
ring-doorbell==0.9.13
|
||||
@@ -2383,7 +2383,7 @@ temperusb==1.6.1
|
||||
# homeassistant.components.tesla_fleet
|
||||
# homeassistant.components.teslemetry
|
||||
# homeassistant.components.tessie
|
||||
tesla-fleet-api==1.1.3
|
||||
tesla-fleet-api==1.2.0
|
||||
|
||||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.5.2
|
||||
|
||||
@@ -480,7 +480,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
|
||||
"hko",
|
||||
"hlk_sw16",
|
||||
"holiday",
|
||||
"home_connect",
|
||||
"homekit",
|
||||
"homekit_controller",
|
||||
"homematic",
|
||||
|
||||
@@ -109,11 +109,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
"devialet": {"async-upnp-client": {"async-timeout"}},
|
||||
"dlna_dmr": {"async-upnp-client": {"async-timeout"}},
|
||||
"dlna_dms": {"async-upnp-client": {"async-timeout"}},
|
||||
"edl21": {
|
||||
# https://github.com/mtdcr/pysml/issues/21
|
||||
# pysml > pyserial-asyncio
|
||||
"pysml": {"pyserial-asyncio", "async-timeout"},
|
||||
},
|
||||
"efergy": {
|
||||
# https://github.com/tkdrob/pyefergy/issues/46
|
||||
# pyefergy > codecov
|
||||
@@ -225,21 +220,11 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
# pymonoprice > pyserial-asyncio
|
||||
"pymonoprice": {"pyserial-asyncio"}
|
||||
},
|
||||
"mysensors": {
|
||||
# https://github.com/theolind/pymysensors/issues/818
|
||||
# pymysensors > pyserial-asyncio
|
||||
"pymysensors": {"pyserial-asyncio"}
|
||||
},
|
||||
"mystrom": {
|
||||
# https://github.com/home-assistant-ecosystem/python-mystrom/issues/55
|
||||
# python-mystrom > setuptools
|
||||
"python-mystrom": {"setuptools"}
|
||||
},
|
||||
"ness_alarm": {
|
||||
# https://github.com/nickw444/nessclient/issues/73
|
||||
# nessclient > pyserial-asyncio
|
||||
"nessclient": {"pyserial-asyncio"}
|
||||
},
|
||||
"nibe_heatpump": {"nibe": {"async-timeout"}},
|
||||
"norway_air": {"pymetno": {"async-timeout"}},
|
||||
"nx584": {
|
||||
@@ -260,11 +245,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
# opower > arrow > types-python-dateutil
|
||||
"arrow": {"types-python-dateutil"}
|
||||
},
|
||||
"osoenergy": {
|
||||
# https://github.com/osohotwateriot/apyosohotwaterapi/pull/4
|
||||
# pyosoenergyapi > unasync > setuptools
|
||||
"unasync": {"setuptools"}
|
||||
},
|
||||
"ovo_energy": {
|
||||
# https://github.com/timmo001/ovoenergy/issues/132
|
||||
# ovoenergy > incremental > setuptools
|
||||
@@ -277,11 +257,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
# gpiozero > colorzero > setuptools
|
||||
"colorzero": {"setuptools"}
|
||||
},
|
||||
"rflink": {
|
||||
# https://github.com/aequitas/python-rflink/issues/78
|
||||
# rflink > pyserial-asyncio
|
||||
"rflink": {"pyserial-asyncio", "async-timeout"}
|
||||
},
|
||||
"ring": {"ring-doorbell": {"async-timeout"}},
|
||||
"rmvtransport": {"pyrmvtransport": {"async-timeout"}},
|
||||
"roborock": {"python-roborock": {"async-timeout"}},
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_alarm_delay',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -113,7 +113,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_battery_nominal_voltage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -214,7 +214,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_battery_shutdown',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -263,7 +263,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_battery_timeout',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -368,7 +368,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_cable_type',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -416,7 +416,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_daemon_version',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -464,7 +464,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_date_and_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -512,7 +512,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_driver',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -560,7 +560,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_firmware_version',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -768,7 +768,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_last_transfer',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -916,7 +916,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_model',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -964,7 +964,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_name',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1012,7 +1012,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_nominal_apparent_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1065,7 +1065,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_nominal_input_voltage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1118,7 +1118,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_nominal_output_power',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1227,7 +1227,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_self_test_interval',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1324,7 +1324,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_sensitivity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1372,7 +1372,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_serial_number',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1420,7 +1420,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_shutdown_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1517,7 +1517,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_status_data',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1565,7 +1565,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_status_date',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1613,7 +1613,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_status_flag',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1880,7 +1880,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_transfer_from_battery',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1928,7 +1928,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_transfer_high',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -1981,7 +1981,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_transfer_low',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -2034,7 +2034,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.myups_transfer_to_battery',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from enum import Enum
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import cover
|
||||
from homeassistant.components.cover import CoverState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_PLATFORM, SERVICE_TOGGLE
|
||||
@@ -13,11 +11,7 @@ from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import MockCover
|
||||
|
||||
from tests.common import (
|
||||
MockEntityPlatform,
|
||||
help_test_all,
|
||||
setup_test_component_platform,
|
||||
)
|
||||
from tests.common import help_test_all, setup_test_component_platform
|
||||
|
||||
|
||||
async def test_services(
|
||||
@@ -159,24 +153,3 @@ def _create_tuples(enum: type[Enum], constant_prefix: str) -> list[tuple[Enum, s
|
||||
def test_all() -> None:
|
||||
"""Test module.__all__ is correctly set."""
|
||||
help_test_all(cover)
|
||||
|
||||
|
||||
def test_deprecated_supported_features_ints(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test deprecated supported features ints."""
|
||||
|
||||
class MockCoverEntity(cover.CoverEntity):
|
||||
_attr_supported_features = 1
|
||||
|
||||
entity = MockCoverEntity()
|
||||
entity.hass = hass
|
||||
entity.platform = MockEntityPlatform(hass)
|
||||
assert entity.supported_features is cover.CoverEntityFeature(1)
|
||||
assert "MockCoverEntity" in caplog.text
|
||||
assert "is using deprecated supported features values" in caplog.text
|
||||
assert "Instead it should use" in caplog.text
|
||||
assert "CoverEntityFeature.OPEN" in caplog.text
|
||||
caplog.clear()
|
||||
assert entity.supported_features is cover.CoverEntityFeature(1)
|
||||
assert "is using deprecated supported features values" not in caplog.text
|
||||
|
||||
@@ -97,7 +97,7 @@ async def test_sensors(
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) is None
|
||||
state = hass.states.get(f"sensor.{DEFAULT_NAME}_total_run_time")
|
||||
assert state.state == "1720984"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) is None
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.DURATION
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.SECONDS
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) is None
|
||||
state = hass.states.get(f"sensor.{DEFAULT_NAME}_wi_fi_ssid")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests helpers."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -77,3 +78,22 @@ async def mock_init_component(
|
||||
async def setup_ha(hass: HomeAssistant) -> None:
|
||||
"""Set up Home Assistant."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_send_message_stream() -> Generator[AsyncMock]:
|
||||
"""Mock stream response."""
|
||||
|
||||
async def mock_generator(stream):
|
||||
for value in stream:
|
||||
yield value
|
||||
|
||||
with patch(
|
||||
"google.genai.chats.AsyncChat.send_message_stream",
|
||||
AsyncMock(),
|
||||
) as mock_send_message_stream:
|
||||
mock_send_message_stream.side_effect = lambda **kwargs: mock_generator(
|
||||
mock_send_message_stream.return_value.pop(0)
|
||||
)
|
||||
|
||||
yield mock_send_message_stream
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Tests for the Google Generative AI Conversation integration conversation platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun import freeze_time
|
||||
@@ -41,25 +40,6 @@ def mock_ulid_tools():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_send_message_stream() -> Generator[AsyncMock]:
|
||||
"""Mock stream response."""
|
||||
|
||||
async def mock_generator(stream):
|
||||
for value in stream:
|
||||
yield value
|
||||
|
||||
with patch(
|
||||
"google.genai.chats.AsyncChat.send_message_stream",
|
||||
AsyncMock(),
|
||||
) as mock_send_message_stream:
|
||||
mock_send_message_stream.side_effect = lambda **kwargs: mock_generator(
|
||||
mock_send_message_stream.return_value.pop(0)
|
||||
)
|
||||
|
||||
yield mock_send_message_stream
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("error"),
|
||||
[
|
||||
|
||||
@@ -150,10 +150,10 @@ async def test_sensor(
|
||||
duration = hass.states.get("sensor.test_duration")
|
||||
assert duration.attributes.get("unit_of_measurement") == UnitOfTime.MINUTES
|
||||
assert duration.attributes.get(ATTR_ICON) == icon
|
||||
assert duration.state == "26"
|
||||
assert duration.state == "26.1833333333333"
|
||||
|
||||
assert float(hass.states.get("sensor.test_distance").state) == pytest.approx(13.682)
|
||||
assert hass.states.get("sensor.test_duration_in_traffic").state == "30"
|
||||
assert hass.states.get("sensor.test_duration_in_traffic").state == "29.6"
|
||||
assert hass.states.get("sensor.test_origin").state == "22nd St NW"
|
||||
assert (
|
||||
hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE)
|
||||
@@ -501,13 +501,13 @@ async def test_restore_state(hass: HomeAssistant) -> None:
|
||||
"1234",
|
||||
attributes={
|
||||
ATTR_LAST_RESET: last_reset,
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES,
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
|
||||
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
),
|
||||
{
|
||||
"native_value": 1234,
|
||||
"native_unit_of_measurement": UnitOfTime.MINUTES,
|
||||
"native_unit_of_measurement": UnitOfTime.SECONDS,
|
||||
"icon": "mdi:car",
|
||||
"last_reset": last_reset,
|
||||
},
|
||||
@@ -518,13 +518,13 @@ async def test_restore_state(hass: HomeAssistant) -> None:
|
||||
"5678",
|
||||
attributes={
|
||||
ATTR_LAST_RESET: last_reset,
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.MINUTES,
|
||||
ATTR_UNIT_OF_MEASUREMENT: UnitOfTime.SECONDS,
|
||||
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
),
|
||||
{
|
||||
"native_value": 5678,
|
||||
"native_unit_of_measurement": UnitOfTime.MINUTES,
|
||||
"native_unit_of_measurement": UnitOfTime.SECONDS,
|
||||
"icon": "mdi:car",
|
||||
"last_reset": last_reset,
|
||||
},
|
||||
@@ -596,12 +596,12 @@ async def test_restore_state(hass: HomeAssistant) -> None:
|
||||
|
||||
# restore from cache
|
||||
state = hass.states.get("sensor.test_duration")
|
||||
assert state.state == "1234"
|
||||
assert state.state == "20.5666666666667"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.MINUTES
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
|
||||
|
||||
state = hass.states.get("sensor.test_duration_in_traffic")
|
||||
assert state.state == "5678"
|
||||
assert state.state == "94.6333333333333"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTime.MINUTES
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) == SensorStateClass.MEASUREMENT
|
||||
|
||||
@@ -799,10 +799,12 @@ async def test_multiple_sections(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
duration = hass.states.get("sensor.test_duration")
|
||||
assert duration.state == "18"
|
||||
assert duration.state == "18.4833333333333"
|
||||
|
||||
assert float(hass.states.get("sensor.test_distance").state) == pytest.approx(3.583)
|
||||
assert hass.states.get("sensor.test_duration_in_traffic").state == "18"
|
||||
assert (
|
||||
hass.states.get("sensor.test_duration_in_traffic").state == "18.4833333333333"
|
||||
)
|
||||
assert hass.states.get("sensor.test_origin").state == "Chemin de Halage"
|
||||
assert (
|
||||
hass.states.get("sensor.test_origin").attributes.get(ATTR_LATITUDE)
|
||||
|
||||
@@ -10,7 +10,6 @@ import pytest
|
||||
|
||||
from homeassistant.components.homewizard.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
@@ -58,36 +57,6 @@ async def test_load_unload_v2(
|
||||
assert mock_config_entry_v2.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_load_unload_v2_as_v1(
|
||||
hass: HomeAssistant,
|
||||
mock_homewizardenergy: MagicMock,
|
||||
) -> None:
|
||||
"""Test loading and unloading of integration with v2 config, but without using it."""
|
||||
|
||||
# Simulate v2 config but as a P1 Meter
|
||||
mock_config_entry = MockConfigEntry(
|
||||
title="Device",
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_IP_ADDRESS: "127.0.0.1",
|
||||
CONF_TOKEN: "00112233445566778899ABCDEFABCDEF",
|
||||
},
|
||||
unique_id="HWE-P1_5c2fafabcdef",
|
||||
)
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
assert len(mock_homewizardenergy.combined.mock_calls) == 1
|
||||
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_load_failed_host_unavailable(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
@@ -243,7 +243,14 @@
|
||||
"1/29/1": [3, 29, 47, 144, 145, 156],
|
||||
"1/29/2": [],
|
||||
"1/29/3": [],
|
||||
"1/29/4": [],
|
||||
"1/29/4": [
|
||||
{
|
||||
"0": null,
|
||||
"1": 15,
|
||||
"2": 2,
|
||||
"3": "Solar"
|
||||
}
|
||||
],
|
||||
"1/29/65532": 0,
|
||||
"1/29/65533": 2,
|
||||
"1/29/65528": [],
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
"""Tests for the Meater integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Meater tests configuration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from meater.MeaterApi import MeaterCook, MeaterProbe
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.meater.const import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from .const import PROBE_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.meater.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_meater_client(mock_probe: Mock) -> Generator[AsyncMock]:
|
||||
"""Mock a Meater client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.meater.coordinator.MeaterApi",
|
||||
autospec=True,
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.meater.config_flow.MeaterApi",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
client.get_all_devices.return_value = [mock_probe]
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Mock a config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="Meater",
|
||||
data={CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"},
|
||||
unique_id="user@host.com",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cook() -> Mock:
|
||||
"""Mock a cook."""
|
||||
mock = Mock(spec=MeaterCook)
|
||||
mock.id = "123123"
|
||||
mock.name = "Whole chicken"
|
||||
mock.state = "Started"
|
||||
mock.target_temperature = 25.0
|
||||
mock.peak_temperature = 27.0
|
||||
mock.time_remaining = 32
|
||||
mock.time_elapsed = 32
|
||||
return mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_probe(mock_cook: Mock) -> Mock:
|
||||
"""Mock a probe."""
|
||||
mock = Mock(spec=MeaterProbe)
|
||||
mock.id = PROBE_ID
|
||||
mock.internal_temperature = 26.0
|
||||
mock.ambient_temperature = 28.0
|
||||
mock.cook = mock_cook
|
||||
mock.time_updated = datetime.fromisoformat("2025-06-16T13:53:51+00:00")
|
||||
return mock
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Constants for the Meater tests."""
|
||||
|
||||
PROBE_ID = "40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58"
|
||||
@@ -0,0 +1,20 @@
|
||||
# serializer version: 1
|
||||
# name: test_entry_diagnostics
|
||||
dict({
|
||||
'40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58': dict({
|
||||
'ambient_temperature': 28.0,
|
||||
'cook': dict({
|
||||
'id': '123123',
|
||||
'name': 'Whole chicken',
|
||||
'peak_temperature': 27.0,
|
||||
'state': 'Started',
|
||||
'target_temperature': 25.0,
|
||||
'time_elapsed': 32,
|
||||
'time_remaining': 32,
|
||||
}),
|
||||
'id': '40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58',
|
||||
'internal_temperature': 26.0,
|
||||
'time_updated': '2025-06-16T13:53:51+00:00',
|
||||
}),
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,34 @@
|
||||
# serializer version: 1
|
||||
# name: test_device_info
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': None,
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': None,
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'meater',
|
||||
'40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58',
|
||||
),
|
||||
}),
|
||||
'is_new': False,
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Apption Labs',
|
||||
'model': 'Meater Probe',
|
||||
'model_id': None,
|
||||
'name': 'Meater Probe 40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'suggested_area': None,
|
||||
'sw_version': None,
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,435 @@
|
||||
# serializer version: 1
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_ambient-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_ambient',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'meater',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'ambient',
|
||||
'unique_id': '40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58-ambient',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_ambient-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_ambient',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '28.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_name-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_name',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'meater',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_name',
|
||||
'unique_id': '40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58-cook_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_name-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_name',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Whole chicken',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_peak_temp-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_peak_temp',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'meater',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_peak_temp',
|
||||
'unique_id': '40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58-cook_peak_temp',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_peak_temp-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_peak_temp',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '27.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_state-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'not_started',
|
||||
'configured',
|
||||
'started',
|
||||
'ready_for_resting',
|
||||
'resting',
|
||||
'slightly_underdone',
|
||||
'finished',
|
||||
'slightly_overdone',
|
||||
'overcooked',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_state',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'meater',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_state',
|
||||
'unique_id': '40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58-cook_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_state-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'enum',
|
||||
'options': list([
|
||||
'not_started',
|
||||
'configured',
|
||||
'started',
|
||||
'ready_for_resting',
|
||||
'resting',
|
||||
'slightly_underdone',
|
||||
'finished',
|
||||
'slightly_overdone',
|
||||
'overcooked',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_state',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'started',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_target_temp-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_target_temp',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'meater',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_target_temp',
|
||||
'unique_id': '40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58-cook_target_temp',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_target_temp-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_target_temp',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '25.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_time_elapsed-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_time_elapsed',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'meater',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_time_elapsed',
|
||||
'unique_id': '40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58-cook_time_elapsed',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_time_elapsed-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_time_elapsed',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2023-10-20T23:59:28+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_time_remaining-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_time_remaining',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'meater',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'cook_time_remaining',
|
||||
'unique_id': '40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58-cook_time_remaining',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_time_remaining-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'timestamp',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_cook_time_remaining',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '2023-10-21T00:00:32+00:00',
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_internal-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_internal',
|
||||
'has_entity_name': False,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'meater',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'internal',
|
||||
'unique_id': '40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58-internal',
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_entities[sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_internal-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'temperature',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.meater_40a72384fa80349314dfd97c84b73a5c1c9da57b59e26d67b573d618fe0d6e58_internal',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '26.0',
|
||||
})
|
||||
# ---
|
||||
@@ -1,12 +1,12 @@
|
||||
"""Define tests for the Meater config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from meater import AuthenticationError, ServiceUnavailableError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.meater import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@@ -14,132 +14,114 @@ from homeassistant.data_entry_flow import FlowResultType
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client():
|
||||
"""Define a fixture for authentication coroutine."""
|
||||
return AsyncMock(return_value=None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_meater(mock_client):
|
||||
"""Mock the meater library."""
|
||||
with patch("homeassistant.components.meater.MeaterApi.authenticate") as mock_:
|
||||
mock_.side_effect = mock_client
|
||||
yield mock_
|
||||
|
||||
|
||||
async def test_duplicate_error(hass: HomeAssistant) -> None:
|
||||
"""Test that errors are shown when duplicates are added."""
|
||||
conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
|
||||
|
||||
MockConfigEntry(domain=DOMAIN, unique_id="user@host.com", data=conf).add_to_hass(
|
||||
hass
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_client", [AsyncMock(side_effect=Exception)])
|
||||
async def test_unknown_auth_error(hass: HomeAssistant, mock_meater) -> None:
|
||||
"""Test that an invalid API/App Key throws an error."""
|
||||
conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf
|
||||
)
|
||||
assert result["errors"] == {"base": "unknown_auth_error"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mock_client", [AsyncMock(side_effect=AuthenticationError)])
|
||||
async def test_invalid_credentials(hass: HomeAssistant, mock_meater) -> None:
|
||||
"""Test that an invalid API/App Key throws an error."""
|
||||
conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf
|
||||
)
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_client", [AsyncMock(side_effect=ServiceUnavailableError)]
|
||||
)
|
||||
async def test_service_unavailable(hass: HomeAssistant, mock_meater) -> None:
|
||||
"""Test that an invalid API/App Key throws an error."""
|
||||
conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=conf
|
||||
)
|
||||
assert result["errors"] == {"base": "service_unavailable_error"}
|
||||
|
||||
|
||||
async def test_user_flow(hass: HomeAssistant, mock_meater) -> None:
|
||||
async def test_user_flow(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_meater_client: AsyncMock
|
||||
) -> None:
|
||||
"""Test that the user flow works."""
|
||||
conf = {CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"}
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.meater.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], conf)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
CONF_USERNAME: "user@host.com",
|
||||
CONF_PASSWORD: "password123",
|
||||
}
|
||||
assert result["result"].unique_id == "user@host.com"
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert config_entry.data == {
|
||||
CONF_USERNAME: "user@host.com",
|
||||
CONF_PASSWORD: "password123",
|
||||
}
|
||||
|
||||
|
||||
async def test_reauth_flow(hass: HomeAssistant, mock_meater) -> None:
|
||||
"""Test that the reauth flow works."""
|
||||
data = {
|
||||
CONF_USERNAME: "user@host.com",
|
||||
CONF_PASSWORD: "password123",
|
||||
}
|
||||
mock_config = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="user@host.com",
|
||||
data=data,
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(AuthenticationError, "invalid_auth"),
|
||||
(ServiceUnavailableError, "service_unavailable_error"),
|
||||
(Exception, "unknown_auth_error"),
|
||||
],
|
||||
)
|
||||
async def test_user_flow_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_meater_client: AsyncMock,
|
||||
exception: Exception,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test that an invalid API/App Key throws an error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
mock_config.add_to_hass(hass)
|
||||
|
||||
result = await mock_config.start_reauth_flow(hass)
|
||||
mock_meater_client.authenticate.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": error}
|
||||
|
||||
mock_meater_client.authenticate.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_duplicate_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_meater_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that errors are shown when duplicates are added."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: "user@host.com", CONF_PASSWORD: "password123"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_reauth_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_meater_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test that the reauth flow works."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] is None
|
||||
assert not result["errors"]
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"password": "passwordabc"},
|
||||
{CONF_PASSWORD: "passwordabc"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
|
||||
config_entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
assert config_entry.data == {
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert mock_config_entry.data == {
|
||||
CONF_USERNAME: "user@host.com",
|
||||
CONF_PASSWORD: "passwordabc",
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Test Meater diagnostics."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
async def test_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_meater_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test config entry diagnostics."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
assert (
|
||||
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
|
||||
== snapshot
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Tests for the Meater integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.meater.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import setup_integration
|
||||
from .const import PROBE_ID
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_device_info(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_meater_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
) -> None:
|
||||
"""Test device registry integration."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
device_entry = device_registry.async_get_device(identifiers={(DOMAIN, PROBE_ID)})
|
||||
assert device_entry is not None
|
||||
assert device_entry == snapshot
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Tests for the Meater sensors."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-10-21")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_meater_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the sensor entities."""
|
||||
with patch("homeassistant.components.meater.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
@@ -152,7 +152,9 @@ def test_support_properties(hass: HomeAssistant, property_suffix: str) -> None:
|
||||
entity4 = MediaPlayerEntity()
|
||||
entity4.hass = hass
|
||||
entity4.platform = MockEntityPlatform(hass)
|
||||
entity4._attr_supported_features = all_features - feature
|
||||
entity4._attr_supported_features = media_player.MediaPlayerEntityFeature(
|
||||
all_features - feature
|
||||
)
|
||||
|
||||
assert getattr(entity1, f"support_{property_suffix}") is False
|
||||
assert getattr(entity2, f"support_{property_suffix}") is True
|
||||
@@ -652,27 +654,3 @@ async def test_get_async_get_browse_image_quoting(
|
||||
url = player.get_browse_image_url("album", media_content_id)
|
||||
await client.get(url)
|
||||
mock_browse_image.assert_called_with("album", media_content_id, None)
|
||||
|
||||
|
||||
def test_deprecated_supported_features_ints(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Test deprecated supported features ints."""
|
||||
|
||||
class MockMediaPlayerEntity(MediaPlayerEntity):
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
"""Return supported features."""
|
||||
return 1
|
||||
|
||||
entity = MockMediaPlayerEntity()
|
||||
entity.hass = hass
|
||||
entity.platform = MockEntityPlatform(hass)
|
||||
assert entity.supported_features_compat is MediaPlayerEntityFeature(1)
|
||||
assert "MockMediaPlayerEntity" in caplog.text
|
||||
assert "is using deprecated supported features values" in caplog.text
|
||||
assert "Instead it should use" in caplog.text
|
||||
assert "MediaPlayerEntityFeature.PAUSE" in caplog.text
|
||||
caplog.clear()
|
||||
assert entity.supported_features_compat is MediaPlayerEntityFeature(1)
|
||||
assert "is using deprecated supported features values" not in caplog.text
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -71,7 +71,6 @@ from .common import (
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_capture_events,
|
||||
async_fire_mqtt_message,
|
||||
async_fire_time_changed,
|
||||
mock_restore_cache_with_extra_data,
|
||||
@@ -892,48 +891,11 @@ async def test_invalid_unit_of_measurement(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test device_class with invalid unit of measurement."""
|
||||
events = async_capture_events(hass, ir.EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED)
|
||||
assert await mqtt_mock_entry()
|
||||
assert (
|
||||
"The unit of measurement `ppm` is not valid together with device class `energy`"
|
||||
"The unit of measurement 'ppm' is not valid together with device class 'energy'"
|
||||
in caplog.text
|
||||
)
|
||||
# A repair issue was logged
|
||||
assert len(events) == 1
|
||||
assert events[0].data["issue_id"] == "sensor.test"
|
||||
# Assert the sensor works
|
||||
async_fire_mqtt_message(hass, "test-topic", "100")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.test")
|
||||
assert state is not None
|
||||
assert state.state == "100"
|
||||
|
||||
caplog.clear()
|
||||
|
||||
discovery_payload = {
|
||||
"name": "bla",
|
||||
"state_topic": "test-topic2",
|
||||
"device_class": "temperature",
|
||||
"unit_of_measurement": "C",
|
||||
}
|
||||
# Now discover an other invalid sensor
|
||||
async_fire_mqtt_message(
|
||||
hass, "homeassistant/sensor/bla/config", json.dumps(discovery_payload)
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert (
|
||||
"The unit of measurement `C` is not valid together with device class `temperature`"
|
||||
in caplog.text
|
||||
)
|
||||
# Assert the sensor works
|
||||
async_fire_mqtt_message(hass, "test-topic2", "21")
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("sensor.bla")
|
||||
assert state is not None
|
||||
assert state.state == "21"
|
||||
|
||||
# No new issue was registered for the discovered entity
|
||||
assert len(events) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"""Constants for the Paperless NGX integration tests."""
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
|
||||
|
||||
USER_INPUT_ONE = {
|
||||
CONF_URL: "https://192.168.69.16:8000",
|
||||
CONF_API_KEY: "12345678",
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
|
||||
USER_INPUT_TWO = {
|
||||
CONF_URL: "https://paperless.example.de",
|
||||
CONF_API_KEY: "87654321",
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
|
||||
USER_INPUT_REAUTH = {CONF_API_KEY: "192837465"}
|
||||
|
||||
@@ -62,6 +62,100 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
def _init_host_mock(host_mock: MagicMock) -> None:
|
||||
host_mock.get_host_data = AsyncMock(return_value=None)
|
||||
host_mock.get_states = AsyncMock(return_value=None)
|
||||
host_mock.check_new_firmware = AsyncMock(return_value=False)
|
||||
host_mock.unsubscribe = AsyncMock(return_value=True)
|
||||
host_mock.logout = AsyncMock(return_value=True)
|
||||
host_mock.reboot = AsyncMock()
|
||||
host_mock.set_ptz_command = AsyncMock()
|
||||
host_mock.is_nvr = True
|
||||
host_mock.is_hub = False
|
||||
host_mock.mac_address = TEST_MAC
|
||||
host_mock.uid = TEST_UID
|
||||
host_mock.onvif_enabled = True
|
||||
host_mock.rtmp_enabled = True
|
||||
host_mock.rtsp_enabled = True
|
||||
host_mock.nvr_name = TEST_NVR_NAME
|
||||
host_mock.port = TEST_PORT
|
||||
host_mock.use_https = TEST_USE_HTTPS
|
||||
host_mock.is_admin = True
|
||||
host_mock.user_level = "admin"
|
||||
host_mock.protocol = "rtsp"
|
||||
host_mock.channels = [0]
|
||||
host_mock.stream_channels = [0]
|
||||
host_mock.new_devices = False
|
||||
host_mock.sw_version_update_required = False
|
||||
host_mock.hardware_version = "IPC_00000"
|
||||
host_mock.sw_version = "v1.0.0.0.0.0000"
|
||||
host_mock.sw_upload_progress.return_value = 100
|
||||
host_mock.manufacturer = "Reolink"
|
||||
host_mock.model = TEST_HOST_MODEL
|
||||
host_mock.supported.return_value = True
|
||||
host_mock.item_number.return_value = TEST_ITEM_NUMBER
|
||||
host_mock.camera_model.return_value = TEST_CAM_MODEL
|
||||
host_mock.camera_name.return_value = TEST_NVR_NAME
|
||||
host_mock.camera_hardware_version.return_value = "IPC_00001"
|
||||
host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000"
|
||||
host_mock.camera_sw_version_update_required.return_value = False
|
||||
host_mock.camera_uid.return_value = TEST_UID_CAM
|
||||
host_mock.camera_online.return_value = True
|
||||
host_mock.channel_for_uid.return_value = 0
|
||||
host_mock.get_encoding.return_value = "h264"
|
||||
host_mock.firmware_update_available.return_value = False
|
||||
host_mock.session_active = True
|
||||
host_mock.timeout = 60
|
||||
host_mock.renewtimer.return_value = 600
|
||||
host_mock.wifi_connection = False
|
||||
host_mock.wifi_signal = None
|
||||
host_mock.whiteled_mode_list.return_value = []
|
||||
host_mock.zoom_range.return_value = {
|
||||
"zoom": {"pos": {"min": 0, "max": 100}},
|
||||
"focus": {"pos": {"min": 0, "max": 100}},
|
||||
}
|
||||
host_mock.capabilities = {"Host": ["RTSP"], "0": ["motion_detection"]}
|
||||
host_mock.checked_api_versions = {"GetEvents": 1}
|
||||
host_mock.abilities = {"abilityChn": [{"aiTrack": {"permit": 0, "ver": 0}}]}
|
||||
host_mock.get_raw_host_data.return_value = (
|
||||
"{'host':'TEST_RESPONSE','channel':'TEST_RESPONSE'}"
|
||||
)
|
||||
|
||||
# enums
|
||||
host_mock.whiteled_mode.return_value = 1
|
||||
host_mock.whiteled_mode_list.return_value = ["off", "auto"]
|
||||
host_mock.doorbell_led.return_value = "Off"
|
||||
host_mock.doorbell_led_list.return_value = ["stayoff", "auto"]
|
||||
host_mock.auto_track_method.return_value = 3
|
||||
host_mock.daynight_state.return_value = "Black&White"
|
||||
host_mock.hub_alarm_tone_id.return_value = 1
|
||||
host_mock.hub_visitor_tone_id.return_value = 1
|
||||
host_mock.recording_packing_time_list = ["30 Minutes", "60 Minutes"]
|
||||
host_mock.recording_packing_time = "60 Minutes"
|
||||
|
||||
# Baichuan
|
||||
host_mock.baichuan_only = False
|
||||
# Disable tcp push by default for tests
|
||||
host_mock.baichuan.port = TEST_BC_PORT
|
||||
host_mock.baichuan.events_active = False
|
||||
host_mock.baichuan.unsubscribe_events = AsyncMock()
|
||||
host_mock.baichuan.check_subscribe_events = AsyncMock()
|
||||
host_mock.baichuan.mac_address.return_value = TEST_MAC_CAM
|
||||
host_mock.baichuan.privacy_mode.return_value = False
|
||||
host_mock.baichuan.day_night_state.return_value = "day"
|
||||
host_mock.baichuan.subscribe_events.side_effect = ReolinkError("Test error")
|
||||
host_mock.baichuan.active_scene = "off"
|
||||
host_mock.baichuan.scene_names = ["off", "home"]
|
||||
host_mock.baichuan.abilities = {
|
||||
0: {"chnID": 0, "aitype": 34615},
|
||||
"Host": {"pushAlarm": 7},
|
||||
}
|
||||
host_mock.baichuan.smart_location_list.return_value = [0]
|
||||
host_mock.baichuan.smart_ai_type_list.return_value = ["people"]
|
||||
host_mock.baichuan.smart_ai_index.return_value = 1
|
||||
host_mock.baichuan.smart_ai_name.return_value = "zone1"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def reolink_connect_class() -> Generator[MagicMock]:
|
||||
"""Mock reolink connection and return both the host_mock and host_mock_class."""
|
||||
@@ -71,97 +165,8 @@ def reolink_connect_class() -> Generator[MagicMock]:
|
||||
) as host_mock_class,
|
||||
):
|
||||
host_mock = host_mock_class.return_value
|
||||
host_mock.get_host_data.return_value = None
|
||||
host_mock.get_states.return_value = None
|
||||
host_mock.supported.return_value = True
|
||||
host_mock.check_new_firmware.return_value = False
|
||||
host_mock.unsubscribe.return_value = True
|
||||
host_mock.logout.return_value = True
|
||||
host_mock.is_nvr = True
|
||||
host_mock.is_hub = False
|
||||
host_mock.mac_address = TEST_MAC
|
||||
host_mock.uid = TEST_UID
|
||||
host_mock.onvif_enabled = True
|
||||
host_mock.rtmp_enabled = True
|
||||
host_mock.rtsp_enabled = True
|
||||
host_mock.nvr_name = TEST_NVR_NAME
|
||||
host_mock.port = TEST_PORT
|
||||
host_mock.use_https = TEST_USE_HTTPS
|
||||
host_mock.is_admin = True
|
||||
host_mock.user_level = "admin"
|
||||
host_mock.protocol = "rtsp"
|
||||
host_mock.channels = [0]
|
||||
host_mock.stream_channels = [0]
|
||||
host_mock.new_devices = False
|
||||
host_mock.sw_version_update_required = False
|
||||
host_mock.hardware_version = "IPC_00000"
|
||||
host_mock.sw_version = "v1.0.0.0.0.0000"
|
||||
host_mock.sw_upload_progress.return_value = 100
|
||||
host_mock.manufacturer = "Reolink"
|
||||
host_mock.model = TEST_HOST_MODEL
|
||||
host_mock.item_number.return_value = TEST_ITEM_NUMBER
|
||||
host_mock.camera_model.return_value = TEST_CAM_MODEL
|
||||
host_mock.camera_name.return_value = TEST_NVR_NAME
|
||||
host_mock.camera_hardware_version.return_value = "IPC_00001"
|
||||
host_mock.camera_sw_version.return_value = "v1.1.0.0.0.0000"
|
||||
host_mock.camera_sw_version_update_required.return_value = False
|
||||
host_mock.camera_uid.return_value = TEST_UID_CAM
|
||||
host_mock.camera_online.return_value = True
|
||||
host_mock.channel_for_uid.return_value = 0
|
||||
host_mock.get_encoding.return_value = "h264"
|
||||
host_mock.firmware_update_available.return_value = False
|
||||
host_mock.session_active = True
|
||||
host_mock.timeout = 60
|
||||
host_mock.renewtimer.return_value = 600
|
||||
host_mock.wifi_connection = False
|
||||
host_mock.wifi_signal = None
|
||||
host_mock.whiteled_mode_list.return_value = []
|
||||
host_mock.zoom_range.return_value = {
|
||||
"zoom": {"pos": {"min": 0, "max": 100}},
|
||||
"focus": {"pos": {"min": 0, "max": 100}},
|
||||
}
|
||||
host_mock.capabilities = {"Host": ["RTSP"], "0": ["motion_detection"]}
|
||||
host_mock.checked_api_versions = {"GetEvents": 1}
|
||||
host_mock.abilities = {"abilityChn": [{"aiTrack": {"permit": 0, "ver": 0}}]}
|
||||
host_mock.get_raw_host_data.return_value = (
|
||||
"{'host':'TEST_RESPONSE','channel':'TEST_RESPONSE'}"
|
||||
)
|
||||
|
||||
reolink_connect.chime_list = []
|
||||
|
||||
# enums
|
||||
host_mock.whiteled_mode.return_value = 1
|
||||
host_mock.whiteled_mode_list.return_value = ["off", "auto"]
|
||||
host_mock.doorbell_led.return_value = "Off"
|
||||
host_mock.doorbell_led_list.return_value = ["stayoff", "auto"]
|
||||
host_mock.auto_track_method.return_value = 3
|
||||
host_mock.daynight_state.return_value = "Black&White"
|
||||
host_mock.hub_alarm_tone_id.return_value = 1
|
||||
host_mock.hub_visitor_tone_id.return_value = 1
|
||||
host_mock.recording_packing_time_list = ["30 Minutes", "60 Minutes"]
|
||||
host_mock.recording_packing_time = "60 Minutes"
|
||||
|
||||
# Baichuan
|
||||
host_mock.baichuan = create_autospec(Baichuan)
|
||||
host_mock.baichuan_only = False
|
||||
# Disable tcp push by default for tests
|
||||
host_mock.baichuan.port = TEST_BC_PORT
|
||||
host_mock.baichuan.events_active = False
|
||||
host_mock.baichuan.mac_address.return_value = TEST_MAC_CAM
|
||||
host_mock.baichuan.privacy_mode.return_value = False
|
||||
host_mock.baichuan.day_night_state.return_value = "day"
|
||||
host_mock.baichuan.subscribe_events.side_effect = ReolinkError("Test error")
|
||||
host_mock.baichuan.active_scene = "off"
|
||||
host_mock.baichuan.scene_names = ["off", "home"]
|
||||
host_mock.baichuan.abilities = {
|
||||
0: {"chnID": 0, "aitype": 34615},
|
||||
"Host": {"pushAlarm": 7},
|
||||
}
|
||||
host_mock.baichuan.smart_location_list.return_value = [0]
|
||||
host_mock.baichuan.smart_ai_type_list.return_value = ["people"]
|
||||
host_mock.baichuan.smart_ai_index.return_value = 1
|
||||
host_mock.baichuan.smart_ai_name.return_value = "zone1"
|
||||
|
||||
_init_host_mock(host_mock)
|
||||
yield host_mock_class
|
||||
|
||||
|
||||
@@ -173,6 +178,18 @@ def reolink_connect(
|
||||
return reolink_connect_class.return_value
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reolink_host() -> Generator[MagicMock]:
|
||||
"""Mock reolink Host class."""
|
||||
with patch(
|
||||
"homeassistant.components.reolink.host.Host", autospec=False
|
||||
) as host_mock_class:
|
||||
host_mock = host_mock_class.return_value
|
||||
host_mock.baichuan = MagicMock()
|
||||
_init_host_mock(host_mock)
|
||||
yield host_mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def reolink_platforms() -> Generator[None]:
|
||||
"""Mock reolink entry setup."""
|
||||
|
||||
@@ -148,6 +148,10 @@
|
||||
'0': 1,
|
||||
'null': 1,
|
||||
}),
|
||||
'GetMask': dict({
|
||||
'0': 1,
|
||||
'null': 1,
|
||||
}),
|
||||
'GetMdAlarm': dict({
|
||||
'0': 1,
|
||||
'null': 1,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user