Skip to content

Commit f151af2

Browse files
authored
Merge pull request #240 from jaroschek/release/1.8.x
Release 1.8.0
2 parents 63f6a50 + 767c844 commit f151af2

21 files changed

Lines changed: 620 additions & 80 deletions

custom_components/myuplink/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from homeassistant.core import HomeAssistant
1616
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
1717
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
18+
from homeassistant.helpers.device_registry import DeviceEntry
1819
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
1920

2021
from .api import AsyncConfigEntryAuth, MyUplink
@@ -120,3 +121,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
120121
)
121122

122123
return True
124+
125+
126+
async def async_remove_config_entry_device(
127+
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
128+
) -> bool:
129+
"""Remove a config entry from a device."""
130+
return True

custom_components/myuplink/api.py

Lines changed: 126 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from datetime import datetime, timedelta
88
import json
99
import logging
10+
from typing import Any
1011

11-
from aiohttp import ClientResponse, ClientSession
12+
from aiohttp import ClientResponse, ClientResponseError, ClientSession
1213

1314
from homeassistant.config_entries import ConfigEntry
1415
from homeassistant.const import (
@@ -26,6 +27,7 @@
2627
API_VERSION,
2728
CONF_ADDITIONAL_PARAMETER,
2829
CONF_ENABLE_SMART_HOME_MODE,
30+
CONF_ENABLE_SMART_HOME_ZONE,
2931
CONF_FETCH_FIRMWARE,
3032
CONF_FETCH_NOTIFICATIONS,
3133
CONF_PARAMETER_WHITELIST,
@@ -92,7 +94,7 @@ def type(self) -> str:
9294
@property
9395
def valid_until(self) -> datetime:
9496
"""Return datetime value of 'validUntil'."""
95-
return datetime(self.raw_data("validUntil"))
97+
return datetime.fromisoformat(self.raw_data["validUntil"])
9698

9799

98100
class Notification:
@@ -166,23 +168,23 @@ def firmware_id(self) -> int:
166168
return int(self.raw_data["firmwareId"])
167169

168170
@property
169-
def current_version(self) -> str:
171+
def current_version(self) -> str | None:
170172
"""Return the current firmware version of the device."""
171-
if self.raw_data["currentFwVersion"].strip() == "":
173+
if self.raw_data.get("currentFwVersion", "").strip() == "":
172174
return None
173175
return self.raw_data["currentFwVersion"].strip()
174176

175177
@property
176-
def pending_version(self) -> str:
178+
def pending_version(self) -> str | None:
177179
"""Return the pending firmware version of the device."""
178-
if self.raw_data["pendingFwVersion"].strip() == "":
180+
if self.raw_data.get("pendingFwVersion", "").strip() == "":
179181
return None
180182
return self.raw_data["pendingFwVersion"].strip()
181183

182184
@property
183-
def desired_version(self) -> str:
185+
def desired_version(self) -> str | None:
184186
"""Return the desired firmware version of the device."""
185-
if self.raw_data["desiredFwVersion"].strip() == "":
187+
if self.raw_data.get("desiredFwVersion", "").strip() == "":
186188
return None
187189
return self.raw_data["desiredFwVersion"].strip()
188190

@@ -266,15 +268,15 @@ def max_value(self) -> int:
266268
@property
267269
def step_value(self) -> int:
268270
"""Return the step value of the parameter."""
269-
return self.raw_data["stepValue"]
271+
return self.raw_data.get("stepValue", 1)
270272

271273
@property
272274
def enum_values(self) -> list[dict]:
273275
"""Return the enum values of the parameter."""
274276
return self.raw_data["enumValues"]
275277

276278
@property
277-
def scale_value(self) -> float | None:
279+
def scale_value(self) -> float:
278280
"""Return the scale value of the parameter."""
279281
if self.raw_data["scaleValue"]:
280282
return float(self.raw_data["scaleValue"])
@@ -287,11 +289,11 @@ def zone_id(self) -> str:
287289
return self.raw_data["zoneId"]
288290

289291
async def update_parameter(self, value) -> None:
290-
"""Patch parameter if writable."""
292+
"""Set parameter value if writable."""
291293
if not self.is_writable:
292294
return
293295
await self.device.system.api.patch_parameter(
294-
self.device.id, str(self.id), str(value)
296+
self.device.id, str(self.id), value
295297
)
296298

297299
def get_platform(self) -> Platform:
@@ -316,7 +318,9 @@ def get_platform(self) -> Platform:
316318
if len(self.enum_values) > 0 and self.is_writable:
317319
return Platform.SELECT
318320

319-
if (self.max_value or self.min_value) and self.is_writable:
321+
if (
322+
self.max_value is not None or self.min_value is not None
323+
) and self.is_writable:
320324
return Platform.NUMBER
321325

322326
return Platform.SENSOR
@@ -331,24 +335,26 @@ def get_unit(self, parameter_unit) -> str:
331335
UnitOfTemperature,
332336
UnitOfTime,
333337
):
334-
for unit in units:
335-
if parameter_unit.lower() == unit.lower():
336-
return str(unit)
338+
with suppress(ValueError):
339+
for unit in units:
340+
if parameter_unit.lower() == unit.lower():
341+
return str(unit)
337342

338343
return parameter_unit
339344

340345

341346
class Zone:
342347
"""Class that represents a zone object in the myUplink API."""
343348

344-
def __init__(self, raw_data: dict) -> None:
349+
def __init__(self, raw_data: dict, device: Device) -> None:
345350
"""Initialize a zone object."""
346351
self.raw_data = raw_data
352+
self.device = device
347353

348354
@property
349355
def id(self) -> int:
350356
"""Return the ID of the zone."""
351-
return self.raw_data["zoneId"]
357+
return int(self.raw_data["zoneId"])
352358

353359
@property
354360
def name(self) -> str:
@@ -358,62 +364,107 @@ def name(self) -> str:
358364
@property
359365
def is_command_only(self) -> bool:
360366
"""Return if the zone is command only."""
361-
return self.raw_data["commandOnly"]
367+
return bool(self.raw_data["commandOnly"])
362368

363369
@property
364-
def supported_modes(self) -> str:
370+
def supported_modes(self) -> str | None:
365371
"""Return the supported modes of the zone."""
366-
return self.raw_data["supportedModes"]
372+
return self.raw_data.get("supportedModes")
367373

368374
@property
369375
def mode(self) -> str:
370376
"""Return the current mode of the zone."""
371377
return self.raw_data["mode"]
372378

373379
@property
374-
def temperature(self) -> float:
380+
def temperature(self) -> float | None:
375381
"""Return the current temperature of the zone."""
376-
return self.raw_data["temperature"]
382+
return (
383+
float(self.raw_data.get("temperature"))
384+
if self.raw_data.get("temperature") is not None
385+
else None
386+
)
377387

378388
@property
379-
def setpoint(self) -> float:
389+
def setpoint(self) -> float | None:
380390
"""Return the target temperature of the zone."""
381-
return self.raw_data["setpoint"]
391+
return (
392+
float(self.raw_data.get("setpoint"))
393+
if self.raw_data.get("setpoint") is not None
394+
else None
395+
)
382396

383397
@property
384-
def setpoint_heating(self) -> float:
398+
def setpoint_heating(self) -> float | None:
385399
"""Return the heating setpoint value of the zone."""
386-
return self.raw_data["setpointHeat"]
400+
return (
401+
float(self.raw_data.get("setpointHeat"))
402+
if self.raw_data.get("setpointHeat") is not None
403+
else None
404+
)
387405

388406
@property
389-
def setpoint_cooling(self) -> float:
407+
def setpoint_cooling(self) -> float | None:
390408
"""Return the cooling setpoint value of the zone."""
391-
return self.raw_data["setpointCool"]
409+
return (
410+
float(self.raw_data.get("setpointCool"))
411+
if self.raw_data.get("setpointCool") is not None
412+
else None
413+
)
392414

393415
@property
394-
def setpoint_range_min(self) -> int:
416+
def setpoint_range_min(self) -> int | None:
395417
"""Return the minimum temperature range of the zone."""
396-
return self.raw_data["setpointRangeMin"]
418+
return (
419+
int(self.raw_data.get("setpointRangeMin"))
420+
if self.raw_data.get("setpointRangeMin") is not None
421+
else None
422+
)
397423

398424
@property
399-
def setpoint_range_max(self) -> int:
425+
def setpoint_range_max(self) -> int | None:
400426
"""Return the maximum temperature range of the zone."""
401-
return self.raw_data["setpointRangeMax"]
427+
return (
428+
int(self.raw_data.get("setpointRangeMax"))
429+
if self.raw_data.get("setpointRangeMax") is not None
430+
else None
431+
)
402432

403433
@property
404434
def is_celsius(self) -> bool:
405435
"""Return if the temperature in the zone is specified as celsius (true) or fahrenheit (false)."""
406-
return self.raw_data["isCelsius"]
436+
return (
437+
bool(self.raw_data.get("isCelsius"))
438+
if self.raw_data.get("isCelsius") is not None
439+
else True
440+
)
407441

408442
@property
409-
def indoor_co2(self) -> int:
443+
def indoor_co2(self) -> int | None:
410444
"""Return the indoor co2 level of the zone."""
411-
return self.raw_data["indoorCo2"]
445+
return (
446+
int(self.raw_data.get("indoorCo2"))
447+
if self.raw_data.get("indoorCo2") is not None
448+
else None
449+
)
412450

413451
@property
414-
def indoor_umidity(self) -> float:
452+
def indoor_humidity(self) -> float | None:
415453
"""Return the indoor humidity of the zone."""
416-
return self.raw_data["indoorHumidity"]
454+
return (
455+
float(self.raw_data.get("indoorHumidity"))
456+
if self.raw_data.get("indoorHumidity") is not None
457+
else None
458+
)
459+
460+
async def update_zone_property(self, property_name: str, value) -> None:
461+
"""Patch zone if writable."""
462+
if self.is_command_only:
463+
return
464+
await self.device.system.api.patch_zone_property(
465+
self.device.id, str(self.id), property_name, value
466+
)
467+
self.raw_data[property_name] = value
417468

418469

419470
class Device:
@@ -463,23 +514,22 @@ def current_firmware_version(self) -> str:
463514
"""Return the current firmware version of the device."""
464515
if "firmware" in self.raw_data:
465516
return self.raw_data["firmware"]["currentFwVersion"]
466-
467-
return self.raw_data["currentFwVersion"]
517+
return self.raw_data.get("currentFwVersion", "N/A")
468518

469519
@property
470520
def desired_firmware_version(self) -> str:
471521
"""Return the desired firmware version of the device."""
472522
if "firmware" in self.raw_data:
473523
return self.raw_data["firmware"]["desiredFwVersion"]
474-
475524
return "?"
476525

477526
async def async_fetch_data(self) -> None:
478527
"""Fetch data from myUplink API."""
479528
self.parameters = await self.system.api.get_parameters(self)
480529
if self.system.api.entry.options.get(CONF_FETCH_FIRMWARE, True):
481530
self.firmware_info = await self.system.api.get_firmware_info(self)
482-
# self.zones = await self.system.api.get_zones(self.id)
531+
if self.system.api.entry.options.get(CONF_ENABLE_SMART_HOME_ZONE, True):
532+
self.zones = await self.system.api.get_zones(self)
483533

484534

485535
class System:
@@ -516,7 +566,7 @@ def security_level(self) -> str:
516566
@property
517567
def has_alaram(self) -> bool:
518568
"""Return if the system has an alaram."""
519-
return self.raw_data["has_alaram"]
569+
return self.raw_data.get("hasAlarm", False)
520570

521571
async def async_fetch_data(self) -> None:
522572
"""Fetch data from myUplink API."""
@@ -663,7 +713,9 @@ async def get_premium_manage(self, system: System) -> bool:
663713

664714
try:
665715
async with self.lock, self.throttle:
666-
resp = await self.auth.request("get", f"systems/{system.id}/subscriptions")
716+
resp = await self.auth.request(
717+
"get", f"systems/{system.id}/subscriptions"
718+
)
667719

668720
# This will raise an exception for 4xx or 5xx errors
669721
resp.raise_for_status()
@@ -674,9 +726,11 @@ async def get_premium_manage(self, system: System) -> bool:
674726
if Subscription(subscription).type == "manage":
675727
return True
676728

677-
except Exception as err:
729+
except ClientResponseError as err:
678730
# We catch the 500 error (and others) here so the integration keeps running
679-
_LOGGER.error("Error fetching subscriptions for system %s: %s", system.id, err)
731+
_LOGGER.error(
732+
"Error fetching subscriptions for system %s: %s", system.id, err
733+
)
680734

681735
return False
682736

@@ -781,17 +835,17 @@ async def get_parameters(self, device: Device) -> list[Parameter]:
781835

782836
return list(unique_parameters.values())
783837

784-
async def get_zones(self, device_id) -> list[Zone]:
838+
async def get_zones(self, device: Device) -> list[Zone]:
785839
"""Return all smart home zones for a device."""
786-
_LOGGER.debug("Fetch zones for device %s", device_id)
840+
_LOGGER.debug("Fetch zones for device %s", device.id)
787841
async with self.lock, self.throttle:
788842
resp = await self.auth.request(
789-
"get", f"devices/{device_id}/smart-home-zones", headers=self.header
843+
"get", f"devices/{device.id}/smart-home-zones", headers=self.header
790844
)
791845
resp.raise_for_status()
792-
return [Zone(zone) for zone in await resp.json()]
846+
return [Zone(zone, device) for zone in await resp.json()]
793847

794-
async def patch_parameter(self, device_id, parameter_id: str, value: str) -> bool:
848+
async def patch_parameter(self, device_id, parameter_id: str, value: Any) -> bool:
795849
"""Update the value of a parameter for a device."""
796850
_LOGGER.debug(
797851
"Patch parameter %s for device %s with value %s",
@@ -809,6 +863,27 @@ async def patch_parameter(self, device_id, parameter_id: str, value: str) -> boo
809863
resp.raise_for_status()
810864
return resp.status == 200
811865

866+
async def patch_zone_property(
867+
self, device_id, zone_id: str, property_name: str, value: str
868+
) -> bool:
869+
"""Update the value of a zone property for a device."""
870+
_LOGGER.debug(
871+
"Patch property %s for zone %s of device %s with value %s",
872+
property_name,
873+
zone_id,
874+
device_id,
875+
value,
876+
)
877+
async with self.lock, self.throttle:
878+
resp = await self.auth.request(
879+
"patch",
880+
f"devices/{device_id}/zones/{zone_id}",
881+
data=json.dumps({property_name: value}),
882+
headers={"Content-Type": "application/json-patch+json"},
883+
)
884+
resp.raise_for_status()
885+
return resp.status == 200
886+
812887
def parse_int_keys(self, dct):
813888
"""Parse object keys into integers."""
814889
rval = {}

0 commit comments

Comments
 (0)