Compare commits

...

85 Commits

Author SHA1 Message Date
Bram Kragten
36ec1b33fe 2024.9.0 (#124880) 2024-09-04 17:41:08 +02:00
Bram Kragten
84a0a28be2 Bump version to 2024.9.0 2024-09-04 17:08:18 +02:00
Michael Hansen
ac19ee3e2e Bump intents to 2024.9.4 (#125232) 2024-09-04 17:07:21 +02:00
Denis Shulyaka
438af042ed Update Anthropic default model to Haiku (#125225) 2024-09-04 17:07:20 +02:00
Robert Resch
122f11c790 Update modified_at datetime on storage collection changes (#125218) 2024-09-04 17:07:19 +02:00
Bram Kragten
de99dfef4e Bump version to 2024.9.0b5 2024-09-04 11:48:24 +02:00
Robert Resch
bcdc3563a5 Bump deebot-client to 8.4.0 (#125207) 2024-09-04 11:48:16 +02:00
Bram Kragten
9ef0a1f0a2 Update frontend to 20240904.0 (#125206) 2024-09-04 11:48:15 +02:00
Matthias Alphart
d0629d4e66 Update knx-frontend to 2024.9.4.64538 (#125196) 2024-09-04 11:48:15 +02:00
G Johansson
65e98eab9c Bump python-holidays to 0.56 (#125182) 2024-09-04 11:48:14 +02:00
J. Nick Koston
a0d9764443 Bump yalexs to 8.6.3 (#125176)
Fixes the battery state not refreshing due to a refactoring
error in the library.

changelog: https://github.com/bdraco/yalexs/compare/v8.6.2...v8.6.3
2024-09-04 11:48:13 +02:00
Joakim Plate
8293f270df Update gardena_bluetooth dependency to 1.4.3 (#125175) 2024-09-04 11:48:12 +02:00
Bram Kragten
116090bff1 Bump version to 2024.9.0b4 2024-09-03 21:12:20 +02:00
J. Nick Koston
6082220f7f Bump yalexs to 8.6.2 (#125162)
changelog: https://github.com/bdraco/yalexs/compare/v8.6.0...v8.6.2
2024-09-03 21:11:30 +02:00
Paul Bottein
74fd16b953 Update frontend to 20240903.1 (#125160) 2024-09-03 21:11:30 +02:00
Andrew Jackson
82cffcbc23 Bump aiomealie to 0.9.2 (#125153)
Bump mealie version
2024-09-03 21:11:29 +02:00
Erik Montnemery
4e1a77326e Restore unnecessary assignment of Template.hass in event helper (#125143) 2024-09-03 21:11:28 +02:00
Bram Kragten
54cf52069e Log deprecation warning when template.Template is created without hass (#125142)
* Log deprecation warning when template.Template is created without hass

* Improve docstring
2024-09-03 21:09:12 +02:00
Erik Montnemery
70b811096c Log deprecation warning when cv.template is called from wrong thread (#125141)
Log deprecation warning when cv.template is called from wrong thread
2024-09-03 19:55:38 +02:00
Marcel van der Veldt
1efd267ee6 Fix energy sensor for ThirdReality Matter powerplug (#125140) 2024-09-03 19:55:38 +02:00
Christopher Fenner
31267b4095 Correct device serial for ViCare integration (#125125)
* expose correct serial

* adapt inits

* adjust _build_entities

* adapt inits

* add serial data point

* update snapshot

* apply suggestions

* apply suggestions
2024-09-03 19:55:37 +02:00
Steven B.
4982e1cbcf Pass hass clientsession to ring config flow (#125119)
Pass hass clientsession to ring config flow
2024-09-03 19:55:36 +02:00
Artur Pragacz
be3b16b7fa Fix Onkyo action select_hdmi_output (#125115)
* Fix Onkyo service select_hdmi_output

* Move Hasskey directly under Onkyo domain
2024-09-03 19:55:35 +02:00
Aaron Bach
393a0ac0df Fix unhandled exception with missing IQVIA data (#125114) 2024-09-03 19:55:35 +02:00
Michal Jál
a0bbcb0401 Bump PySwitchbot to 0.48.2 (#125113) 2024-09-03 19:55:34 +02:00
J. Nick Koston
3f65bc78e8 Bump yalexs to 8.6.0 (#125102) 2024-09-03 19:55:33 +02:00
Avi Miller
d005440544 Bump aiolifx to 1.0.9 and remove unused HomeKit model prefixes (#125055)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-09-03 19:55:32 +02:00
Artur Pragacz
94d2da1685 Fix area registry indexing when there is a name collision (#125050) 2024-09-03 19:55:31 +02:00
ilan
4c5ba0617a Bump py-madvr2 to 1.6.32 (#125049)
feat: update lib
2024-09-03 19:55:31 +02:00
Richard Kroegel
a58bf149fc Fix blocking calls for OpenAI conversation (#125010) 2024-09-03 19:55:30 +02:00
MJJ
005be4e8ba Increase timeout for fetching buienradar weather data (#124597)
Increase timeout for fetching weather data
2024-09-03 19:55:29 +02:00
Allen Porter
b81d7a0ed8 Update nest to only include the image attachment payload for cameras that support fetching media (#124590)
Only include the image attachment payload for cameras that support fetching media
2024-09-03 19:55:28 +02:00
Martin Hjelmare
9a690ed421 Handle telegram polling errors (#124327) 2024-09-03 19:55:27 +02:00
Philip Vanloo
009989d7ae Add Linkplay mTLS/HTTPS and improve logging (#124307)
* Work

* Implement 0.0.8 changes, fixup tests

* Cleanup

* Implement new playmodes, close clientsession upon ha close

* Implement new playmodes, close clientsession upon ha close

* Add test for zeroconf bridge failure

* Bump 0.0.9
Address old comments in 113940

* Exact _async_register_default_clientsession_shutdown
2024-09-03 19:55:26 +02:00
UltimateGG
c7d1ad27f0 Fix updating insteon modem configuration while disconnected (#121918)
#121917 Fix updating insteon modem configuration while disconnected
2024-09-03 19:55:25 +02:00
Bram Kragten
3af11fb2b1 Bump version to 2024.9.0b3 2024-09-02 20:06:41 +02:00
Steven B.
c839cc1f15 Call async_write_ha_state after ring update (#125096)
Use async_write_ha_state after ring update
2024-09-02 20:06:21 +02:00
Paul Bottein
a0f2e2ebdd Update frontend to 20240902.0 (#125093) 2024-09-02 20:06:20 +02:00
dontinelli
d07e62b2f1 Bump fyta_cli to 0.6.6 (#125065) 2024-09-02 20:06:19 +02:00
tronikos
e7f957def2 Bump androidtvremote2 to 0.1.2 to fix blocking event loop when loading ssl certificate chain (#125061)
Bump androidtvremote2 to 0.1.2
2024-09-02 20:06:19 +02:00
Erik Montnemery
16ab57c9a6 Fix motionblinds_ble tests (#125060) 2024-09-02 20:06:18 +02:00
J. Nick Koston
1a67052cbd Bump habluetooth to 3.4.0 (#125058)
changelog: https://github.com/Bluetooth-Devices/habluetooth/compare/v3.3.2...v3.4.0
2024-09-02 20:06:17 +02:00
Erik Montnemery
f85a802ebd Don't raise when registering entity service with invalid schema (#125057)
* Don't raise when registering entity service with invalid schema

* Update homeassistant/helpers/service.py

Co-authored-by: Robert Resch <robert@resch.dev>

---------

Co-authored-by: Robert Resch <robert@resch.dev>
2024-09-02 20:06:16 +02:00
Shay Levy
3b5c08ecf8 Bump aioshelly to 11.4.2 (#125036) 2024-09-02 20:06:16 +02:00
J. Nick Koston
450c63ad28 Bump yarl to 1.9.7 (#125035) 2024-09-02 20:06:15 +02:00
Michael
a8f472f44e Add diagnostics platform to modern forms (#125032) 2024-09-02 20:06:14 +02:00
dontinelli
fa3a301e97 Add ConductivityConverter in websocket_api.py (#125029) 2024-09-02 20:06:13 +02:00
Martin Hjelmare
b1ef1be9a3 Bump python-telegram-bot to 21.5 (#125025) 2024-09-02 20:06:12 +02:00
Joost Lekkerkerker
62ef951ace Bump aiomealie to 0.9.1 (#125017) 2024-09-02 20:06:12 +02:00
Richard Kroegel
06660f9170 Fix BMW client blocking on load_default_certs (#125015)
* Fix BMW client blocking load_default_certs

* Use get_default_context
2024-09-02 20:06:11 +02:00
Richard Kroegel
7662ca8a96 Fix telegram_bot blocking on load_default_certs (#125014)
* Fix telegram_bot blocking on load_default_certs

* Use sync variant of create_issue
2024-09-02 20:06:10 +02:00
Richard Kroegel
e04fc74fcf Fix ollama blocking on load_default_certs (#125012)
* Fix ollama blocking on load_default_certs

* Use get_default_context instead of client_context
2024-09-02 20:06:09 +02:00
Alan Murray
f9bca7619c Bump aiopulse to 0.4.6 (#124964)
Non-breaking changes to fix isses:
 * eliminating hub exceptions raised due use of unicode strings.
 * eliminating hub exceptions raised due to Timers being configured on hub.
2024-09-02 20:06:08 +02:00
J. Nick Koston
1b9aa727f8 Bump yarl to 1.9.6 (#124955)
* Bump yarl to 1.9.5

changelog: https://github.com/aio-libs/yarl/compare/v1.9.4...v1.9.5

* remove default port since mocker does exact matching and yarl now normalizes this

* 1.9.6
2024-09-02 20:06:07 +02:00
Joost Lekkerkerker
d54c1935f8 Define household support in Mealie (#124950) 2024-09-02 20:06:07 +02:00
Steven B.
9cfad05793 Exclude tplink firmware entities (#124935)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-09-02 20:06:06 +02:00
Steven B.
03ab471d23 Bump python-kasa to 0.7.2 (#124930) 2024-09-02 20:06:05 +02:00
Joost Lekkerkerker
b2b69e40fd Make set_value required in number template (#124917)
* Make set_value required in number template

* Make set_value required in number template

* Fix tests
2024-09-02 20:06:04 +02:00
J. Nick Koston
c6ff445dd4 Bump aioshelly to 11.4.1 to accomodate shelly GetStatus calls that take a few seconds to respond (#124893)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2024-09-02 20:06:03 +02:00
vhkristof
0948a94409 Bump renault-api to v0.2.7 (#124858)
* Bump renault-api to v0.2.7

* Updated requirements_all and requirements_test_all
2024-09-02 20:06:02 +02:00
J. Nick Koston
9be20d6130 Restore sisyphus integration (#124749)
* Revert "Disable sisyphus integration (#124742)"

This reverts commit 1b304e60d9.

* Restore sisyphus integration

reverts #124742 and updates the lib instead

changelog: https://github.com/jkeljo/sisyphus-control/compare/v3.1.3...v3.1.4

release is pending: https://github.com/jkeljo/sisyphus-control/pull/8#issuecomment-2313893689
2024-09-02 20:06:01 +02:00
Etienne Soufflet
c4e484539d Fix Tado fan speed for AC (#122415)
* change capabilities

* fix tests 2

* improve usability with capabilities

* fix swings management

* Update homeassistant/components/tado/climate.py

Co-authored-by: Erwin Douna <e.douna@gmail.com>

* fix after Erwin's review

* fix after joostlek's review

* use constant

* use in instead of get

---------

Co-authored-by: Erwin Douna <e.douna@gmail.com>
2024-09-02 20:06:00 +02:00
Jeef
234f32265e Bump Intellifire to 4.1.9 (#121091)
* rebase

* Minor patch to fix duplicate DeviceInfo beign created - if data hasnt updated yet

* rebase

* Minor patch to fix duplicate DeviceInfo beign created - if data hasnt updated yet

* fixing formatting

* Update homeassistant/components/intellifire/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/intellifire/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Removing cloud connectivity sensor - leaving local one in

* Renaming class to something more useful

* addressing pr

* Update homeassistant/components/intellifire/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* add ruff exception

* Fix test annotations

* remove access to private variable

* Bumping to 4.1.9 instead of 4.1.5

* A renaming

* rename

* Updated testing

* Update __init__.py

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

* updateing styrings

* Update tests/components/intellifire/conftest.py

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

* Testing refactor - WIP

* everything is passing - cleanup still needed

* cleaning up comments

* update pr

* unrename

* Update homeassistant/components/intellifire/coordinator.py

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

* fixing sentence

* fixed fixture and removed error codes

* reverted a bad change

* fixing strings.json

* revert renaming

* fix

* typing inother pr

* adding extra tests - one has a really dumb name

* using a real value

* added a migration in

* Update homeassistant/components/intellifire/config_flow.py

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

* Update tests/components/intellifire/test_init.py

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

* cleanup continues

* addressing pr

* switch back to debug

* Update tests/components/intellifire/conftest.py

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

* some changes

* restore property mock cuase didnt work otherwise

* cleanup has begun

* removed extra text

* addressing pr stuff

* fixed reauth

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-09-02 20:05:59 +02:00
Bram Kragten
411b014da2 Bump version to 2024.9.0b2 2024-08-30 20:08:46 +02:00
Joost Lekkerkerker
3a8aa4200d Bump aiomealie to 0.9.0 (#124924)
* Bump aiomealie to 0.9.0

* Bump aiomealie to 0.9.0
2024-08-30 20:08:20 +02:00
Josef Zweck
dd8471e786 Bump lmcloud 1.2.2 (#124911)
bump lmcloud 1.2.2
2024-08-30 20:08:19 +02:00
Josef Zweck
f33b4b0dc0 Bump lmcloud to 1.2.1 (#124908) 2024-08-30 20:08:19 +02:00
J. Nick Koston
8ab8f7a740 Add a repair issue for Yale Home users using the August integration (#124895)
The Yale Home brand will stop working with the August integration very
soon. Users must migrate to the Yale integration to avoid an interruption in service.
2024-08-30 20:08:18 +02:00
J. Nick Koston
ee9e3fe27b Bump yalexs to 8.5.5 (#124891)
changelog: https://github.com/bdraco/yalexs/compare/v8.5.4...v8.5.5
2024-08-30 20:08:17 +02:00
J. Nick Koston
d4830caac0 Bump aioesphomeapi to 25.3.1 (#124890)
changelog: https://github.com/esphome/aioesphomeapi/compare/v25.2.1...v25.3.1
2024-08-30 20:08:16 +02:00
TheJulianJES
3b4e3b1370 Fix ZHA group removal entity registry cleanup (#124889)
* Fix ZHA cleanup entity registry parameter

* Fix missing `gateway` when accessing coordinator device

* Get `ZHADeviceProxy` for coordinator device
2024-08-30 20:08:15 +02:00
J. Nick Koston
533c8ca31c Address yale review comments part 2 (#124887)
* Remove some unneeded block till done

* Additional state check cleanups and snapshots

* Use more snapshots in yale tests
2024-08-30 20:08:15 +02:00
Michael Hansen
8668af17f6 Bump intents to 2024.8.29 (#124874) 2024-08-30 20:08:14 +02:00
Louis Christ
5b866e071c Handle CancelledError in bluesound integration (#124873)
Catch CancledError in async_will_remove_from_hass
2024-08-30 20:08:13 +02:00
IceBotYT
37af180edc Bump nice-go to 0.3.8 (#124872)
* Bump nice-go to 0.3.6

* Bump to 0.3.7

* Bump to 0.3.8
2024-08-30 20:08:13 +02:00
Robert Resch
0d5dc01048 Bump PyTurboJPEG to 1.7.5 (#124865) 2024-08-30 20:08:12 +02:00
Robert Resch
bd2be0a763 Optimize hassfest image (#124855)
* Optimize hassfest docker image

* Adjust CI

* Use dynamic uv version

* Remove workaround
2024-08-30 20:08:11 +02:00
J. Nick Koston
98cbd7d8da Address august review comments (#124819)
* Address august review comments

Followup to https://github.com/home-assistant/core/pull/124677

* cleanup loop

* drop mixin name

* event entity add cleanup

* remove duplicate prop

* pep0695 type

* remove some not needed block till done

* cleanup august tests

* switch to freezegun

* snapshots for dev reg

* SOURCE_USER nit

* snapshots

* pytest.raises

* not loaded check
2024-08-30 20:08:10 +02:00
puddly
26f3305743 Bump ZHA to 0.0.32 (#124804)
* Always prefer XY color mode in ZHA

Remove a few more HS remnants

* Use new ZHA OTA format

* Bump ZHA to 0.0.32

* Fix existing OTA unit tests

* Fix schema conversion test to account for new command parameters

* Update snapshot with new `zcl_type` kwarg

* Migrate existing entities to icon translations

* Remove "no longer compatible" test

* Test that the library release summary is correctly exposed to ZHA

* Revert "Always prefer XY color mode in ZHA"

This reverts commit 8fb7789ea8ddb6ed2a287aed5010374c0452f6c9.

* Test `release_notes`, not `release_summary`
2024-08-30 20:08:10 +02:00
tronikos
3c0480596d Attempt to fix IndexError in Opower (#124478)
* Change the order of async_add_external_statistics in Opower

* Use consumption_statistic_id instead of cost_statistic_id
2024-08-30 20:08:09 +02:00
Jeef
81d2231e6f Bump weatherflow4py to 0.2.23 (#124072)
patch weatherflow for new data
2024-08-30 20:08:08 +02:00
Tony
2d041a1fa9 Bump aioruckus to v0.41 removing blocking call to load_default_certs from ruckus_unleashed integration (#123974)
* fix ruckusd_unleashed blocking call to load_default_certs

* remove extra loggers, bump aioruckus ver for debian packagers
2024-08-30 20:08:07 +02:00
Paulus Schoutsen
516f3295bf 2024.8.3 (#124569) 2024-08-25 16:06:09 +02:00
Franck Nijhof
94516de724 2024.8.2 (#124069) 2024-08-16 18:43:41 +02:00
Franck Nijhof
ae4fc9504a 2024.8.1 (#123544) 2024-08-10 19:32:02 +02:00
177 changed files with 4604 additions and 1916 deletions

View File

@@ -491,7 +491,7 @@ jobs:
packages: write
attestations: write
id-token: write
needs: ["init", "build_base"]
needs: ["init"]
if: github.repository_owner == 'home-assistant'
env:
HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
@@ -510,8 +510,8 @@ jobs:
- name: Build Docker image
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
with:
context: ./script/hassfest/docker
build-args: BASE_IMAGE=ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
load: true
tags: ${{ env.HASSFEST_IMAGE_TAG }}
@@ -523,8 +523,8 @@ jobs:
id: push
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
with:
context: ./script/hassfest/docker
build-args: BASE_IMAGE=ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
push: true
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/acmeda",
"iot_class": "local_push",
"loggers": ["aiopulse"],
"requirements": ["aiopulse==0.4.4"]
"requirements": ["aiopulse==0.4.6"]
}

View File

@@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["androidtvremote2"],
"quality_scale": "platinum",
"requirements": ["androidtvremote2==0.1.1"],
"requirements": ["androidtvremote2==0.1.2"],
"zeroconf": ["_androidtvremote2._tcp.local."]
}

View File

@@ -8,7 +8,7 @@ LOGGER = logging.getLogger(__package__)
CONF_RECOMMENDED = "recommended"
CONF_PROMPT = "prompt"
CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "claude-3-5-sonnet-20240620"
RECOMMENDED_CHAT_MODEL = "claude-3-haiku-20240307"
CONF_MAX_TOKENS = "max_tokens"
RECOMMENDED_MAX_TOKENS = 1024
CONF_TEMPERATURE = "temperature"

View File

@@ -6,15 +6,16 @@ from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from yalexs.const import Brand
from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from .const import DOMAIN, PLATFORMS
from .data import AugustData
@@ -24,7 +25,27 @@ from .util import async_create_august_clientsession
type AugustConfigEntry = ConfigEntry[AugustData]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@callback
def _async_create_yale_brand_migration_issue(
hass: HomeAssistant, entry: AugustConfigEntry
) -> None:
"""Create an issue for a brand migration."""
ir.async_create_issue(
hass,
DOMAIN,
"yale_brand_migration",
breaks_in_ha_version="2024.9",
learn_more_url="https://www.home-assistant.io/integrations/yale",
translation_key="yale_brand_migration",
is_fixable=False,
severity=ir.IssueSeverity.CRITICAL,
translation_placeholders={
"migrate_url": "https://my.home-assistant.io/redirect/config_flow_start?domain=yale"
},
)
async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool:
"""Set up August from a config entry."""
session = async_create_august_clientsession(hass)
august_gateway = AugustGateway(Path(hass.config.config_dir), session)
@@ -40,6 +61,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_remove_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> None:
"""Remove an August config entry."""
ir.async_delete_issue(hass, DOMAIN, "yale_brand_migration")
async def async_unload_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -51,6 +77,8 @@ async def async_setup_august(
"""Set up the August component."""
config = cast(YaleXSConfig, entry.data)
await august_gateway.async_setup(config)
if august_gateway.api.brand == Brand.YALE_HOME:
_async_create_yale_brand_migration_issue(hass, entry)
await august_gateway.async_authenticate()
await august_gateway.async_refresh_access_token_if_needed()
data = entry.runtime_data = AugustData(hass, august_gateway)

View File

@@ -109,12 +109,11 @@ async def async_setup_entry(
for description in SENSOR_TYPES_DOORBELL
)
for doorbell in data.doorbells:
entities.extend(
AugustDoorbellBinarySensor(data, doorbell, description)
for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL
)
entities.extend(
AugustDoorbellBinarySensor(data, doorbell, description)
for description in SENSOR_TYPES_DOORBELL + SENSOR_TYPES_VIDEO_DOORBELL
for doorbell in data.doorbells
)
async_add_entities(entities)

View File

@@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AugustConfigEntry
from .entity import AugustEntityMixin
from .entity import AugustEntity
async def async_setup_entry(
@@ -18,7 +18,7 @@ async def async_setup_entry(
async_add_entities(AugustWakeLockButton(data, lock, "wake") for lock in data.locks)
class AugustWakeLockButton(AugustEntityMixin, ButtonEntity):
class AugustWakeLockButton(AugustEntity, ButtonEntity):
"""Representation of an August lock wake button."""
_attr_translation_key = "wake"

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AugustConfigEntry, AugustData
from .const import DEFAULT_NAME, DEFAULT_TIMEOUT
from .entity import AugustEntityMixin
from .entity import AugustEntity
_LOGGER = logging.getLogger(__name__)
@@ -38,7 +38,7 @@ async def async_setup_entry(
)
class AugustCamera(AugustEntityMixin, Camera):
class AugustCamera(AugustEntity, Camera):
"""An implementation of an August security camera."""
_attr_translation_key = "camera"

View File

@@ -9,7 +9,7 @@ from typing import Any
import aiohttp
import voluptuous as vol
from yalexs.authenticator_common import ValidationResult
from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND
from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND, Brand
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -28,6 +28,12 @@ from .const import (
from .gateway import AugustGateway
from .util import async_create_august_clientsession
# The Yale Home Brand is not supported by the August integration
# anymore and should migrate to the Yale integration
AVAILABLE_BRANDS = BRANDS_WITHOUT_OAUTH.copy()
del AVAILABLE_BRANDS[Brand.YALE_HOME]
_LOGGER = logging.getLogger(__name__)
@@ -118,7 +124,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN):
vol.Required(
CONF_BRAND,
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
): vol.In(BRANDS_WITHOUT_OAUTH),
): vol.In(AVAILABLE_BRANDS),
vol.Required(
CONF_LOGIN_METHOD,
default=self._user_auth_details.get(

View File

@@ -20,7 +20,7 @@ from .const import MANUFACTURER
DEVICE_TYPES = ["keypad", "lock", "camera", "doorbell", "door", "bell"]
class AugustEntityMixin(Entity):
class AugustEntity(Entity):
"""Base implementation for August device."""
_attr_should_poll = False
@@ -87,7 +87,7 @@ class AugustEntityMixin(Entity):
self._update_from_data()
class AugustDescriptionEntity(AugustEntityMixin):
class AugustDescriptionEntity(AugustEntity):
"""An August entity with a description."""
def __init__(

View File

@@ -63,22 +63,17 @@ async def async_setup_entry(
) -> None:
"""Set up the august event platform."""
data = config_entry.runtime_data
entities: list[AugustEventEntity] = []
for lock in data.locks:
detail = data.get_device_detail(lock.device_id)
if detail.doorbell:
entities.extend(
AugustEventEntity(data, lock, description)
for description in TYPES_DOORBELL
)
for doorbell in data.doorbells:
entities.extend(
AugustEventEntity(data, doorbell, description)
for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
)
entities: list[AugustEventEntity] = [
AugustEventEntity(data, lock, description)
for description in TYPES_DOORBELL
for lock in data.locks
if (detail := data.get_device_detail(lock.device_id)) and detail.doorbell
]
entities.extend(
AugustEventEntity(data, doorbell, description)
for description in TYPES_DOORBELL + TYPES_VIDEO_DOORBELL
for doorbell in data.doorbells
)
async_add_entities(entities)
@@ -86,7 +81,6 @@ class AugustEventEntity(AugustDescriptionEntity, EventEntity):
"""An august event entity."""
entity_description: AugustEventEntityDescription
_attr_has_entity_name = True
_last_activity: Activity | None = None
@callback

View File

@@ -19,7 +19,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
import homeassistant.util.dt as dt_util
from . import AugustConfigEntry, AugustData
from .entity import AugustEntityMixin
from .entity import AugustEntity
_LOGGER = logging.getLogger(__name__)
@@ -36,7 +36,7 @@ async def async_setup_entry(
async_add_entities(AugustLock(data, lock) for lock in data.locks)
class AugustLock(AugustEntityMixin, RestoreEntity, LockEntity):
class AugustLock(AugustEntity, RestoreEntity, LockEntity):
"""Representation of an August lock."""
_attr_name = None

View File

@@ -24,5 +24,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==8.5.4", "yalexs-ble==2.4.3"]
"requirements": ["yalexs==8.6.3", "yalexs-ble==2.4.3"]
}

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic, TypeVar, cast
from typing import Any, cast
from yalexs.activity import ActivityType, LockOperationActivity
from yalexs.doorbell import Doorbell
@@ -42,7 +42,7 @@ from .const import (
OPERATION_METHOD_REMOTE,
OPERATION_METHOD_TAG,
)
from .entity import AugustDescriptionEntity, AugustEntityMixin
from .entity import AugustDescriptionEntity, AugustEntity
def _retrieve_device_battery_state(detail: LockDetail) -> int:
@@ -55,14 +55,13 @@ def _retrieve_linked_keypad_battery_state(detail: KeypadDetail) -> int | None:
return detail.battery_percentage
_T = TypeVar("_T", LockDetail, KeypadDetail)
@dataclass(frozen=True, kw_only=True)
class AugustSensorEntityDescription(SensorEntityDescription, Generic[_T]):
class AugustSensorEntityDescription[T: LockDetail | KeypadDetail](
SensorEntityDescription
):
"""Mixin for required keys."""
value_fn: Callable[[_T], int | None]
value_fn: Callable[[T], int | None]
SENSOR_TYPE_DEVICE_BATTERY = AugustSensorEntityDescription[LockDetail](
@@ -114,7 +113,7 @@ async def async_setup_entry(
async_add_entities(entities)
class AugustOperatorSensor(AugustEntityMixin, RestoreSensor):
class AugustOperatorSensor(AugustEntity, RestoreSensor):
"""Representation of an August lock operation sensor."""
_attr_translation_key = "operator"
@@ -198,10 +197,12 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreSensor):
self._operated_autorelock = last_attrs[ATTR_OPERATION_AUTORELOCK]
class AugustBatterySensor(AugustDescriptionEntity, SensorEntity, Generic[_T]):
class AugustBatterySensor[T: LockDetail | KeypadDetail](
AugustDescriptionEntity, SensorEntity
):
"""Representation of an August sensor."""
entity_description: AugustSensorEntityDescription[_T]
entity_description: AugustSensorEntityDescription[T]
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE

View File

@@ -1,4 +1,10 @@
{
"issues": {
"yale_brand_migration": {
"title": "Yale Home has a new integration",
"description": "Add the [Yale integration]({migrate_url}), and remove the August integration as soon as possible to avoid an interruption in service. The Yale Home brand will stop working with the August integration soon and will be removed in a future release."
}
},
"config": {
"error": {
"unhandled": "Unhandled error: {error}",

View File

@@ -63,16 +63,11 @@ def _activity_time_based(latest: Activity) -> Activity | None:
"""Get the latest state of the sensor."""
start = latest.activity_start_time
end = latest.activity_end_time + TIME_TO_DECLARE_DETECTION
if start <= _native_datetime() <= end:
if start <= datetime.now() <= end:
return latest
return None
def _native_datetime() -> datetime:
"""Return time in the format august uses without timezone."""
return datetime.now()
def retrieve_online_state(
data: AugustData, detail: DoorbellDetail | LockDetail
) -> bool:

View File

@@ -309,7 +309,7 @@ class BluesoundPlayer(MediaPlayerEntity):
return True
async def _start_poll_command(self):
async def _poll_loop(self):
"""Loop which polls the status of the player."""
while True:
try:
@@ -335,7 +335,7 @@ class BluesoundPlayer(MediaPlayerEntity):
await super().async_added_to_hass()
self._polling_task = self.hass.async_create_background_task(
self._start_poll_command(),
self._poll_loop(),
name=f"bluesound.polling_{self.host}:{self.port}",
)
@@ -345,7 +345,9 @@ class BluesoundPlayer(MediaPlayerEntity):
assert self._polling_task is not None
if self._polling_task.cancel():
await self._polling_task
# the sleeps in _poll_loop will raise CancelledError
with suppress(CancelledError):
await self._polling_task
self.hass.data[DATA_BLUESOUND].remove(self)

View File

@@ -20,6 +20,6 @@
"bluetooth-auto-recovery==1.4.2",
"bluetooth-data-tools==1.20.0",
"dbus-fast==2.24.0",
"habluetooth==3.3.2"
"habluetooth==3.4.0"
]
}

View File

@@ -15,6 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.ssl import get_default_context
from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
@@ -33,6 +34,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
entry.data[CONF_PASSWORD],
get_region_from_name(entry.data[CONF_REGION]),
observer_position=GPSPosition(hass.config.latitude, hass.config.longitude),
verify=get_default_context(),
)
self.read_only = entry.options[CONF_READ_ONLY]
self._entry = entry

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"quality_scale": "platinum",
"requirements": ["bimmer-connected[china]==0.16.2"]
"requirements": ["bimmer-connected[china]==0.16.3"]
}

View File

@@ -2,6 +2,7 @@
DOMAIN = "buienradar"
DEFAULT_TIMEOUT = 60
DEFAULT_TIMEFRAME = 60
DEFAULT_DIMENSION = 700

View File

@@ -1,9 +1,9 @@
"""Shared utilities for different supported platforms."""
from asyncio import timeout
from datetime import datetime, timedelta
from http import HTTPStatus
import logging
from typing import Any
import aiohttp
from buienradar.buienradar import parse_data
@@ -27,12 +27,12 @@ from buienradar.constants import (
from buienradar.urls import JSON_FEED_URL, json_precipitation_forecast_url
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.util import dt as dt_util
from .const import SCHEDULE_NOK, SCHEDULE_OK
from .const import DEFAULT_TIMEOUT, SCHEDULE_NOK, SCHEDULE_OK
__all__ = ["BrData"]
_LOGGER = logging.getLogger(__name__)
@@ -59,10 +59,10 @@ class BrData:
load_error_count: int = WARN_THRESHOLD
rain_error_count: int = WARN_THRESHOLD
def __init__(self, hass, coordinates, timeframe, devices):
def __init__(self, hass: HomeAssistant, coordinates, timeframe, devices) -> None:
"""Initialize the data object."""
self.devices = devices
self.data = {}
self.data: dict[str, Any] | None = {}
self.hass = hass
self.coordinates = coordinates
self.timeframe = timeframe
@@ -93,9 +93,9 @@ class BrData:
resp = None
try:
websession = async_get_clientsession(self.hass)
async with timeout(10):
resp = await websession.get(url)
async with websession.get(
url, timeout=aiohttp.ClientTimeout(total=DEFAULT_TIMEOUT)
) as resp:
result[STATUS_CODE] = resp.status
result[CONTENT] = await resp.text()
if resp.status == HTTPStatus.OK:

View File

@@ -130,7 +130,7 @@ class BrWeather(WeatherEntity):
_attr_should_poll = False
_attr_supported_features = WeatherEntityFeature.FORECAST_DAILY
def __init__(self, config, coordinates):
def __init__(self, config, coordinates) -> None:
"""Initialize the platform with a data instance and station name."""
self._stationname = config.get(CONF_NAME, "Buienradar")
self._attr_name = self._stationname or f"BR {'(unknown station)'}"

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/camera",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["PyTurboJPEG==1.7.1"]
"requirements": ["PyTurboJPEG==1.7.5"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.8.7"]
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.9.4"]
}

View File

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

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==25.2.1",
"aioesphomeapi==25.3.1",
"esphome-dashboard-api==1.2.3",
"bleak-esphome==1.0.0"
],

View File

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

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["fyta_cli==0.6.3"]
"requirements": ["fyta_cli==0.6.6"]
}

View File

@@ -14,5 +14,5 @@
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
"iot_class": "local_polling",
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
"requirements": ["gardena-bluetooth==1.4.2"]
"requirements": ["gardena-bluetooth==1.4.3"]
}

View File

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

View File

@@ -211,7 +211,7 @@ async def websocket_update_modem_config(
"""Get the schema for the modem configuration."""
config = msg["config"]
config_entry = get_insteon_config_entry(hass)
is_connected = devices.modem.connected
is_connected = devices.modem is not None and devices.modem.connected
if not await _async_connect(**config):
connection.send_error(

View File

@@ -2,15 +2,17 @@
from __future__ import annotations
from aiohttp import ClientConnectionError
from intellifire4py import IntellifireControlAsync
from intellifire4py.exceptions import LoginException
from intellifire4py.intellifire import IntellifireAPICloud, IntellifireAPILocal
import asyncio
from intellifire4py import UnifiedFireplace
from intellifire4py.cloud_interface import IntelliFireCloudInterface
from intellifire4py.model import IntelliFireCommonFireplaceData
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
CONF_IP_ADDRESS,
CONF_PASSWORD,
CONF_USERNAME,
Platform,
@@ -18,7 +20,18 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import CONF_USER_ID, DOMAIN, LOGGER
from .const import (
CONF_AUTH_COOKIE,
CONF_CONTROL_MODE,
CONF_READ_MODE,
CONF_SERIAL,
CONF_USER_ID,
CONF_WEB_CLIENT_ID,
DOMAIN,
INIT_WAIT_TIME_SECONDS,
LOGGER,
STARTUP_TIMEOUT,
)
from .coordinator import IntellifireDataUpdateCoordinator
PLATFORMS = [
@@ -32,79 +45,114 @@ PLATFORMS = [
]
def _construct_common_data(entry: ConfigEntry) -> IntelliFireCommonFireplaceData:
"""Convert config entry data into IntelliFireCommonFireplaceData."""
return IntelliFireCommonFireplaceData(
auth_cookie=entry.data[CONF_AUTH_COOKIE],
user_id=entry.data[CONF_USER_ID],
web_client_id=entry.data[CONF_WEB_CLIENT_ID],
serial=entry.data[CONF_SERIAL],
api_key=entry.data[CONF_API_KEY],
ip_address=entry.data[CONF_IP_ADDRESS],
read_mode=entry.options[CONF_READ_MODE],
control_mode=entry.options[CONF_CONTROL_MODE],
)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate entries."""
LOGGER.debug(
"Migrating configuration from version %s.%s",
config_entry.version,
config_entry.minor_version,
)
if config_entry.version == 1:
new = {**config_entry.data}
if config_entry.minor_version < 2:
username = config_entry.data[CONF_USERNAME]
password = config_entry.data[CONF_PASSWORD]
# Create a Cloud Interface
async with IntelliFireCloudInterface() as cloud_interface:
await cloud_interface.login_with_credentials(
username=username, password=password
)
new_data = cloud_interface.user_data.get_data_for_ip(new[CONF_HOST])
if not new_data:
raise ConfigEntryAuthFailed
new[CONF_API_KEY] = new_data.api_key
new[CONF_WEB_CLIENT_ID] = new_data.web_client_id
new[CONF_AUTH_COOKIE] = new_data.auth_cookie
new[CONF_IP_ADDRESS] = new_data.ip_address
new[CONF_SERIAL] = new_data.serial
hass.config_entries.async_update_entry(
config_entry,
data=new,
options={CONF_READ_MODE: "local", CONF_CONTROL_MODE: "local"},
unique_id=new[CONF_SERIAL],
version=1,
minor_version=2,
)
LOGGER.debug("Pseudo Migration %s successful", config_entry.version)
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up IntelliFire from a config entry."""
LOGGER.debug("Setting up config entry: %s", entry.unique_id)
if CONF_USERNAME not in entry.data:
LOGGER.debug("Old config entry format detected: %s", entry.unique_id)
LOGGER.debug("Config entry without username detected: %s", entry.unique_id)
raise ConfigEntryAuthFailed
ift_control = IntellifireControlAsync(
fireplace_ip=entry.data[CONF_HOST],
)
try:
await ift_control.login(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
fireplace: UnifiedFireplace = (
await UnifiedFireplace.build_fireplace_from_common(
_construct_common_data(entry)
)
)
except (ConnectionError, ClientConnectionError) as err:
raise ConfigEntryNotReady from err
except LoginException as err:
raise ConfigEntryAuthFailed(err) from err
finally:
await ift_control.close()
# Extract API Key and User_ID from ift_control
# Eventually this will migrate to using IntellifireAPICloud
if CONF_USER_ID not in entry.data or CONF_API_KEY not in entry.data:
LOGGER.info(
"Updating intellifire config entry for %s with api information",
entry.unique_id,
)
cloud_api = IntellifireAPICloud()
await cloud_api.login(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
)
api_key = cloud_api.get_fireplace_api_key()
user_id = cloud_api.get_user_id()
# Update data entry
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_API_KEY: api_key,
CONF_USER_ID: user_id,
},
LOGGER.debug("Waiting for Fireplace to Initialize")
await asyncio.wait_for(
_async_wait_for_initialization(fireplace), timeout=STARTUP_TIMEOUT
)
except TimeoutError as err:
raise ConfigEntryNotReady(
"Initialization of fireplace timed out after 10 minutes"
) from err
else:
api_key = entry.data[CONF_API_KEY]
user_id = entry.data[CONF_USER_ID]
# Instantiate local control
api = IntellifireAPILocal(
fireplace_ip=entry.data[CONF_HOST],
api_key=api_key,
user_id=user_id,
# Construct coordinator
data_update_coordinator = IntellifireDataUpdateCoordinator(
hass=hass, fireplace=fireplace
)
# Define the update coordinator
coordinator = IntellifireDataUpdateCoordinator(
hass=hass,
api=api,
)
LOGGER.debug("Fireplace to Initialized - Awaiting first refresh")
await data_update_coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data_update_coordinator
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def _async_wait_for_initialization(
fireplace: UnifiedFireplace, timeout=STARTUP_TIMEOUT
):
"""Wait for a fireplace to be initialized."""
while (
fireplace.data.ipv4_address == "127.0.0.1" and fireplace.data.serial == "unset"
):
LOGGER.debug(f"Waiting for fireplace to initialize [{fireplace.read_mode}]")
await asyncio.sleep(INIT_WAIT_TIME_SECONDS)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from intellifire4py import IntellifirePollData
from intellifire4py.model import IntelliFirePollData
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -26,7 +26,7 @@ from .entity import IntellifireEntity
class IntellifireBinarySensorRequiredKeysMixin:
"""Mixin for required keys."""
value_fn: Callable[[IntellifirePollData], bool]
value_fn: Callable[[IntelliFirePollData], bool]
@dataclass(frozen=True)

View File

@@ -69,7 +69,7 @@ class IntellifireClimate(IntellifireEntity, ClimateEntity):
super().__init__(coordinator, description)
if coordinator.data.thermostat_on:
self.last_temp = coordinator.data.thermostat_setpoint_c
self.last_temp = int(coordinator.data.thermostat_setpoint_c)
@property
def hvac_mode(self) -> HVACMode:

View File

@@ -7,16 +7,33 @@ from dataclasses import dataclass
from typing import Any
from aiohttp import ClientConnectionError
from intellifire4py import AsyncUDPFireplaceFinder
from intellifire4py.exceptions import LoginException
from intellifire4py.intellifire import IntellifireAPICloud, IntellifireAPILocal
from intellifire4py.cloud_interface import IntelliFireCloudInterface
from intellifire4py.exceptions import LoginError
from intellifire4py.local_api import IntelliFireAPILocal
from intellifire4py.model import IntelliFireCommonFireplaceData
import voluptuous as vol
from homeassistant.components.dhcp import DhcpServiceInfo
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_API_KEY,
CONF_HOST,
CONF_IP_ADDRESS,
CONF_PASSWORD,
CONF_USERNAME,
)
from .const import CONF_USER_ID, DOMAIN, LOGGER
from .const import (
API_MODE_LOCAL,
CONF_AUTH_COOKIE,
CONF_CONTROL_MODE,
CONF_READ_MODE,
CONF_SERIAL,
CONF_USER_ID,
CONF_WEB_CLIENT_ID,
DOMAIN,
LOGGER,
)
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str})
@@ -31,17 +48,20 @@ class DiscoveredHostInfo:
serial: str | None
async def validate_host_input(host: str, dhcp_mode: bool = False) -> str:
async def _async_poll_local_fireplace_for_serial(
host: str, dhcp_mode: bool = False
) -> str:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
LOGGER.debug("Instantiating IntellifireAPI with host: [%s]", host)
api = IntellifireAPILocal(fireplace_ip=host)
api = IntelliFireAPILocal(fireplace_ip=host)
await api.poll(suppress_warnings=dhcp_mode)
serial = api.data.serial
LOGGER.debug("Found a fireplace: %s", serial)
# Return the serial number which will be used to calculate a unique ID for the device/sensors
return serial
@@ -50,239 +70,206 @@ class IntelliFireConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for IntelliFire."""
VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None:
"""Initialize the Config Flow Handler."""
self._host: str = ""
self._serial: str = ""
self._not_configured_hosts: list[DiscoveredHostInfo] = []
# DHCP Variables
self._dhcp_discovered_serial: str = "" # used only in discovery mode
self._discovered_host: DiscoveredHostInfo
self._dhcp_mode = False
self._is_reauth = False
self._not_configured_hosts: list[DiscoveredHostInfo] = []
self._reauth_needed: DiscoveredHostInfo
async def _find_fireplaces(self):
"""Perform UDP discovery."""
fireplace_finder = AsyncUDPFireplaceFinder()
discovered_hosts = await fireplace_finder.search_fireplace(timeout=12)
configured_hosts = {
entry.data[CONF_HOST]
for entry in self._async_current_entries(include_ignore=False)
if CONF_HOST in entry.data # CONF_HOST will be missing for ignored entries
}
self._configured_serials: list[str] = []
self._not_configured_hosts = [
DiscoveredHostInfo(ip, None)
for ip in discovered_hosts
if ip not in configured_hosts
]
LOGGER.debug("Discovered Hosts: %s", discovered_hosts)
LOGGER.debug("Configured Hosts: %s", configured_hosts)
LOGGER.debug("Not Configured Hosts: %s", self._not_configured_hosts)
async def validate_api_access_and_create_or_update(
self, *, host: str, username: str, password: str, serial: str
):
"""Validate username/password against api."""
LOGGER.debug("Attempting login to iftapi with: %s", username)
ift_cloud = IntellifireAPICloud()
await ift_cloud.login(username=username, password=password)
api_key = ift_cloud.get_fireplace_api_key()
user_id = ift_cloud.get_user_id()
data = {
CONF_HOST: host,
CONF_PASSWORD: password,
CONF_USERNAME: username,
CONF_API_KEY: api_key,
CONF_USER_ID: user_id,
}
# Update or Create
existing_entry = await self.async_set_unique_id(serial)
if existing_entry:
self.hass.config_entries.async_update_entry(existing_entry, data=data)
await self.hass.config_entries.async_reload(existing_entry.entry_id)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(title=f"Fireplace {serial}", data=data)
async def async_step_api_config(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Configure API access."""
errors = {}
control_schema = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
if user_input is not None:
control_schema = vol.Schema(
{
vol.Required(
CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")
): str,
vol.Required(
CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")
): str,
}
)
try:
return await self.validate_api_access_and_create_or_update(
host=self._host,
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
serial=self._serial,
)
except (ConnectionError, ClientConnectionError):
errors["base"] = "iftapi_connect"
LOGGER.error(
"Could not connect to iftapi.net over https - verify connectivity"
)
except LoginException:
errors["base"] = "api_error"
LOGGER.error("Invalid credentials for iftapi.net")
return self.async_show_form(
step_id="api_config", errors=errors, data_schema=control_schema
)
async def _async_validate_ip_and_continue(self, host: str) -> ConfigFlowResult:
"""Validate local config and continue."""
self._async_abort_entries_match({CONF_HOST: host})
self._serial = await validate_host_input(host)
await self.async_set_unique_id(self._serial, raise_on_progress=False)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
# Store current data and jump to next stage
self._host = host
return await self.async_step_api_config()
async def async_step_manual_device_entry(self, user_input=None):
"""Handle manual input of local IP configuration."""
LOGGER.debug("STEP: manual_device_entry")
errors = {}
self._host = user_input.get(CONF_HOST) if user_input else None
if user_input is not None:
try:
return await self._async_validate_ip_and_continue(self._host)
except (ConnectionError, ClientConnectionError):
errors["base"] = "cannot_connect"
return self.async_show_form(
step_id="manual_device_entry",
errors=errors,
data_schema=vol.Schema({vol.Required(CONF_HOST, default=self._host): str}),
)
async def async_step_pick_device(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Pick which device to configure."""
errors = {}
LOGGER.debug("STEP: pick_device")
if user_input is not None:
if user_input[CONF_HOST] == MANUAL_ENTRY_STRING:
return await self.async_step_manual_device_entry()
try:
return await self._async_validate_ip_and_continue(user_input[CONF_HOST])
except (ConnectionError, ClientConnectionError):
errors["base"] = "cannot_connect"
return self.async_show_form(
step_id="pick_device",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(CONF_HOST): vol.In(
[host.ip for host in self._not_configured_hosts]
+ [MANUAL_ENTRY_STRING]
)
}
),
)
# Define a cloud api interface we can use
self.cloud_api_interface = IntelliFireCloudInterface()
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Start the user flow."""
# Launch fireplaces discovery
await self._find_fireplaces()
LOGGER.debug("STEP: user")
if self._not_configured_hosts:
LOGGER.debug("Running Step: pick_device")
return await self.async_step_pick_device()
LOGGER.debug("Running Step: manual_device_entry")
return await self.async_step_manual_device_entry()
current_entries = self._async_current_entries(include_ignore=False)
self._configured_serials = [
entry.data[CONF_SERIAL] for entry in current_entries
]
return await self.async_step_cloud_api()
async def async_step_cloud_api(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Authenticate against IFTAPI Cloud in order to see configured devices.
Local control of IntelliFire devices requires that the user download the correct API KEY which is only available on the cloud. Cloud control of the devices requires the user has at least once authenticated against the cloud and a set of cookie variables have been stored locally.
"""
errors: dict[str, str] = {}
LOGGER.debug("STEP: cloud_api")
if user_input is not None:
try:
async with self.cloud_api_interface as cloud_interface:
await cloud_interface.login_with_credentials(
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
)
# If login was successful pass username/password to next step
return await self.async_step_pick_cloud_device()
except LoginError:
errors["base"] = "api_error"
return self.async_show_form(
step_id="cloud_api",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
),
)
async def async_step_pick_cloud_device(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Step to select a device from the cloud.
We can only get here if we have logged in. If there is only one device available it will be auto-configured,
else the user will be given a choice to pick a device.
"""
errors: dict[str, str] = {}
LOGGER.debug(
f"STEP: pick_cloud_device: {user_input} - DHCP_MODE[{self._dhcp_mode}"
)
if self._dhcp_mode or user_input is not None:
if self._dhcp_mode:
serial = self._dhcp_discovered_serial
LOGGER.debug(f"DHCP Mode detected for serial [{serial}]")
if user_input is not None:
serial = user_input[CONF_SERIAL]
# Run a unique ID Check prior to anything else
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured(updates={CONF_SERIAL: serial})
# If Serial is Good obtain fireplace and configure
fireplace = self.cloud_api_interface.user_data.get_data_for_serial(serial)
if fireplace:
return await self._async_create_config_entry_from_common_data(
fireplace=fireplace
)
# Parse User Data to see if we auto-configure or prompt for selection:
user_data = self.cloud_api_interface.user_data
available_fireplaces: list[IntelliFireCommonFireplaceData] = [
fp
for fp in user_data.fireplaces
if fp.serial not in self._configured_serials
]
# Abort if all devices have been configured
if not available_fireplaces:
return self.async_abort(reason="no_available_devices")
# If there is a single fireplace configure it
if len(available_fireplaces) == 1:
if self._is_reauth:
reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
return await self._async_create_config_entry_from_common_data(
fireplace=available_fireplaces[0], existing_entry=reauth_entry
)
return await self._async_create_config_entry_from_common_data(
fireplace=available_fireplaces[0]
)
return self.async_show_form(
step_id="pick_cloud_device",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(CONF_SERIAL): vol.In(
[fp.serial for fp in available_fireplaces]
)
}
),
)
async def _async_create_config_entry_from_common_data(
self,
fireplace: IntelliFireCommonFireplaceData,
existing_entry: ConfigEntry | None = None,
) -> ConfigFlowResult:
"""Construct a config entry based on an object of IntelliFireCommonFireplaceData."""
data = {
CONF_IP_ADDRESS: fireplace.ip_address,
CONF_API_KEY: fireplace.api_key,
CONF_SERIAL: fireplace.serial,
CONF_AUTH_COOKIE: fireplace.auth_cookie,
CONF_WEB_CLIENT_ID: fireplace.web_client_id,
CONF_USER_ID: fireplace.user_id,
CONF_USERNAME: self.cloud_api_interface.user_data.username,
CONF_PASSWORD: self.cloud_api_interface.user_data.password,
}
options = {CONF_READ_MODE: API_MODE_LOCAL, CONF_CONTROL_MODE: API_MODE_LOCAL}
if existing_entry:
return self.async_update_reload_and_abort(
existing_entry, data=data, options=options
)
return self.async_create_entry(
title=f"Fireplace {fireplace.serial}", data=data, options=options
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
LOGGER.debug("STEP: reauth")
self._is_reauth = True
entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
assert entry
assert entry.unique_id
# populate the expected vars
self._serial = entry.unique_id
self._host = entry.data[CONF_HOST]
self._dhcp_discovered_serial = entry.data[CONF_SERIAL] # type: ignore[union-attr]
placeholders = {CONF_HOST: self._host, "serial": self._serial}
placeholders = {"serial": self._dhcp_discovered_serial}
self.context["title_placeholders"] = placeholders
return await self.async_step_api_config()
return await self.async_step_cloud_api()
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP Discovery."""
self._dhcp_mode = True
# Run validation logic on ip
host = discovery_info.ip
LOGGER.debug("STEP: dhcp for host %s", host)
ip_address = discovery_info.ip
LOGGER.debug("STEP: dhcp for ip_address %s", ip_address)
self._async_abort_entries_match({CONF_HOST: host})
self._async_abort_entries_match({CONF_IP_ADDRESS: ip_address})
try:
self._serial = await validate_host_input(host, dhcp_mode=True)
self._dhcp_discovered_serial = await _async_poll_local_fireplace_for_serial(
ip_address, dhcp_mode=True
)
except (ConnectionError, ClientConnectionError):
LOGGER.debug(
"DHCP Discovery has determined %s is not an IntelliFire device", host
"DHCP Discovery has determined %s is not an IntelliFire device",
ip_address,
)
return self.async_abort(reason="not_intellifire_device")
await self.async_set_unique_id(self._serial)
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self._discovered_host = DiscoveredHostInfo(ip=host, serial=self._serial)
placeholders = {CONF_HOST: host, "serial": self._serial}
self.context["title_placeholders"] = placeholders
self._set_confirm_only()
return await self.async_step_dhcp_confirm()
async def async_step_dhcp_confirm(self, user_input=None):
"""Attempt to confirm."""
LOGGER.debug("STEP: dhcp_confirm")
# Add the hosts one by one
host = self._discovered_host.ip
serial = self._discovered_host.serial
if user_input is None:
# Show the confirmation dialog
return self.async_show_form(
step_id="dhcp_confirm",
description_placeholders={CONF_HOST: host, "serial": serial},
)
return self.async_create_entry(
title=f"Fireplace {serial}",
data={CONF_HOST: host},
)
return await self.async_step_cloud_api()

View File

@@ -5,11 +5,22 @@ from __future__ import annotations
import logging
DOMAIN = "intellifire"
CONF_USER_ID = "user_id"
LOGGER = logging.getLogger(__package__)
DEFAULT_THERMOSTAT_TEMP = 21
CONF_USER_ID = "user_id" # part of the cloud cookie
CONF_WEB_CLIENT_ID = "web_client_id" # part of the cloud cookie
CONF_AUTH_COOKIE = "auth_cookie" # part of the cloud cookie
CONF_SERIAL = "serial"
CONF_READ_MODE = "cloud_read"
CONF_CONTROL_MODE = "cloud_control"
DEFAULT_THERMOSTAT_TEMP = 21
API_MODE_LOCAL = "local"
API_MODE_CLOUD = "cloud"
STARTUP_TIMEOUT = 600
INIT_WAIT_TIME_SECONDS = 10

View File

@@ -2,27 +2,27 @@
from __future__ import annotations
import asyncio
from datetime import timedelta
from aiohttp import ClientConnectionError
from intellifire4py import IntellifirePollData
from intellifire4py.intellifire import IntellifireAPILocal
from intellifire4py import UnifiedFireplace
from intellifire4py.control import IntelliFireController
from intellifire4py.model import IntelliFirePollData
from intellifire4py.read import IntelliFireDataProvider
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, LOGGER
class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData]):
class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntelliFirePollData]):
"""Class to manage the polling of the fireplace API."""
def __init__(
self,
hass: HomeAssistant,
api: IntellifireAPILocal,
fireplace: UnifiedFireplace,
) -> None:
"""Initialize the Coordinator."""
super().__init__(
@@ -31,36 +31,21 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData
name=DOMAIN,
update_interval=timedelta(seconds=15),
)
self._api = api
async def _async_update_data(self) -> IntellifirePollData:
if not self._api.is_polling_in_background:
LOGGER.info("Starting Intellifire Background Polling Loop")
await self._api.start_background_polling()
# Don't return uninitialized poll data
async with asyncio.timeout(15):
try:
await self._api.poll()
except (ConnectionError, ClientConnectionError) as exception:
raise UpdateFailed from exception
LOGGER.debug("Failure Count %d", self._api.failed_poll_attempts)
if self._api.failed_poll_attempts > 10:
LOGGER.debug("Too many polling errors - raising exception")
raise UpdateFailed
return self._api.data
self.fireplace = fireplace
@property
def read_api(self) -> IntellifireAPILocal:
def read_api(self) -> IntelliFireDataProvider:
"""Return the Status API pointer."""
return self._api
return self.fireplace.read_api
@property
def control_api(self) -> IntellifireAPILocal:
def control_api(self) -> IntelliFireController:
"""Return the control API."""
return self._api
return self.fireplace.control_api
async def _async_update_data(self) -> IntelliFirePollData:
return self.fireplace.data
@property
def device_info(self) -> DeviceInfo:
@@ -69,7 +54,6 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntellifirePollData
manufacturer="Hearth and Home",
model="IFT-WFM",
name="IntelliFire",
identifiers={("IntelliFire", f"{self.read_api.data.serial}]")},
sw_version=self.read_api.data.fw_ver_str,
configuration_url=f"http://{self._api.fireplace_ip}/poll",
identifiers={("IntelliFire", str(self.fireplace.serial))},
configuration_url=f"http://{self.fireplace.ip_address}/poll",
)

View File

@@ -9,7 +9,7 @@ from . import IntellifireDataUpdateCoordinator
class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]):
"""Define a generic class for Intellifire entities."""
"""Define a generic class for IntelliFire entities."""
_attr_attribution = "Data provided by unpublished Intellifire API"
_attr_has_entity_name = True
@@ -22,6 +22,8 @@ class IntellifireEntity(CoordinatorEntity[IntellifireDataUpdateCoordinator]):
"""Class initializer."""
super().__init__(coordinator=coordinator)
self.entity_description = description
self._attr_unique_id = f"{description.key}_{coordinator.read_api.data.serial}"
self._attr_unique_id = f"{description.key}_{coordinator.fireplace.serial}"
self.identifiers = ({("IntelliFire", f"{coordinator.fireplace.serial}]")},)
# Configure the Device Info
self._attr_device_info = self.coordinator.device_info

View File

@@ -7,7 +7,8 @@ from dataclasses import dataclass
import math
from typing import Any
from intellifire4py import IntellifireControlAsync, IntellifirePollData
from intellifire4py.control import IntelliFireController
from intellifire4py.model import IntelliFirePollData
from homeassistant.components.fan import (
FanEntity,
@@ -31,8 +32,8 @@ from .entity import IntellifireEntity
class IntellifireFanRequiredKeysMixin:
"""Required keys for fan entity."""
set_fn: Callable[[IntellifireControlAsync, int], Awaitable]
value_fn: Callable[[IntellifirePollData], bool]
set_fn: Callable[[IntelliFireController, int], Awaitable]
value_fn: Callable[[IntelliFirePollData], int]
speed_range: tuple[int, int]
@@ -91,7 +92,8 @@ class IntellifireFan(IntellifireEntity, FanEntity):
def percentage(self) -> int | None:
"""Return fan percentage."""
return ranged_value_to_percentage(
self.entity_description.speed_range, self.coordinator.read_api.data.fanspeed
self.entity_description.speed_range,
self.coordinator.read_api.data.fanspeed,
)
@property

View File

@@ -6,7 +6,8 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from intellifire4py import IntellifireControlAsync, IntellifirePollData
from intellifire4py.control import IntelliFireController
from intellifire4py.model import IntelliFirePollData
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@@ -27,8 +28,8 @@ from .entity import IntellifireEntity
class IntellifireLightRequiredKeysMixin:
"""Required keys for fan entity."""
set_fn: Callable[[IntellifireControlAsync, int], Awaitable]
value_fn: Callable[[IntellifirePollData], bool]
set_fn: Callable[[IntelliFireController, int], Awaitable]
value_fn: Callable[[IntelliFirePollData], int]
@dataclass(frozen=True)
@@ -56,7 +57,7 @@ class IntellifireLight(IntellifireEntity, LightEntity):
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
@property
def brightness(self):
def brightness(self) -> int:
"""Return the current brightness 0-255."""
return 85 * self.entity_description.value_fn(self.coordinator.read_api.data)

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/intellifire",
"iot_class": "local_polling",
"loggers": ["intellifire4py"],
"requirements": ["intellifire4py==2.2.2"]
"requirements": ["intellifire4py==4.1.9"]
}

View File

@@ -6,8 +6,6 @@ from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from intellifire4py import IntellifirePollData
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@@ -29,7 +27,9 @@ from .entity import IntellifireEntity
class IntellifireSensorRequiredKeysMixin:
"""Mixin for required keys."""
value_fn: Callable[[IntellifirePollData], int | str | datetime | None]
value_fn: Callable[
[IntellifireDataUpdateCoordinator], int | str | datetime | float | None
]
@dataclass(frozen=True)
@@ -40,16 +40,29 @@ class IntellifireSensorEntityDescription(
"""Describes a sensor entity."""
def _time_remaining_to_timestamp(data: IntellifirePollData) -> datetime | None:
def _time_remaining_to_timestamp(
coordinator: IntellifireDataUpdateCoordinator,
) -> datetime | None:
"""Define a sensor that takes into account timezone."""
if not (seconds_offset := data.timeremaining_s):
if not (seconds_offset := coordinator.data.timeremaining_s):
return None
return utcnow() + timedelta(seconds=seconds_offset)
def _downtime_to_timestamp(data: IntellifirePollData) -> datetime | None:
def _downtime_to_timestamp(
coordinator: IntellifireDataUpdateCoordinator,
) -> datetime | None:
"""Define a sensor that takes into account a timezone."""
if not (seconds_offset := data.downtime):
if not (seconds_offset := coordinator.data.downtime):
return None
return utcnow() - timedelta(seconds=seconds_offset)
def _uptime_to_timestamp(
coordinator: IntellifireDataUpdateCoordinator,
) -> datetime | None:
"""Return a timestamp of how long the sensor has been up."""
if not (seconds_offset := coordinator.data.uptime):
return None
return utcnow() - timedelta(seconds=seconds_offset)
@@ -60,14 +73,14 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
translation_key="flame_height",
state_class=SensorStateClass.MEASUREMENT,
# UI uses 1-5 for flame height, backing lib uses 0-4
value_fn=lambda data: (data.flameheight + 1),
value_fn=lambda coordinator: (coordinator.data.flameheight + 1),
),
IntellifireSensorEntityDescription(
key="temperature",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: data.temperature_c,
value_fn=lambda coordinator: coordinator.data.temperature_c,
),
IntellifireSensorEntityDescription(
key="target_temp",
@@ -75,13 +88,13 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: data.thermostat_setpoint_c,
value_fn=lambda coordinator: coordinator.data.thermostat_setpoint_c,
),
IntellifireSensorEntityDescription(
key="fan_speed",
translation_key="fan_speed",
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.fanspeed,
value_fn=lambda coordinator: coordinator.data.fanspeed,
),
IntellifireSensorEntityDescription(
key="timer_end_timestamp",
@@ -102,27 +115,27 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
translation_key="uptime",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: utcnow() - timedelta(seconds=data.uptime),
value_fn=_uptime_to_timestamp,
),
IntellifireSensorEntityDescription(
key="connection_quality",
translation_key="connection_quality",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.connection_quality,
value_fn=lambda coordinator: coordinator.data.connection_quality,
entity_registry_enabled_default=False,
),
IntellifireSensorEntityDescription(
key="ecm_latency",
translation_key="ecm_latency",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.ecm_latency,
value_fn=lambda coordinator: coordinator.data.ecm_latency,
entity_registry_enabled_default=False,
),
IntellifireSensorEntityDescription(
key="ipv4_address",
translation_key="ipv4_address",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda data: data.ipv4_address,
value_fn=lambda coordinator: coordinator.data.ipv4_address,
),
)
@@ -134,17 +147,17 @@ async def async_setup_entry(
coordinator: IntellifireDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
IntellifireSensor(coordinator=coordinator, description=description)
IntelliFireSensor(coordinator=coordinator, description=description)
for description in INTELLIFIRE_SENSORS
)
class IntellifireSensor(IntellifireEntity, SensorEntity):
"""Extends IntellifireEntity with Sensor specific logic."""
class IntelliFireSensor(IntellifireEntity, SensorEntity):
"""Extends IntelliFireEntity with Sensor specific logic."""
entity_description: IntellifireSensorEntityDescription
@property
def native_value(self) -> int | str | datetime | None:
def native_value(self) -> int | str | datetime | float | None:
"""Return the state."""
return self.entity_description.value_fn(self.coordinator.read_api.data)
return self.entity_description.value_fn(self.coordinator)

View File

@@ -1,39 +1,30 @@
{
"config": {
"flow_title": "{serial} ({host})",
"flow_title": "{serial}",
"step": {
"manual_device_entry": {
"description": "Local Configuration",
"data": {
"host": "Host (IP Address)"
}
"pick_cloud_device": {
"title": "Configure fireplace",
"description": "Select fireplace by serial number:"
},
"api_config": {
"cloud_api": {
"description": "Authenticate against IntelliFire Cloud",
"data_description": {
"username": "Your IntelliFire app username",
"password": "Your IntelliFire app password"
},
"data": {
"username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"dhcp_confirm": {
"description": "Do you want to set up {host}\nSerial: {serial}?"
},
"pick_device": {
"title": "Device Selection",
"description": "The following IntelliFire devices were discovered. Please select which you wish to configure.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"api_error": "Login failed",
"iftapi_connect": "Error conecting to iftapi.net"
"api_error": "Login failed"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"not_intellifire_device": "Not an IntelliFire Device."
"not_intellifire_device": "Not an IntelliFire device.",
"no_available_devices": "All available devices have already been configured."
}
},
"entity": {

View File

@@ -6,16 +6,13 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from intellifire4py import IntellifirePollData
from intellifire4py.intellifire import IntellifireAPILocal
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import IntellifireDataUpdateCoordinator
from .const import DOMAIN
from .coordinator import IntellifireDataUpdateCoordinator
from .entity import IntellifireEntity
@@ -23,9 +20,9 @@ from .entity import IntellifireEntity
class IntellifireSwitchRequiredKeysMixin:
"""Mixin for required keys."""
on_fn: Callable[[IntellifireAPILocal], Awaitable]
off_fn: Callable[[IntellifireAPILocal], Awaitable]
value_fn: Callable[[IntellifirePollData], bool]
on_fn: Callable[[IntellifireDataUpdateCoordinator], Awaitable]
off_fn: Callable[[IntellifireDataUpdateCoordinator], Awaitable]
value_fn: Callable[[IntellifireDataUpdateCoordinator], bool]
@dataclass(frozen=True)
@@ -39,16 +36,16 @@ INTELLIFIRE_SWITCHES: tuple[IntellifireSwitchEntityDescription, ...] = (
IntellifireSwitchEntityDescription(
key="on_off",
translation_key="flame",
on_fn=lambda control_api: control_api.flame_on(),
off_fn=lambda control_api: control_api.flame_off(),
value_fn=lambda data: data.is_on,
on_fn=lambda coordinator: coordinator.control_api.flame_on(),
off_fn=lambda coordinator: coordinator.control_api.flame_off(),
value_fn=lambda coordinator: coordinator.read_api.data.is_on,
),
IntellifireSwitchEntityDescription(
key="pilot",
translation_key="pilot_light",
on_fn=lambda control_api: control_api.pilot_on(),
off_fn=lambda control_api: control_api.pilot_off(),
value_fn=lambda data: data.pilot_on,
on_fn=lambda coordinator: coordinator.control_api.pilot_on(),
off_fn=lambda coordinator: coordinator.control_api.pilot_off(),
value_fn=lambda coordinator: coordinator.read_api.data.pilot_on,
),
)
@@ -74,15 +71,15 @@ class IntellifireSwitch(IntellifireEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the switch."""
await self.entity_description.on_fn(self.coordinator.control_api)
await self.entity_description.on_fn(self.coordinator)
await self.async_update_ha_state(force_refresh=True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch."""
await self.entity_description.off_fn(self.coordinator.control_api)
await self.entity_description.off_fn(self.coordinator)
await self.async_update_ha_state(force_refresh=True)
@property
def is_on(self) -> bool | None:
"""Return the on state."""
return self.entity_description.value_fn(self.coordinator.read_api.data)
return self.entity_description.value_fn(self.coordinator)

View File

@@ -244,8 +244,8 @@ class IndexSensor(IQVIAEntity, SensorEntity):
key = self.entity_description.key.split("_")[-1].title()
try:
[period] = [p for p in data["periods"] if p["Type"] == key] # type: ignore[index]
except TypeError:
period = next(p for p in data["periods"] if p["Type"] == key) # type: ignore[index]
except StopIteration:
return
data = cast(dict[str, Any], data)

View File

@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.1.1",
"xknxproject==3.7.1",
"knx-frontend==2024.8.9.225351"
"knx-frontend==2024.9.4.64538"
],
"single_config_entry": true
}

View File

@@ -53,6 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
cloud_client = LaMarzoccoCloudClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
client=get_async_client(hass),
)
# initialize local API

View File

@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["lmcloud"],
"requirements": ["lmcloud==1.1.13"]
"requirements": ["lmcloud==1.2.2"]
}

View File

@@ -17,7 +17,6 @@
"models": [
"LIFX A19",
"LIFX A21",
"LIFX B10",
"LIFX Beam",
"LIFX BR30",
"LIFX Candle",
@@ -41,7 +40,6 @@
"LIFX Round",
"LIFX Square",
"LIFX String",
"LIFX T10",
"LIFX Tile",
"LIFX White",
"LIFX Z"
@@ -50,7 +48,7 @@
"iot_class": "local_polling",
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
"requirements": [
"aiolifx==1.0.8",
"aiolifx==1.0.9",
"aiolifx-effects==0.3.2",
"aiolifx-themes==0.5.0"
]

View File

@@ -1,17 +1,22 @@
"""Support for LinkPlay devices."""
from dataclasses import dataclass
from aiohttp import ClientSession
from linkplay.bridge import LinkPlayBridge
from linkplay.discovery import linkplay_factory_bridge
from linkplay.discovery import linkplay_factory_httpapi_bridge
from linkplay.exceptions import LinkPlayRequestException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import PLATFORMS
from .utils import async_get_client_session
@dataclass
class LinkPlayData:
"""Data for LinkPlay."""
@@ -24,16 +29,17 @@ type LinkPlayConfigEntry = ConfigEntry[LinkPlayData]
async def async_setup_entry(hass: HomeAssistant, entry: LinkPlayConfigEntry) -> bool:
"""Async setup hass config entry. Called when an entry has been setup."""
session = async_get_clientsession(hass)
if (
bridge := await linkplay_factory_bridge(entry.data[CONF_HOST], session)
) is None:
session: ClientSession = await async_get_client_session(hass)
bridge: LinkPlayBridge | None = None
try:
bridge = await linkplay_factory_httpapi_bridge(entry.data[CONF_HOST], session)
except LinkPlayRequestException as exception:
raise ConfigEntryNotReady(
f"Failed to connect to LinkPlay device at {entry.data[CONF_HOST]}"
)
) from exception
entry.runtime_data = LinkPlayData()
entry.runtime_data.bridge = bridge
entry.runtime_data = LinkPlayData(bridge=bridge)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -1,16 +1,22 @@
"""Config flow to configure LinkPlay component."""
import logging
from typing import Any
from linkplay.discovery import linkplay_factory_bridge
from aiohttp import ClientSession
from linkplay.bridge import LinkPlayBridge
from linkplay.discovery import linkplay_factory_httpapi_bridge
from linkplay.exceptions import LinkPlayRequestException
import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_MODEL
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .utils import async_get_client_session
_LOGGER = logging.getLogger(__name__)
class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
@@ -25,10 +31,15 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle Zeroconf discovery."""
session = async_get_clientsession(self.hass)
bridge = await linkplay_factory_bridge(discovery_info.host, session)
session: ClientSession = await async_get_client_session(self.hass)
bridge: LinkPlayBridge | None = None
if bridge is None:
try:
bridge = await linkplay_factory_httpapi_bridge(discovery_info.host, session)
except LinkPlayRequestException:
_LOGGER.exception(
"Failed to connect to LinkPlay device at %s", discovery_info.host
)
return self.async_abort(reason="cannot_connect")
self.data[CONF_HOST] = discovery_info.host
@@ -66,14 +77,26 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input:
session = async_get_clientsession(self.hass)
bridge = await linkplay_factory_bridge(user_input[CONF_HOST], session)
session: ClientSession = await async_get_client_session(self.hass)
bridge: LinkPlayBridge | None = None
try:
bridge = await linkplay_factory_httpapi_bridge(
user_input[CONF_HOST], session
)
except LinkPlayRequestException:
_LOGGER.exception(
"Failed to connect to LinkPlay device at %s", user_input[CONF_HOST]
)
errors["base"] = "cannot_connect"
if bridge is not None:
self.data[CONF_HOST] = user_input[CONF_HOST]
self.data[CONF_MODEL] = bridge.device.name
await self.async_set_unique_id(bridge.device.uuid)
await self.async_set_unique_id(
bridge.device.uuid, raise_on_progress=False
)
self._abort_if_unique_id_configured(
updates={CONF_HOST: self.data[CONF_HOST]}
)
@@ -83,7 +106,6 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
data={CONF_HOST: self.data[CONF_HOST]},
)
errors["base"] = "cannot_connect"
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),

View File

@@ -4,3 +4,4 @@ from homeassistant.const import Platform
DOMAIN = "linkplay"
PLATFORMS = [Platform.MEDIA_PLAYER]
CONF_SESSION = "session"

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/linkplay",
"integration_type": "hub",
"iot_class": "local_polling",
"requirements": ["python-linkplay==0.0.8"],
"requirements": ["python-linkplay==0.0.9"],
"zeroconf": ["_linkplay._tcp.local."]
}

View File

@@ -48,6 +48,17 @@ SOURCE_MAP: dict[PlayingMode, str] = {
PlayingMode.XLR: "XLR",
PlayingMode.HDMI: "HDMI",
PlayingMode.OPTICAL_2: "Optical 2",
PlayingMode.EXTERN_BLUETOOTH: "External Bluetooth",
PlayingMode.PHONO: "Phono",
PlayingMode.ARC: "ARC",
PlayingMode.COAXIAL_2: "Coaxial 2",
PlayingMode.TF_CARD_1: "SD Card 1",
PlayingMode.TF_CARD_2: "SD Card 2",
PlayingMode.CD: "CD",
PlayingMode.DAB: "DAB Radio",
PlayingMode.FM: "FM Radio",
PlayingMode.RCA: "RCA",
PlayingMode.UDISK: "USB",
}
SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k for k, v in SOURCE_MAP.items()}

View File

@@ -2,6 +2,14 @@
from typing import Final
from aiohttp import ClientSession
from linkplay.utils import async_create_unverified_client_session
from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE
from homeassistant.core import Event, HomeAssistant, callback
from .const import CONF_SESSION, DOMAIN
MANUFACTURER_ARTSOUND: Final[str] = "ArtSound"
MANUFACTURER_ARYLIC: Final[str] = "Arylic"
MANUFACTURER_IEAST: Final[str] = "iEAST"
@@ -44,3 +52,22 @@ def get_info_from_project(project: str) -> tuple[str, str]:
return MANUFACTURER_IEAST, MODELS_IEAST_AUDIOCAST_M5
case _:
return MANUFACTURER_GENERIC, MODELS_GENERIC
async def async_get_client_session(hass: HomeAssistant) -> ClientSession:
"""Get a ClientSession that can be used with LinkPlay devices."""
hass.data.setdefault(DOMAIN, {})
if CONF_SESSION not in hass.data[DOMAIN]:
clientsession: ClientSession = await async_create_unverified_client_session()
@callback
def _async_close_websession(event: Event) -> None:
"""Close websession."""
clientsession.detach()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_CLOSE, _async_close_websession)
hass.data[DOMAIN][CONF_SESSION] = clientsession
return clientsession
session: ClientSession = hass.data[DOMAIN][CONF_SESSION]
return session

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/madvr",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["py-madvr2==1.6.29"]
"requirements": ["py-madvr2==1.6.32"]
}

View File

@@ -384,7 +384,7 @@ DISCOVERY_SCHEMAS = [
key="ThirdRealityEnergySensorWattAccumulated",
device_class=SensorDeviceClass.ENERGY,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_display_precision=3,
state_class=SensorStateClass.TOTAL_INCREASING,
measurement_to_ha=lambda x: x / 1000,

View File

@@ -48,6 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo
),
)
try:
await client.define_household_support()
about = await client.get_about()
version = create_version(about.version)
except MealieAuthenticationError as error:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/mealie",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["aiomealie==0.8.1"]
"requirements": ["aiomealie==0.9.2"]
}

View File

@@ -0,0 +1,36 @@
"""Diagnostics support for Modern Forms."""
from __future__ import annotations
from dataclasses import asdict
from typing import TYPE_CHECKING, Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import ModernFormsDataUpdateCoordinator
REDACT_CONFIG = {CONF_MAC}
REDACT_DEVICE_INFO = {"mac_address", "owner"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
if TYPE_CHECKING:
assert coordinator is not None
return {
"config_entry": async_redact_data(entry.as_dict(), REDACT_CONFIG),
"device": {
"info": async_redact_data(
asdict(coordinator.modern_forms.info), REDACT_DEVICE_INFO
),
"status": asdict(coordinator.modern_forms.status),
},
}

View File

@@ -166,38 +166,43 @@ class SignalUpdateCallback:
)
if not device_entry:
return
supported_traits = self._supported_traits(device_id)
for api_event_type, image_event in events.items():
if not (event_type := EVENT_NAME_MAP.get(api_event_type)):
continue
nest_event_id = image_event.event_token
attachment = {
"image": EVENT_THUMBNAIL_URL_FORMAT.format(
device_id=device_entry.id, event_token=image_event.event_token
),
}
if self._supports_clip(device_id):
attachment["video"] = EVENT_MEDIA_API_URL_FORMAT.format(
device_id=device_entry.id, event_token=image_event.event_token
)
message = {
"device_id": device_entry.id,
"type": event_type,
"timestamp": event_message.timestamp,
"nest_event_id": nest_event_id,
"attachment": attachment,
}
if (
TraitType.CAMERA_EVENT_IMAGE in supported_traits
or TraitType.CAMERA_CLIP_PREVIEW in supported_traits
):
attachment = {
"image": EVENT_THUMBNAIL_URL_FORMAT.format(
device_id=device_entry.id, event_token=image_event.event_token
)
}
if TraitType.CAMERA_CLIP_PREVIEW in supported_traits:
attachment["video"] = EVENT_MEDIA_API_URL_FORMAT.format(
device_id=device_entry.id, event_token=image_event.event_token
)
message["attachment"] = attachment
if image_event.zones:
message["zones"] = image_event.zones
self._hass.bus.async_fire(NEST_EVENT, message)
def _supports_clip(self, device_id: str) -> bool:
def _supported_traits(self, device_id: str) -> list[TraitType]:
if not (
device_manager := self._hass.data[DOMAIN]
.get(self._config_entry_id, {})
.get(DATA_DEVICE_MANAGER)
) or not (device := device_manager.devices.get(device_id)):
return False
return TraitType.CAMERA_CLIP_PREVIEW in device.traits
return []
return list(device.traits)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nice_go",
"iot_class": "cloud_push",
"loggers": ["nice-go"],
"requirements": ["nice-go==0.3.5"]
"requirements": ["nice-go==0.3.8"]
}

View File

@@ -13,6 +13,7 @@ from homeassistant.const import CONF_URL, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.util.ssl import get_default_context
from .const import (
CONF_KEEP_ALIVE,
@@ -43,7 +44,7 @@ PLATFORMS = (Platform.CONVERSATION,)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Ollama from a config entry."""
settings = {**entry.data, **entry.options}
client = ollama.AsyncClient(host=settings[CONF_URL])
client = ollama.AsyncClient(host=settings[CONF_URL], verify=get_default_context())
try:
async with asyncio.timeout(DEFAULT_TIMEOUT):
await client.list()

View File

@@ -33,6 +33,7 @@ from homeassistant.helpers.selector import (
TextSelectorConfig,
TextSelectorType,
)
from homeassistant.util.ssl import get_default_context
from .const import (
CONF_KEEP_ALIVE,
@@ -91,7 +92,9 @@ class OllamaConfigFlow(ConfigFlow, domain=DOMAIN):
errors = {}
try:
self.client = ollama.AsyncClient(host=self.url)
self.client = ollama.AsyncClient(
host=self.url, verify=get_default_context()
)
async with asyncio.timeout(DEFAULT_TIMEOUT):
response = await self.client.list()

View File

@@ -11,7 +11,7 @@ import pyeiscp
import voluptuous as vol
from homeassistant.components.media_player import (
DOMAIN,
DOMAIN as MEDIA_PLAYER_DOMAIN,
PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
MediaPlayerEntity,
MediaPlayerEntityFeature,
@@ -28,9 +28,14 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.hass_dict import HassKey
_LOGGER = logging.getLogger(__name__)
DOMAIN = "onkyo"
DATA_MP_ENTITIES: HassKey[list[dict[str, OnkyoMediaPlayer]]] = HassKey(DOMAIN)
CONF_SOURCES = "sources"
CONF_MAX_VOLUME = "max_volume"
CONF_RECEIVER_MAX_VOLUME = "receiver_max_volume"
@@ -148,6 +153,33 @@ class ReceiverInfo:
identifier: str
async def async_register_services(hass: HomeAssistant) -> None:
"""Register Onkyo services."""
async def async_service_handle(service: ServiceCall) -> None:
"""Handle for services."""
entity_ids = service.data[ATTR_ENTITY_ID]
targets: list[OnkyoMediaPlayer] = []
for receiver_entities in hass.data[DATA_MP_ENTITIES]:
targets.extend(
entity
for entity in receiver_entities.values()
if entity.entity_id in entity_ids
)
for target in targets:
if service.service == SERVICE_SELECT_HDMI_OUTPUT:
await target.async_select_output(service.data[ATTR_HDMI_OUTPUT])
hass.services.async_register(
MEDIA_PLAYER_DOMAIN,
SERVICE_SELECT_HDMI_OUTPUT,
async_service_handle,
schema=ONKYO_SELECT_OUTPUT_SCHEMA,
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
@@ -155,29 +187,10 @@ async def async_setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Onkyo platform."""
await async_register_services(hass)
receivers: dict[str, pyeiscp.Connection] = {} # indexed by host
entities: dict[str, dict[str, OnkyoMediaPlayer]] = {} # indexed by host and zone
async def async_service_handle(service: ServiceCall) -> None:
"""Handle for services."""
entity_ids = service.data[ATTR_ENTITY_ID]
targets = [
entity
for h in entities.values()
for entity in h.values()
if entity.entity_id in entity_ids
]
for target in targets:
if service.service == SERVICE_SELECT_HDMI_OUTPUT:
await target.async_select_output(service.data[ATTR_HDMI_OUTPUT])
hass.services.async_register(
DOMAIN,
SERVICE_SELECT_HDMI_OUTPUT,
async_service_handle,
schema=ONKYO_SELECT_OUTPUT_SCHEMA,
)
all_entities = hass.data.setdefault(DATA_MP_ENTITIES, [])
host = config.get(CONF_HOST)
name = config.get(CONF_NAME)
@@ -188,6 +201,9 @@ async def async_setup_platform(
async def async_setup_receiver(
info: ReceiverInfo, discovered: bool, name: str | None
) -> None:
entities: dict[str, OnkyoMediaPlayer] = {}
all_entities.append(entities)
@callback
def async_onkyo_update_callback(
message: tuple[str, str, Any], origin: str
@@ -199,7 +215,7 @@ async def async_setup_platform(
)
zone, _, value = message
entity = entities[origin].get(zone)
entity = entities.get(zone)
if entity is not None:
if entity.enabled:
entity.process_update(message)
@@ -210,7 +226,7 @@ async def async_setup_platform(
zone_entity = OnkyoMediaPlayer(
receiver, sources, zone, max_volume, receiver_max_volume
)
entities[origin][zone] = zone_entity
entities[zone] = zone_entity
async_add_entities([zone_entity])
@callback
@@ -221,7 +237,7 @@ async def async_setup_platform(
"Receiver (re)connected: %s (%s)", receiver.name, receiver.host
)
for entity in entities[origin].values():
for entity in entities.values():
entity.backfill_state()
_LOGGER.debug("Creating receiver: %s (%s)", info.model_name, info.host)
@@ -237,9 +253,7 @@ async def async_setup_platform(
receiver.name = name or info.model_name
receiver.discovered = discovered
# Store the receiver object and create a dictionary to store its entities.
receivers[receiver.host] = receiver
entities[receiver.host] = {}
# Discover what zones are available for the receiver by querying the power.
# If we get a response for the specific zone, it means it is available.
@@ -251,7 +265,7 @@ async def async_setup_platform(
main_entity = OnkyoMediaPlayer(
receiver, sources, "main", max_volume, receiver_max_volume
)
entities[receiver.host]["main"] = main_entity
entities["main"] = main_entity
async_add_entities([main_entity])
if host is not None:

View File

@@ -19,6 +19,7 @@ from homeassistant.exceptions import (
ServiceValidationError,
)
from homeassistant.helpers import config_validation as cv, selector
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, LOGGER
@@ -88,7 +89,14 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) -> bool:
"""Set up OpenAI Conversation from a config entry."""
client = openai.AsyncOpenAI(api_key=entry.data[CONF_API_KEY])
client = openai.AsyncOpenAI(
api_key=entry.data[CONF_API_KEY],
http_client=get_async_client(hass),
)
# Cache current platform data which gets added to each request (caching done by library)
_ = await hass.async_add_executor_job(client.platform_headers)
try:
await hass.async_add_executor_job(client.with_options(timeout=10.0).models.list)
except openai.AuthenticationError as err:

View File

@@ -110,7 +110,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
)
last_stat = await get_instance(self.hass).async_add_executor_job(
get_last_statistics, self.hass, 1, cost_statistic_id, True, set()
get_last_statistics, self.hass, 1, consumption_statistic_id, True, set()
)
if not last_stat:
_LOGGER.debug("Updating statistic for the first time")
@@ -124,7 +124,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
cost_reads = await self._async_get_cost_reads(
account,
self.api.utility.timezone(),
last_stat[cost_statistic_id][0]["start"],
last_stat[consumption_statistic_id][0]["start"],
)
if not cost_reads:
_LOGGER.debug("No recent usage/cost data. Skipping update")
@@ -141,7 +141,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
)
cost_sum = cast(float, stats[cost_statistic_id][0]["sum"])
consumption_sum = cast(float, stats[consumption_statistic_id][0]["sum"])
last_stats_time = stats[cost_statistic_id][0]["start"]
last_stats_time = stats[consumption_statistic_id][0]["start"]
cost_statistics = []
consumption_statistics = []
@@ -187,7 +187,17 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
else UnitOfVolume.CENTUM_CUBIC_FEET,
)
_LOGGER.debug(
"Adding %s statistics for %s",
len(cost_statistics),
cost_statistic_id,
)
async_add_external_statistics(self.hass, cost_metadata, cost_statistics)
_LOGGER.debug(
"Adding %s statistics for %s",
len(consumption_statistics),
consumption_statistic_id,
)
async_add_external_statistics(
self.hass, consumption_metadata, consumption_statistics
)

View File

@@ -15,6 +15,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import (
ConductivityConverter,
DataRateConverter,
DistanceConverter,
DurationConverter,
@@ -48,7 +49,7 @@ from .util import PERIOD_SCHEMA, get_instance, resolve_period
UNIT_SCHEMA = vol.Schema(
{
vol.Optional("conductivity"): vol.In(DataRateConverter.VALID_UNITS),
vol.Optional("conductivity"): vol.In(ConductivityConverter.VALID_UNITS),
vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS),
vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional("duration"): vol.In(DurationConverter.VALID_UNITS),

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "platinum",
"requirements": ["renault-api==0.2.5"]
"requirements": ["renault-api==0.2.7"]
}

View File

@@ -81,6 +81,8 @@ class RingCam(RingEntity[RingDoorBell], Camera):
history_data = self._device.last_history
if history_data:
self._last_event = history_data[0]
# will call async_update to update the attributes and get the
# video url from the api
self.async_schedule_update_ha_state(True)
else:
self._last_event = None
@@ -183,7 +185,7 @@ class RingCam(RingEntity[RingDoorBell], Camera):
await self._device.async_set_motion_detection(new_state)
self._attr_motion_detection_enabled = new_state
self.async_schedule_update_ha_state(False)
self.async_write_ha_state()
async def async_enable_motion_detection(self) -> None:
"""Enable motion detection in the camera."""

View File

@@ -17,6 +17,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_2FA, DOMAIN
@@ -31,7 +32,10 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, Any]:
"""Validate the user input allows us to connect."""
auth = Auth(f"{APPLICATION_NAME}/{ha_version}")
auth = Auth(
f"{APPLICATION_NAME}/{ha_version}",
http_client_session=async_get_clientsession(hass),
)
try:
token = await auth.async_fetch_token(

View File

@@ -86,7 +86,7 @@ class RingLight(RingEntity[RingStickUpCam], LightEntity):
self._attr_is_on = new_state == OnOffState.ON
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
self.async_schedule_update_ha_state()
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the light on for 30 seconds."""

View File

@@ -87,7 +87,7 @@ class SirenSwitch(BaseRingSwitch):
self._attr_is_on = new_state > 0
self._no_updates_until = dt_util.utcnow() + SKIP_UPDATES_DELAY
self.async_schedule_update_ha_state()
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the siren on for 30 seconds."""

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/ruckus_unleashed",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioruckus", "xmltodict"],
"requirements": ["aioruckus==0.34"]
"loggers": ["aioruckus"],
"requirements": ["aioruckus==0.41"]
}

View File

@@ -793,8 +793,7 @@ class ShellyRpcPollingCoordinator(ShellyCoordinatorBase[RpcDevice]):
LOGGER.debug("Polling Shelly RPC Device - %s", self.name)
try:
await self.device.update_status()
await self.device.get_dynamic_components()
await self.device.poll()
except (DeviceConnectionError, RpcCallError) as err:
raise UpdateFailed(f"Device disconnected: {err!r}") from err
except InvalidAuthError:

View File

@@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["aioshelly"],
"quality_scale": "platinum",
"requirements": ["aioshelly==11.3.0"],
"requirements": ["aioshelly==11.4.2"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@@ -1,10 +1,9 @@
"""Support for controlling Sisyphus Kinetic Art Tables."""
# mypy: ignore-errors
import asyncio
import logging
# from sisyphus_control import Table
from sisyphus_control import Table
import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform

View File

@@ -2,9 +2,8 @@
"domain": "sisyphus",
"name": "Sisyphus",
"codeowners": ["@jkeljo"],
"disabled": "This integration is disabled because it uses an old version of socketio.",
"documentation": "https://www.home-assistant.io/integrations/sisyphus",
"iot_class": "local_push",
"loggers": ["sisyphus_control"],
"requirements": ["sisyphus-control==3.1.3"]
"requirements": ["sisyphus-control==3.1.4"]
}

View File

@@ -1,11 +1,10 @@
"""Support for track controls on the Sisyphus Kinetic Art Table."""
# mypy: ignore-errors
from __future__ import annotations
import aiohttp
from sisyphus_control import Track
# from sisyphus_control import Track
from homeassistant.components.media_player import (
MediaPlayerEntity,
MediaPlayerEntityFeature,

View File

@@ -1,5 +0,0 @@
extend = "../../../pyproject.toml"
lint.extend-ignore = [
"F821"
]

View File

@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["PyTurboJPEG==1.7.1", "ha-av==10.1.1", "numpy==1.26.0"]
"requirements": ["PyTurboJPEG==1.7.5", "ha-av==10.1.1", "numpy==1.26.0"]
}

View File

@@ -39,5 +39,5 @@
"documentation": "https://www.home-assistant.io/integrations/switchbot",
"iot_class": "local_push",
"loggers": ["switchbot"],
"requirements": ["PySwitchbot==0.48.1"]
"requirements": ["PySwitchbot==0.48.2"]
}

View File

@@ -16,6 +16,7 @@ from homeassistant.components.climate import (
SWING_BOTH,
SWING_HORIZONTAL,
SWING_OFF,
SWING_ON,
SWING_VERTICAL,
ClimateEntity,
ClimateEntityFeature,
@@ -47,7 +48,6 @@ from .const import (
HA_TO_TADO_FAN_MODE_MAP,
HA_TO_TADO_FAN_MODE_MAP_LEGACY,
HA_TO_TADO_HVAC_MODE_MAP,
HA_TO_TADO_SWING_MODE_MAP,
ORDERED_KNOWN_TADO_MODES,
PRESET_AUTO,
SIGNAL_TADO_UPDATE_RECEIVED,
@@ -55,17 +55,20 @@ from .const import (
SUPPORT_PRESET_MANUAL,
TADO_DEFAULT_MAX_TEMP,
TADO_DEFAULT_MIN_TEMP,
TADO_FAN_LEVELS,
TADO_FAN_SPEEDS,
TADO_FANLEVEL_SETTING,
TADO_FANSPEED_SETTING,
TADO_HORIZONTAL_SWING_SETTING,
TADO_HVAC_ACTION_TO_HA_HVAC_ACTION,
TADO_MODES_WITH_NO_TEMP_SETTING,
TADO_SWING_OFF,
TADO_SWING_ON,
TADO_SWING_SETTING,
TADO_TO_HA_FAN_MODE_MAP,
TADO_TO_HA_FAN_MODE_MAP_LEGACY,
TADO_TO_HA_HVAC_MODE_MAP,
TADO_TO_HA_OFFSET_MAP,
TADO_TO_HA_SWING_MODE_MAP,
TADO_VERTICAL_SWING_SETTING,
TEMP_OFFSET,
TYPE_AIR_CONDITIONING,
TYPE_HEATING,
@@ -166,29 +169,30 @@ def create_climate_entity(
supported_hvac_modes.append(TADO_TO_HA_HVAC_MODE_MAP[mode])
if (
capabilities[mode].get("swings")
or capabilities[mode].get("verticalSwing")
or capabilities[mode].get("horizontalSwing")
TADO_SWING_SETTING in capabilities[mode]
or TADO_VERTICAL_SWING_SETTING in capabilities[mode]
or TADO_VERTICAL_SWING_SETTING in capabilities[mode]
):
support_flags |= ClimateEntityFeature.SWING_MODE
supported_swing_modes = []
if capabilities[mode].get("swings"):
if TADO_SWING_SETTING in capabilities[mode]:
supported_swing_modes.append(
TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_ON]
)
if capabilities[mode].get("verticalSwing"):
if TADO_VERTICAL_SWING_SETTING in capabilities[mode]:
supported_swing_modes.append(SWING_VERTICAL)
if capabilities[mode].get("horizontalSwing"):
if TADO_HORIZONTAL_SWING_SETTING in capabilities[mode]:
supported_swing_modes.append(SWING_HORIZONTAL)
if (
SWING_HORIZONTAL in supported_swing_modes
and SWING_HORIZONTAL in supported_swing_modes
and SWING_VERTICAL in supported_swing_modes
):
supported_swing_modes.append(SWING_BOTH)
supported_swing_modes.append(TADO_TO_HA_SWING_MODE_MAP[TADO_SWING_OFF])
if not capabilities[mode].get("fanSpeeds") and not capabilities[mode].get(
"fanLevel"
if (
TADO_FANSPEED_SETTING not in capabilities[mode]
and TADO_FANLEVEL_SETTING not in capabilities[mode]
):
continue
@@ -197,14 +201,15 @@ def create_climate_entity(
if supported_fan_modes:
continue
if capabilities[mode].get("fanSpeeds"):
if TADO_FANSPEED_SETTING in capabilities[mode]:
supported_fan_modes = generate_supported_fanmodes(
TADO_TO_HA_FAN_MODE_MAP_LEGACY, capabilities[mode]["fanSpeeds"]
TADO_TO_HA_FAN_MODE_MAP_LEGACY,
capabilities[mode][TADO_FANSPEED_SETTING],
)
else:
supported_fan_modes = generate_supported_fanmodes(
TADO_TO_HA_FAN_MODE_MAP, capabilities[mode]["fanLevel"]
TADO_TO_HA_FAN_MODE_MAP, capabilities[mode][TADO_FANLEVEL_SETTING]
)
cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"]
@@ -316,12 +321,16 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
self._target_temp: float | None = None
self._current_tado_fan_speed = CONST_FAN_OFF
self._current_tado_fan_level = CONST_FAN_OFF
self._current_tado_hvac_mode = CONST_MODE_OFF
self._current_tado_hvac_action = HVACAction.OFF
self._current_tado_swing_mode = TADO_SWING_OFF
self._current_tado_vertical_swing = TADO_SWING_OFF
self._current_tado_horizontal_swing = TADO_SWING_OFF
capabilities = tado.get_capabilities(zone_id)
self._current_tado_capabilities = capabilities
self._tado_zone_data: PyTado.TadoZone = {}
self._tado_geofence_data: dict[str, str] | None = None
@@ -382,20 +391,23 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
def fan_mode(self) -> str | None:
"""Return the fan setting."""
if self._ac_device:
return TADO_TO_HA_FAN_MODE_MAP.get(
self._current_tado_fan_speed,
TADO_TO_HA_FAN_MODE_MAP_LEGACY.get(
if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
return TADO_TO_HA_FAN_MODE_MAP_LEGACY.get(
self._current_tado_fan_speed, FAN_AUTO
),
)
)
if self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
return TADO_TO_HA_FAN_MODE_MAP.get(
self._current_tado_fan_level, FAN_AUTO
)
return FAN_AUTO
return None
def set_fan_mode(self, fan_mode: str) -> None:
"""Turn fan on/off."""
if self._current_tado_fan_speed in TADO_FAN_LEVELS:
self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode])
else:
if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP_LEGACY[fan_mode])
elif self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode])
@property
def preset_mode(self) -> str:
@@ -555,24 +567,30 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
swing = None
if self._attr_swing_modes is None:
return
if (
SWING_VERTICAL in self._attr_swing_modes
or SWING_HORIZONTAL in self._attr_swing_modes
):
if swing_mode == SWING_VERTICAL:
if swing_mode == SWING_OFF:
if self._is_valid_setting_for_hvac_mode(TADO_SWING_SETTING):
swing = TADO_SWING_OFF
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
horizontal_swing = TADO_SWING_OFF
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
vertical_swing = TADO_SWING_OFF
if swing_mode == SWING_ON:
swing = TADO_SWING_ON
if swing_mode == SWING_VERTICAL:
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
vertical_swing = TADO_SWING_ON
elif swing_mode == SWING_HORIZONTAL:
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
horizontal_swing = TADO_SWING_OFF
if swing_mode == SWING_HORIZONTAL:
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
vertical_swing = TADO_SWING_OFF
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
horizontal_swing = TADO_SWING_ON
elif swing_mode == SWING_BOTH:
if swing_mode == SWING_BOTH:
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
vertical_swing = TADO_SWING_ON
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
horizontal_swing = TADO_SWING_ON
elif swing_mode == SWING_OFF:
if SWING_VERTICAL in self._attr_swing_modes:
vertical_swing = TADO_SWING_OFF
if SWING_HORIZONTAL in self._attr_swing_modes:
horizontal_swing = TADO_SWING_OFF
else:
swing = HA_TO_TADO_SWING_MODE_MAP[swing_mode]
self._control_hvac(
swing_mode=swing,
@@ -596,21 +614,23 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
self._device_id
][TEMP_OFFSET][offset_key]
self._current_tado_fan_speed = (
self._tado_zone_data.current_fan_level
if self._tado_zone_data.current_fan_level is not None
else self._tado_zone_data.current_fan_speed
)
self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode
self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action
self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode
self._current_tado_vertical_swing = (
self._tado_zone_data.current_vertical_swing_mode
)
self._current_tado_horizontal_swing = (
self._tado_zone_data.current_horizontal_swing_mode
)
if self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
self._current_tado_fan_level = self._tado_zone_data.current_fan_level
if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed
if self._is_valid_setting_for_hvac_mode(TADO_SWING_SETTING):
self._current_tado_swing_mode = self._tado_zone_data.current_swing_mode
if self._is_valid_setting_for_hvac_mode(TADO_VERTICAL_SWING_SETTING):
self._current_tado_vertical_swing = (
self._tado_zone_data.current_vertical_swing_mode
)
if self._is_valid_setting_for_hvac_mode(TADO_HORIZONTAL_SWING_SETTING):
self._current_tado_horizontal_swing = (
self._tado_zone_data.current_horizontal_swing_mode
)
@callback
def _async_update_zone_callback(self) -> None:
@@ -665,7 +685,10 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
self._target_temp = target_temp
if fan_mode:
self._current_tado_fan_speed = fan_mode
if self._is_valid_setting_for_hvac_mode(TADO_FANSPEED_SETTING):
self._current_tado_fan_speed = fan_mode
if self._is_valid_setting_for_hvac_mode(TADO_FANLEVEL_SETTING):
self._current_tado_fan_level = fan_mode
if swing_mode:
self._current_tado_swing_mode = swing_mode
@@ -735,21 +758,32 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
fan_speed = None
fan_level = None
if self.supported_features & ClimateEntityFeature.FAN_MODE:
if self._current_tado_fan_speed in TADO_FAN_LEVELS:
fan_level = self._current_tado_fan_speed
elif self._current_tado_fan_speed in TADO_FAN_SPEEDS:
if self._is_current_setting_supported_by_current_hvac_mode(
TADO_FANSPEED_SETTING, self._current_tado_fan_speed
):
fan_speed = self._current_tado_fan_speed
if self._is_current_setting_supported_by_current_hvac_mode(
TADO_FANLEVEL_SETTING, self._current_tado_fan_level
):
fan_level = self._current_tado_fan_level
swing = None
vertical_swing = None
horizontal_swing = None
if (
self.supported_features & ClimateEntityFeature.SWING_MODE
) and self._attr_swing_modes is not None:
if SWING_VERTICAL in self._attr_swing_modes:
if self._is_current_setting_supported_by_current_hvac_mode(
TADO_VERTICAL_SWING_SETTING, self._current_tado_vertical_swing
):
vertical_swing = self._current_tado_vertical_swing
if SWING_HORIZONTAL in self._attr_swing_modes:
if self._is_current_setting_supported_by_current_hvac_mode(
TADO_HORIZONTAL_SWING_SETTING, self._current_tado_horizontal_swing
):
horizontal_swing = self._current_tado_horizontal_swing
if vertical_swing is None and horizontal_swing is None:
if self._is_current_setting_supported_by_current_hvac_mode(
TADO_SWING_SETTING, self._current_tado_swing_mode
):
swing = self._current_tado_swing_mode
self._tado.set_zone_overlay(
@@ -765,3 +799,20 @@ class TadoClimate(TadoZoneEntity, ClimateEntity):
vertical_swing=vertical_swing, # api defaults to not sending verticalSwing if swing not None
horizontal_swing=horizontal_swing, # api defaults to not sending horizontalSwing if swing not None
)
def _is_valid_setting_for_hvac_mode(self, setting: str) -> bool:
return (
self._current_tado_capabilities.get(self._current_tado_hvac_mode, {}).get(
setting
)
is not None
)
def _is_current_setting_supported_by_current_hvac_mode(
self, setting: str, current_state: str | None
) -> bool:
if self._is_valid_setting_for_hvac_mode(setting):
return current_state in self._current_tado_capabilities[
self._current_tado_hvac_mode
].get(setting, [])
return False

View File

@@ -234,3 +234,10 @@ CONF_READING = "reading"
ATTR_MESSAGE = "message"
WATER_HEATER_FALLBACK_REPAIR = "water_heater_fallback"
TADO_SWING_SETTING = "swings"
TADO_FANSPEED_SETTING = "fanSpeeds"
TADO_FANLEVEL_SETTING = "fanLevel"
TADO_VERTICAL_SWING_SETTING = "verticalSwing"
TADO_HORIZONTAL_SWING_SETTING = "horizontalSwing"

View File

@@ -41,6 +41,7 @@ from homeassistant.exceptions import TemplateError
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import async_get_loaded_integration
from homeassistant.util.ssl import get_default_context, get_default_no_verify_context
_LOGGER = logging.getLogger(__name__)
@@ -378,7 +379,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
for p_config in domain_config:
# Each platform config gets its own bot
bot = initialize_bot(hass, p_config)
bot = await hass.async_add_executor_job(initialize_bot, hass, p_config)
p_type: str = p_config[CONF_PLATFORM]
platform = platforms[p_type]
@@ -486,7 +487,7 @@ def initialize_bot(hass: HomeAssistant, p_config: dict) -> Bot:
# Auth can actually be stuffed into the URL, but the docs have previously
# indicated to put them here.
auth = proxy_params.pop("username"), proxy_params.pop("password")
ir.async_create_issue(
ir.create_issue(
hass,
DOMAIN,
"proxy_params_auth_deprecation",
@@ -503,7 +504,7 @@ def initialize_bot(hass: HomeAssistant, p_config: dict) -> Bot:
learn_more_url="https://github.com/home-assistant/core/pull/112778",
)
else:
ir.async_create_issue(
ir.create_issue(
hass,
DOMAIN,
"proxy_params_deprecation",
@@ -852,7 +853,11 @@ class TelegramNotificationService:
username=kwargs.get(ATTR_USERNAME),
password=kwargs.get(ATTR_PASSWORD),
authentication=kwargs.get(ATTR_AUTHENTICATION),
verify_ssl=kwargs.get(ATTR_VERIFY_SSL),
verify_ssl=(
get_default_context()
if kwargs.get(ATTR_VERIFY_SSL, False)
else get_default_no_verify_context()
),
)
if file_content:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/telegram_bot",
"iot_class": "cloud_push",
"loggers": ["telegram"],
"requirements": ["python-telegram-bot[socks]==21.0.1"]
"requirements": ["python-telegram-bot[socks]==21.5"]
}

View File

@@ -25,14 +25,22 @@ async def async_setup_platform(hass, bot, config):
async def process_error(update: Update, context: CallbackContext) -> None:
"""Telegram bot error handler."""
if context.error:
error_callback(context.error, update)
def error_callback(error: Exception, update: Update | None = None) -> None:
"""Log the error."""
try:
if context.error:
raise context.error
raise error
except (TimedOut, NetworkError, RetryAfter):
# Long polling timeout or connection problem. Nothing serious.
pass
except TelegramError:
_LOGGER.error('Update "%s" caused error: "%s"', update, context.error)
if update is not None:
_LOGGER.error('Update "%s" caused error: "%s"', update, error)
else:
_LOGGER.error("%s: %s", error.__class__.__name__, error)
class PollBot(BaseTelegramBotEntity):
@@ -53,7 +61,7 @@ class PollBot(BaseTelegramBotEntity):
"""Start the polling task."""
_LOGGER.debug("Starting polling")
await self.application.initialize()
await self.application.updater.start_polling()
await self.application.updater.start_polling(error_callback=error_callback)
await self.application.start()
async def stop_polling(self, event=None):

View File

@@ -70,7 +70,7 @@ NUMBER_CONFIG_SCHEMA = vol.Schema(
vol.Required(CONF_NAME): cv.template,
vol.Required(CONF_STATE): cv.template,
vol.Required(CONF_STEP): cv.template,
vol.Optional(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
vol.Required(CONF_SET_VALUE): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_MIN): cv.template,
vol.Optional(CONF_MAX): cv.template,
vol.Optional(CONF_DEVICE_ID): selector.DeviceSelector(),
@@ -154,11 +154,10 @@ class TemplateNumber(TemplateEntity, NumberEntity):
super().__init__(hass, config=config, unique_id=unique_id)
assert self._attr_name is not None
self._value_template = config[CONF_STATE]
self._command_set_value = (
Script(hass, config[CONF_SET_VALUE], self._attr_name, DOMAIN)
if config.get(CONF_SET_VALUE, None) is not None
else None
self._command_set_value = Script(
hass, config[CONF_SET_VALUE], self._attr_name, DOMAIN
)
self._step_template = config[CONF_STEP]
self._min_value_template = config[CONF_MIN]
self._max_value_template = config[CONF_MAX]

View File

@@ -68,6 +68,8 @@ EXCLUDED_FEATURES = {
# update
"current_firmware_version",
"available_firmware_version",
"update_available",
"check_latest_firmware",
}

View File

@@ -301,5 +301,5 @@
"iot_class": "local_polling",
"loggers": ["kasa"],
"quality_scale": "platinum",
"requirements": ["python-kasa[speedups]==0.7.1"]
"requirements": ["python-kasa[speedups]==0.7.2"]
}

View File

@@ -112,61 +112,36 @@ def _build_entities(
entities: list[ViCareBinarySensor] = []
for device in device_list:
entities.extend(_build_entities_for_device(device.api, device.config))
# add device entities
entities.extend(
_build_entities_for_component(
get_circuits(device.api), device.config, CIRCUIT_SENSORS
ViCareBinarySensor(
description,
device.config,
device.api,
)
for description in GLOBAL_SENSORS
if is_supported(description.key, description, device.api)
)
entities.extend(
_build_entities_for_component(
get_burners(device.api), device.config, BURNER_SENSORS
# add component entities
for component_list, entity_description_list in (
(get_circuits(device.api), CIRCUIT_SENSORS),
(get_burners(device.api), BURNER_SENSORS),
(get_compressors(device.api), COMPRESSOR_SENSORS),
):
entities.extend(
ViCareBinarySensor(
description,
device.config,
device.api,
component,
)
for component in component_list
for description in entity_description_list
if is_supported(description.key, description, component)
)
)
entities.extend(
_build_entities_for_component(
get_compressors(device.api), device.config, COMPRESSOR_SENSORS
)
)
return entities
def _build_entities_for_device(
device: PyViCareDevice,
device_config: PyViCareDeviceConfig,
) -> list[ViCareBinarySensor]:
"""Create device specific ViCare binary sensor entities."""
return [
ViCareBinarySensor(
device_config,
device,
description,
)
for description in GLOBAL_SENSORS
if is_supported(description.key, description, device)
]
def _build_entities_for_component(
components: list[PyViCareHeatingDeviceComponent],
device_config: PyViCareDeviceConfig,
entity_descriptions: tuple[ViCareBinarySensorEntityDescription, ...],
) -> list[ViCareBinarySensor]:
"""Create component specific ViCare binary sensor entities."""
return [
ViCareBinarySensor(
device_config,
component,
description,
)
for component in components
for description in entity_descriptions
if is_supported(description.key, description, component)
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@@ -190,12 +165,13 @@ class ViCareBinarySensor(ViCareEntity, BinarySensorEntity):
def __init__(
self,
device_config: PyViCareDeviceConfig,
api: PyViCareDevice | PyViCareHeatingDeviceComponent,
description: ViCareBinarySensorEntityDescription,
device_config: PyViCareDeviceConfig,
device: PyViCareDevice,
component: PyViCareHeatingDeviceComponent | None = None,
) -> None:
"""Initialize the sensor."""
super().__init__(device_config, api, description.key)
super().__init__(description.key, device_config, device, component)
self.entity_description = description
@property

View File

@@ -54,9 +54,9 @@ def _build_entities(
return [
ViCareButton(
description,
device.config,
device.api,
description,
)
for device in device_list
for description in BUTTON_DESCRIPTIONS
@@ -87,12 +87,12 @@ class ViCareButton(ViCareEntity, ButtonEntity):
def __init__(
self,
description: ViCareButtonEntityDescription,
device_config: PyViCareDeviceConfig,
device: PyViCareDevice,
description: ViCareButtonEntityDescription,
) -> None:
"""Initialize the button."""
super().__init__(device_config, device, description.key)
super().__init__(description.key, device_config, device)
self.entity_description = description
def press(self) -> None:

View File

@@ -148,7 +148,7 @@ class ViCareClimate(ViCareEntity, ClimateEntity):
circuit: PyViCareHeatingCircuit,
) -> None:
"""Initialize the climate device."""
super().__init__(device_config, device, circuit.id)
super().__init__(circuit.id, device_config, device)
self._circuit = circuit
self._attributes: dict[str, Any] = {}
self._attributes["vicare_programs"] = self._circuit.getPrograms()

View File

@@ -2,6 +2,9 @@
from PyViCare.PyViCareDevice import Device as PyViCareDevice
from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig
from PyViCare.PyViCareHeatingDevice import (
HeatingDeviceWithComponent as PyViCareHeatingDeviceComponent,
)
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
@@ -16,21 +19,24 @@ class ViCareEntity(Entity):
def __init__(
self,
unique_id_suffix: str,
device_config: PyViCareDeviceConfig,
device: PyViCareDevice,
unique_id_suffix: str,
component: PyViCareHeatingDeviceComponent | None = None,
) -> None:
"""Initialize the entity."""
self._api = device
self._api: PyViCareDevice | PyViCareHeatingDeviceComponent = (
component if component else device
)
self._attr_unique_id = f"{device_config.getConfig().serial}-{unique_id_suffix}"
# valid for compressors, circuits, burners (HeatingDeviceWithComponent)
if hasattr(device, "id"):
self._attr_unique_id += f"-{device.id}"
if component:
self._attr_unique_id += f"-{component.id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_config.getConfig().serial)},
serial_number=device_config.getConfig().serial,
serial_number=device.getSerial(),
name=device_config.getModel(),
manufacturer="Viessmann",
model=device_config.getModel(),

View File

@@ -129,7 +129,7 @@ class ViCareFan(ViCareEntity, FanEntity):
device: PyViCareDevice,
) -> None:
"""Initialize the fan entity."""
super().__init__(device_config, device, self._attr_translation_key)
super().__init__(self._attr_translation_key, device_config, device)
def update(self) -> None:
"""Update state of fan."""

View File

@@ -233,30 +233,30 @@ def _build_entities(
) -> list[ViCareNumber]:
"""Create ViCare number entities for a device."""
entities: list[ViCareNumber] = [
ViCareNumber(
device.config,
device.api,
description,
)
for device in device_list
for description in DEVICE_ENTITY_DESCRIPTIONS
if is_supported(description.key, description, device.api)
]
entities.extend(
[
entities: list[ViCareNumber] = []
for device in device_list:
# add device entities
entities.extend(
ViCareNumber(
device.config,
circuit,
description,
device.config,
device.api,
)
for description in DEVICE_ENTITY_DESCRIPTIONS
if is_supported(description.key, description, device.api)
)
# add component entities
entities.extend(
ViCareNumber(
description,
device.config,
device.api,
circuit,
)
for device in device_list
for circuit in get_circuits(device.api)
for description in CIRCUIT_ENTITY_DESCRIPTIONS
if is_supported(description.key, description, circuit)
]
)
)
return entities
@@ -283,12 +283,13 @@ class ViCareNumber(ViCareEntity, NumberEntity):
def __init__(
self,
device_config: PyViCareDeviceConfig,
api: PyViCareDevice | PyViCareHeatingDeviceComponent,
description: ViCareNumberEntityDescription,
device_config: PyViCareDeviceConfig,
device: PyViCareDevice,
component: PyViCareHeatingDeviceComponent | None = None,
) -> None:
"""Initialize the number."""
super().__init__(device_config, api, description.key)
super().__init__(description.key, device_config, device, component)
self.entity_description = description
@property

View File

@@ -747,7 +747,6 @@ GLOBAL_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
),
)
CIRCUIT_SENSORS: tuple[ViCareSensorEntityDescription, ...] = (
ViCareSensorEntityDescription(
key="supply_temperature",
@@ -865,61 +864,36 @@ def _build_entities(
entities: list[ViCareSensor] = []
for device in device_list:
entities.extend(_build_entities_for_device(device.api, device.config))
# add device entities
entities.extend(
_build_entities_for_component(
get_circuits(device.api), device.config, CIRCUIT_SENSORS
ViCareSensor(
description,
device.config,
device.api,
)
for description in GLOBAL_SENSORS
if is_supported(description.key, description, device.api)
)
entities.extend(
_build_entities_for_component(
get_burners(device.api), device.config, BURNER_SENSORS
# add component entities
for component_list, entity_description_list in (
(get_circuits(device.api), CIRCUIT_SENSORS),
(get_burners(device.api), BURNER_SENSORS),
(get_compressors(device.api), COMPRESSOR_SENSORS),
):
entities.extend(
ViCareSensor(
description,
device.config,
device.api,
component,
)
for component in component_list
for description in entity_description_list
if is_supported(description.key, description, component)
)
)
entities.extend(
_build_entities_for_component(
get_compressors(device.api), device.config, COMPRESSOR_SENSORS
)
)
return entities
def _build_entities_for_device(
device: PyViCareDevice,
device_config: PyViCareDeviceConfig,
) -> list[ViCareSensor]:
"""Create device specific ViCare sensor entities."""
return [
ViCareSensor(
device_config,
device,
description,
)
for description in GLOBAL_SENSORS
if is_supported(description.key, description, device)
]
def _build_entities_for_component(
components: list[PyViCareHeatingDeviceComponent],
device_config: PyViCareDeviceConfig,
entity_descriptions: tuple[ViCareSensorEntityDescription, ...],
) -> list[ViCareSensor]:
"""Create component specific ViCare sensor entities."""
return [
ViCareSensor(
device_config,
component,
description,
)
for component in components
for description in entity_descriptions
if is_supported(description.key, description, component)
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@@ -943,12 +917,13 @@ class ViCareSensor(ViCareEntity, SensorEntity):
def __init__(
self,
device_config: PyViCareDeviceConfig,
api: PyViCareDevice | PyViCareHeatingDeviceComponent,
description: ViCareSensorEntityDescription,
device_config: PyViCareDeviceConfig,
device: PyViCareDevice,
component: PyViCareHeatingDeviceComponent | None = None,
) -> None:
"""Initialize the sensor."""
super().__init__(device_config, api, description.key)
super().__init__(description.key, device_config, device, component)
self.entity_description = description
# run update to have device_class set depending on unit_of_measurement
self.update()

View File

@@ -113,7 +113,7 @@ class ViCareWater(ViCareEntity, WaterHeaterEntity):
circuit: PyViCareHeatingCircuit,
) -> None:
"""Initialize the DHW water_heater device."""
super().__init__(device_config, device, circuit.id)
super().__init__(circuit.id, device_config, device)
self._circuit = circuit
self._attributes: dict[str, Any] = {}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud",
"iot_class": "cloud_polling",
"loggers": ["weatherflow4py"],
"requirements": ["weatherflow4py==0.2.21"]
"requirements": ["weatherflow4py==0.2.23"]
}

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