Skip to content

Commit f044a9c

Browse files
lackasCFenner
andauthored
refactor: move accessor from service to device (#774)
* refactor: move accessor from service to device Splits the device-identity concern (installation/gateway/device IDs) from the HTTP/cache concern (OAuth, request building, caching). Service instances no longer hold an accessor — it lives on Device and PyViCareDeviceConfig and is passed at each call site. Prep work for a future gateway-scoped service that can serve multiple devices from a single bulk fetch. Picked up from CFenner's #628 on current master since the 8-month drift (notably #764's PACKAGE_NOT_PAID_FOR exception handling in ViCareCachedService) made a fresh implementation cleaner than the rebase. Same design and API shape — public surface (device.getProperty/setProperty, DeviceConfig.as*) is unchanged. * fix(tests): cleanup mypy + pylint warnings in refactored test helpers - ViCareDeviceAccessor takes int as first arg per its annotation; pass 0 instead of "[id]" in the typed setUp of test_PyViCareDeviceConfig - Rename intentionally-unused mock params to _-prefix (accessor, requested_roles) so pylint doesn't flag unused-argument --------- Co-authored-by: Christopher Fenner <9592452+CFenner@users.noreply.github.com>
1 parent 50889f7 commit f044a9c

54 files changed

Lines changed: 331 additions & 204 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

PyViCare/PyViCare.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ def initWithExternalOAuth(self, oauth_manager: AbstractViCareOAuthManager) -> No
3232
def initWithBrowserOAuth(self, client_id: str, token_file: str) -> None:
3333
self.initWithExternalOAuth(ViCareBrowserOAuthManager(client_id, token_file))
3434

35-
def __buildService(self, accessor, roles):
35+
def __buildService(self, roles):
3636
if self.cacheDuration > 0:
37-
return ViCareCachedService(self.oauth_manager, accessor, roles, self.cacheDuration)
38-
return ViCareService(self.oauth_manager, accessor, roles)
37+
return ViCareCachedService(self.oauth_manager, roles, self.cacheDuration)
38+
return ViCareService(self.oauth_manager, roles)
3939

4040
def __loadInstallations(self):
4141
installations = self.oauth_manager.get(
@@ -61,11 +61,11 @@ def __extract_all_devices(self):
6161
for device in gateway.devices:
6262
accessor = ViCareDeviceAccessor(
6363
installation.id, gateway.serial, device.id)
64-
service = self.__buildService(accessor, device.roles)
64+
service = self.__buildService(device.roles)
6565

6666
logger.info("Device found: %s (type=%s)", device.modelId, device.deviceType)
6767

68-
yield PyViCareDeviceConfig(service, device.id, device.modelId, device.status, device.deviceType, device.roles)
68+
yield PyViCareDeviceConfig(accessor, service, device.modelId, device.status, device.deviceType, device.roles)
6969

7070

7171
class DictWrap(object):

PyViCare/PyViCareCachedService.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
import threading
3-
from typing import Any, List
3+
from datetime import datetime
4+
from typing import Any, List, Optional
45

56
from PyViCare.PyViCareAbstractOAuthManager import AbstractViCareOAuthManager
67
from PyViCare.PyViCareService import (ViCareDeviceAccessor, ViCareService,
@@ -18,24 +19,24 @@
1819

1920
class ViCareCachedService(ViCareService):
2021

21-
def __init__(self, oauth_manager: AbstractViCareOAuthManager, accessor: ViCareDeviceAccessor, roles: List[str], cacheDuration: int) -> None:
22-
ViCareService.__init__(self, oauth_manager, accessor, roles)
22+
def __init__(self, oauth_manager: AbstractViCareOAuthManager, roles: List[str], cacheDuration: int) -> None:
23+
ViCareService.__init__(self, oauth_manager, roles)
2324
self.__cacheDuration = cacheDuration
24-
self.__cache = None
25-
self.__cacheTime = None
25+
self.__cache: Optional[dict] = None
26+
self.__cacheTime: Optional[datetime] = None
2627
self.__lock = threading.Lock()
2728

28-
def getProperty(self, property_name: str) -> Any:
29-
data = self.__get_or_update_cache()
29+
def getProperty(self, accessor: ViCareDeviceAccessor, property_name: str) -> Any:
30+
data = self.__get_or_update_cache(accessor)
3031
entities = data["data"]
3132
return readFeature(entities, property_name)
3233

33-
def setProperty(self, property_name, action, data):
34-
response = super().setProperty(property_name, action, data)
34+
def setProperty(self, accessor: ViCareDeviceAccessor, property_name: str, action: str, data: Any) -> Any:
35+
response = super().setProperty(accessor, property_name, action, data)
3536
self.clear_cache()
3637
return response
3738

38-
def __get_or_update_cache(self):
39+
def __get_or_update_cache(self, accessor: ViCareDeviceAccessor):
3940
with self.__lock:
4041
if self.is_cache_invalid():
4142
# we always set the cache time before we fetch the data
@@ -45,7 +46,7 @@ def __get_or_update_cache(self):
4546
self.__cacheTime = ViCareTimer().now()
4647

4748
try:
48-
data = self.fetch_all_features()
49+
data = self.fetch_all_features(accessor)
4950
except PyViCareNotPaidForError as e:
5051
logger.error("Viessmann API denied access (PACKAGE_NOT_PAID_FOR). Features unavailable: %s", e)
5152
if self.__cache is not None:

PyViCare/PyViCareDevice.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import Any
22

3-
from PyViCare.PyViCareService import ViCareService
3+
from PyViCare.PyViCareService import ViCareDeviceAccessor, ViCareService
44
from PyViCare.PyViCareUtils import PyViCareNotSupportedFeatureError, handleAPICommandErrors, handleNotSupported
55

66

@@ -11,14 +11,15 @@ class Device:
1111
Note that currently, a new token is generated for each run.
1212
"""
1313

14-
def __init__(self, service: ViCareService) -> None:
14+
def __init__(self, accessor: ViCareDeviceAccessor, service: ViCareService) -> None:
15+
self.accessor = accessor
1516
self.service = service
1617

1718
def getProperty(self, property_name: str) -> Any:
18-
return self.service.getProperty(property_name)
19+
return self.service.getProperty(self.accessor, property_name)
1920

2021
def setProperty(self, property_name: str, action: str, data: Any) -> Any:
21-
return self.service.setProperty(property_name, action, data)
22+
return self.service.setProperty(self.accessor, property_name, action, data)
2223

2324
@handleNotSupported
2425
def getSerial(self):

PyViCare/PyViCareDeviceConfig.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from PyViCare.PyViCareRepeater import Repeater
1717
from PyViCare.PyViCareElectricalEnergySystem import ElectricalEnergySystem
1818
from PyViCare.PyViCareGateway import Gateway
19+
from PyViCare.PyViCareService import ViCareDeviceAccessor, ViCareService
1920
from PyViCare.PyViCareUtils import PyViCareNotPaidForError
2021
from PyViCare.PyViCareVentilationDevice import VentilationDevice
2122

@@ -25,64 +26,65 @@
2526

2627
class PyViCareDeviceConfig:
2728
# pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-instance-attributes
28-
def __init__(self, service, device_id, device_model, status, device_type=None, roles=None):
29+
def __init__(self, accessor: ViCareDeviceAccessor, service: ViCareService, device_model, status, device_type=None, roles=None):
30+
self.accessor = accessor
2931
self.service = service
30-
self.device_id = device_id
32+
self.device_id = accessor.device_id
3133
self.device_model = device_model
3234
self.status = status
3335
self.device_type = device_type
3436
self.roles = roles if roles is not None else []
3537

3638
def asGeneric(self):
37-
return HeatingDevice(self.service)
39+
return HeatingDevice(self.accessor, self.service)
3840

3941
def asGazBoiler(self):
40-
return GazBoiler(self.service)
42+
return GazBoiler(self.accessor, self.service)
4143

4244
def asFuelCell(self):
43-
return FuelCell(self.service)
45+
return FuelCell(self.accessor, self.service)
4446

4547
def asHeatPump(self):
46-
return HeatPump(self.service)
48+
return HeatPump(self.accessor, self.service)
4749

4850
def asOilBoiler(self):
49-
return OilBoiler(self.service)
51+
return OilBoiler(self.accessor, self.service)
5052

5153
def asPelletsBoiler(self):
52-
return PelletsBoiler(self.service)
54+
return PelletsBoiler(self.accessor, self.service)
5355

5456
def asHybridDevice(self):
55-
return Hybrid(self.service)
57+
return Hybrid(self.accessor, self.service)
5658

5759
def asRadiatorActuator(self):
58-
return RadiatorActuator(self.service)
60+
return RadiatorActuator(self.accessor, self.service)
5961

6062
def asFloorHeating(self):
61-
return FloorHeating(self.service)
63+
return FloorHeating(self.accessor, self.service)
6264

6365
def asFloorHeatingChannel(self):
64-
return FloorHeatingChannel(self.service)
66+
return FloorHeatingChannel(self.accessor, self.service)
6567

6668
def asRoomSensor(self):
67-
return RoomSensor(self.service)
69+
return RoomSensor(self.accessor, self.service)
6870

6971
def asRoomControl(self):
70-
return RoomControl(self.service)
72+
return RoomControl(self.accessor, self.service)
7173

7274
def asRepeater(self):
73-
return Repeater(self.service)
75+
return Repeater(self.accessor, self.service)
7476

7577
def asElectricalEnergySystem(self):
76-
return ElectricalEnergySystem(self.service)
78+
return ElectricalEnergySystem(self.accessor, self.service)
7779

7880
def asGateway(self):
79-
return Gateway(self.service)
81+
return Gateway(self.accessor, self.service)
8082

8183
def asVentilation(self):
82-
return VentilationDevice(self.service)
84+
return VentilationDevice(self.accessor, self.service)
8385

8486
def getConfig(self):
85-
return self.service.accessor
87+
return self.accessor
8688

8789
def getId(self):
8890
return self.device_id
@@ -144,7 +146,7 @@ def asAutoDetectDevice(self):
144146
def _isHybridByFeatures(self):
145147
"""Check API features to detect hybrid devices (both burners and compressors)."""
146148
try:
147-
features = self.service.fetch_all_features()
149+
features = self.service.fetch_all_features(self.accessor)
148150
feature_names = [f["feature"] for f in features.get("data", [])]
149151
has_burners = any(f.startswith("heating.burners") for f in feature_names)
150152
has_compressors = any(f.startswith("heating.compressors") for f in feature_names)
@@ -162,7 +164,7 @@ def _isHybridByFeatures(self):
162164
return False
163165

164166
def get_raw_json(self):
165-
return self.service.fetch_all_features()
167+
return self.service.fetch_all_features(self.accessor)
166168

167169
def dump_secure(self, flat=False):
168170
device_info = {

PyViCare/PyViCareFuelCell.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def getBurner(self, burner):
1717

1818
@handleNotSupported
1919
def getAvailableBurners(self):
20-
return get_available_burners(self.service)
20+
return get_available_burners(self)
2121

2222
@handleNotSupported
2323
def getReturnTemperature(self):

PyViCare/PyViCareGazBoiler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def getBurner(self, burner):
1717

1818
@handleNotSupported
1919
def getAvailableBurners(self):
20-
return get_available_burners(self.service)
20+
return get_available_burners(self)
2121

2222
@handleNotSupported
2323
def getGasConsumptionHeatingUnit(self):

PyViCare/PyViCareHeatingDevice.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ def all_set(_list: List[Any]) -> bool:
1717
return all(v is not None for v in _list)
1818

1919

20-
def get_available_burners(service):
20+
def get_available_burners(device):
2121
# workaround starting from 25.01.2022
2222
# see: https://github.com/somm15/PyViCare/issues/243
2323
available_burners = []
2424
for burner in ['0', '1', '2', '3', '4', '5']:
2525
with suppress(PyViCareNotSupportedFeatureError):
26-
if service.getProperty(f"heating.burners.{burner}") is not None:
26+
if device.getProperty(f"heating.burners.{burner}") is not None:
2727
available_burners.append(burner)
2828

2929
return available_burners
@@ -701,8 +701,8 @@ def getHeatingSchedule(self):
701701

702702
@handleAPICommandErrors
703703
def setHeatingSchedule(self, schedule: dict) -> None:
704-
self.service.setProperty(f"heating.circuits.{self.circuit}.heating.schedule",
705-
"setSchedule", {'newSchedule': schedule})
704+
self.device.setProperty(f"heating.circuits.{self.circuit}.heating.schedule",
705+
"setSchedule", {'newSchedule': schedule})
706706

707707
@handleNotSupported
708708
def getHeatingScheduleModes(self) -> list: # type: ignore[type-arg]

PyViCare/PyViCareOilBoiler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def getBurner(self, burner):
1717

1818
@handleNotSupported
1919
def getAvailableBurners(self):
20-
return get_available_burners(self.service)
20+
return get_available_burners(self)
2121

2222
@handleNotSupported
2323
def getBoilerTemperature(self):

PyViCare/PyViCarePelletsBoiler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def getBurner(self, burner) -> PelletsBurner:
1616

1717
@handleNotSupported
1818
def getAvailableBurners(self):
19-
return get_available_burners(self.service)
19+
return get_available_burners(self)
2020

2121
@handleNotSupported
2222
def getBoilerTemperature(self):

0 commit comments

Comments
 (0)