Compare commits

...

54 Commits

Author SHA1 Message Date
G Johansson 7411e54b2c Remove deprecated use of incorrect UoM with device class in mqtt sensor 2025-06-17 16:37:47 +00:00
Abílio Costa ed9503324d Fix flaky Reolink webhook test (#147036) 2025-06-17 17:18:48 +01:00
Allen Porter 22a06a6c2e Bump ical to 10.0.4 (#147005)
* Bump ical to 10.0.4

* Bump ical to 10.0.4 in google
2025-06-17 07:06:51 -07:00
Michael Hansen 3b611b9b03 Add TTS response timeout for idle state (#146984)
* Add TTS response timeout for idle state

* Consider time spent sending TTS audio in timeout
2025-06-17 09:39:18 -04:00
Noah Husby 79cc3bffc6 Bump aiorussound to 4.6.0 (#147023) 2025-06-17 14:40:56 +02:00
Martin Hjelmare 5c455304a5 Disable Z-Wave indidator CC entities by default (#147018)
* Update discovery tests

* Disable Z-Wave indidator CC entities by default
2025-06-17 15:39:22 +03:00
Erik Montnemery 058f860be7 Fix incorrect use of zip in service.async_get_all_descriptions (#147013)
* Fix incorrect use of zip in service.async_get_all_descriptions

* Fix lint errors in test
2025-06-17 14:24:31 +02:00
Joost Lekkerkerker ef319c966d Bump nextcord to 3.1.0 (#147020) 2025-06-17 14:11:55 +02:00
Robin Lintermann adc4e9fdc1 Bump pysmarlaapi version to 0.9.0 (#146629)
Bump pysmarlaapi version
Fix default values of entities
2025-06-17 11:23:50 +02:00
Maciej Bieniek 40a00fb790 Address late review for NextDNS integration (#146980)
key instead of Key
2025-06-17 11:23:03 +02:00
G Johansson 0926b16095 Remove deprecated support feature values in cover (#146987) 2025-06-17 10:46:08 +02:00
G Johansson 308c89af4a Remove deprecated support feature values in media_player (#146986) 2025-06-17 10:33:41 +02:00
G Johansson b0c2a47288 Remove deprecated support feature values in vacuum (#146982) 2025-06-17 10:32:58 +02:00
Joost Lekkerkerker c446cce2cc Bump pySmartThings to 3.2.5 (#146983) 2025-06-16 22:44:14 +01:00
Abílio Costa e02267ad89 Improve bootstrap file logging test (#146670) 2025-06-16 21:55:16 +01:00
Thomas55555 36381e6753 Bump aioautomower to 2025.6.0 (#146979) 2025-06-16 22:52:23 +02:00
Manu 6533562f4e Rename Xiaomi Miio integration to Xiaomi Home (#146555)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-16 21:51:54 +01:00
Ludovic BOUÉ 1bc6ea98ce Set Matter SolarPower tagList in fixture (#146837)
Update solar_power.json

Set tagList to [{"0":null,"1":15,"2":2,"3":"Solar"}]
2025-06-16 22:46:27 +02:00
elmurato bab34b844b Fix blocking open in Minecraft Server (#146820)
Fix blocking open by dnspython
2025-06-16 22:46:11 +02:00
Etienne C. ad3dac0373 Removed rounding of durations in Here Travel Time sensors (#146838)
* Removed rounding of durations

* Set duration sensors unit to seconds

* Updated Here Travel Time tests

* Update homeassistant/components/here_travel_time/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update homeassistant/components/here_travel_time/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Updated Here Travel Time tests

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-06-16 22:20:01 +02:00
Maciej Bieniek c5d93e5456 Fix translation key in NextDNS integration (#146976)
* Fix translation key

* Better wording
2025-06-16 21:37:19 +02:00
J. Diego Rodríguez Royo ef9b46dce5 Record current IQS state for Home Connect (#131703)
* Home Connect quality scale

* Update current iqs

* Docs rules done

* parallel-updates rule

* Complete appropriate-polling's comment

* Apply suggestions

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-06-16 21:30:06 +02:00
Abílio Costa 6f3ceb83c2 Use non-autospec mock for Reolink's button tests (#146969) 2025-06-16 21:14:02 +02:00
Joost Lekkerkerker 589577a04c Add diagnostics support to Meater (#146967) 2025-06-16 20:17:30 +02:00
Joost Lekkerkerker cb21bb6542 Make Meater cook state an enum (#146958) 2025-06-16 19:13:34 +01:00
mswilson ad64139b8e Add switch for Samsung ice bites (and rename ice maker) (#146925)
* Add switch for ice bites (and rename ice maker)

Fixes: home-assistant/home-assistant.io#37826

* Fix tests

* Fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-06-16 19:31:49 +02:00
Joost Lekkerkerker 9ae0cfc7e5 Create entities directly on setup in Meater (#146953)
* Don't wait an update when adding devices in Meater

* Fix
2025-06-16 18:23:20 +02:00
Joost Lekkerkerker dffaf49eca Use runtime data in Meater (#146961) 2025-06-16 17:18:21 +02:00
Maciej Bieniek 4add783108 Use entity base class for NextDNS entities (#146934)
* Add entity module

* Add NextDnsEntityDescription class

* Remove NextDnsEntityDescription

* Create DeviceInfo in entity module

* Use property
2025-06-16 16:58:47 +02:00
Joost Lekkerkerker 421251308f Add Meater sensor tests (#146952) 2025-06-16 16:19:35 +02:00
Aviad Levy cce878213f Add Telegram Bot message reactions (#146354) 2025-06-16 14:48:59 +01:00
Joost Lekkerkerker 664441eaec Improve Meater config flow tests (#146951) 2025-06-16 15:40:43 +02:00
Maciej Bieniek d4686a3cce Add config flow data description for NextDNS (#146938)
* Add config flow data description

* Better wording
2025-06-16 15:28:25 +02:00
Hessel 6e92247799 Fix missing key for ecosmart in older Wallbox models (#146847)
* fix 146839, missing key

* added tests for this issue

* added tests for this issue

* added tests for this issue, formatting

* Prevent loading select on missing key

* Prevent loading select on missing key - formatting fixed

* Update homeassistant/components/wallbox/coordinator.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-06-16 15:15:17 +02:00
Etienne C. f5355c833e Add duration device class in Here Travel Time sensors (#146804) 2025-06-16 15:14:43 +02:00
Joost Lekkerkerker add9f4c5ab Move Meater coordinator to module (#146946)
* Move Meater coordinator to module

* Fix tests
2025-06-16 14:48:44 +02:00
starkillerOG 38973fe64a Add Reolink privacy mask switch (#146906) 2025-06-16 14:40:19 +02:00
epenet d657964729 Simplify habitica service actions (#146746) 2025-06-16 14:37:38 +02:00
Nathan Spencer 25c408484c Set goalzero total run time sensor device class to duration (#146897) 2025-06-16 14:35:56 +02:00
Florian von Garrel c335b5b37c Add verify ssl option to paperless-ngx integration (#146802)
* add verify ssl config option

* Refactoring

* Use .get() with default value instead of migration

* Reconfigure fix

* minor changes
2025-06-16 14:31:22 +02:00
Josef Zweck 61b00892c3 Add debug log for update in onedrive (#146907) 2025-06-16 14:17:36 +02:00
Maciej Bieniek e47e2c92fe Change PARALLEL_UPDATES to 0 for read-only NextDNS platforms (#146939)
Change PARALLEL_UPDATES to 0 for read-only platforms
2025-06-16 14:11:48 +02:00
Duco Sebel 3283965b45 Re-enable v2 API support for HomeWizard P1 Meter (#146927) 2025-06-16 14:11:35 +02:00
epenet 4a9cbc79f2 Bump pysml to 0.1.5 (#146935) 2025-06-16 12:56:03 +01:00
epenet 33978ce59e Bump pyosoenergyapi to 1.1.5 (#146942) 2025-06-16 12:46:38 +01:00
epenet d5262231a1 Bump pymysensors to 0.25.0 (#146941) 2025-06-16 13:37:39 +02:00
Brett Adams b563f9078a Significantly improve Tesla Fleet config flow (#146794)
* Improved config flow

* Tests

* Improvements

* Dashboard url & tests

* Apply suggestions from code review

Co-authored-by: Norbert Rittel <norbert@rittel.de>

* revert oauth change

* fully restore oauth file

* remove CONF_DOMAIN

* Add pick_implementation back in

* Use try else

* Improve translation

* use CONF_DOMAIN

---------

Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-06-16 13:29:17 +02:00
epenet e8667dfbe0 Bump nessclient to 1.2.0 (#146937) 2025-06-16 12:11:57 +01:00
dependabot[bot] 8d4f5d78ff Bump dawidd6/action-download-artifact from 10 to 11 (#146928)
Bumps [dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact) from 10 to 11.
- [Release notes](https://github.com/dawidd6/action-download-artifact/releases)
- [Commits](https://github.com/dawidd6/action-download-artifact/compare/v10...v11)

---
updated-dependencies:
- dependency-name: dawidd6/action-download-artifact
  dependency-version: '11'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 10:42:10 +02:00
mbo18 e354a850c9 Bump python-rflink to 0.0.67 (#146908)
* update python-rflink

* remove from FORBIDDEN_PACKAGE_EXCEPTIONS
2025-06-16 10:36:20 +02:00
Ernst Klamer 5ea026d369 Bump bthome-ble to 3.13.1 (#146871) 2025-06-16 11:29:00 +03:00
Brett Adams ddfe17d0a4 Bump tesla-fleet-api to match Protobuf compatibility (#146918)
Bump for v1.2.0
2025-06-16 10:12:34 +02:00
Yuxin Wang 85aa7bef1e Add sensor categorizations for APCUPSD (#146863)
* Add sensor categorizations

* Fix snapshot problem

* Fix snapshot problem
2025-06-16 08:43:31 +02:00
Paulus Schoutsen 8498928e47 Move Google Gen AI fixture to allow reuse (#146921) 2025-06-15 23:00:27 -04:00
119 changed files with 3740 additions and 1657 deletions
+2 -2
View File
@@ -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"]
}
+1 -1
View File
@@ -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"]
}
+8 -73
View File
@@ -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()
}
+22 -15
View File
@@ -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
+12 -1
View File
@@ -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)
+3 -6
View File
@@ -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 -19
View File
@@ -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
+10 -25
View File
@@ -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)
+10 -1
View File
@@ -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%]"
}
}
},
+3 -9
View File
@@ -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 youre 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."]
}
+2 -1
View File
@@ -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"]
}
+3 -2
View File
@@ -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."""
+1 -1
View File
@@ -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"]
}
+2 -15
View File
@@ -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
+8 -8
View File
@@ -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
+1 -1
View File
@@ -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",
+6 -4
View File
@@ -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(
+13 -13
View File
@@ -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
+13 -13
View File
@@ -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
-1
View File
@@ -480,7 +480,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"hko",
"hlk_sw16",
"holiday",
"home_connect",
"homekit",
"homekit_controller",
"homematic",
-25
View File
@@ -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,
+1 -28
View File
@@ -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
+1 -1
View File
@@ -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)
-31
View File
@@ -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": [],
+12
View File
@@ -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()
+80
View File
@@ -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
+3
View File
@@ -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',
})
# ---
+86 -104
View File
@@ -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
)
+28
View File
@@ -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
+29
View File
@@ -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)
+3 -25
View File
@@ -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
+2 -40
View File
@@ -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(
+3 -1
View File
@@ -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"}
+107 -90
View File
@@ -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