11"""PoolLab API handler."""
22
3+ from dataclasses import dataclass , field , fields
34from datetime import datetime
45from typing import Any
56
7+ API_ENDPOINT = "https://backend.labcom.cloud/graphql"
8+
69from gql import Client , gql
710from gql .transport .aiohttp import AIOHTTPTransport
811
9- API_ENDPOINT = "https://backend.labcom.cloud/graphql"
10-
1112# Measurement ranges according to https://poollab.org/static/manuals/poollab_manual_gb-fr-e-d-i.pdf
1213MEAS_RANGES_BY_SCENARIO = {
1314 # Active oxygene 0-30
3839}
3940
4041
41- class Measurement (object ):
42- """Data class for decoded water measurement."""
42+ @dataclass
43+ class Measurement :
44+ """Data class for PoolLab measurements."""
4345
44- id : int = None
46+ id : int | None = None
4547 scenario : str = ""
4648 parameter : str = ""
4749 parameter_id : str = ""
@@ -54,50 +56,43 @@ class Measurement(object):
5456 ideal_low : str = ""
5557 ideal_high : str = ""
5658 ideal_status : str = ""
57- timestamp : datetime = None
59+ timestamp : datetime | None = None
60+ interpreted_value : float | None = field (init = False , repr = False , default = None )
61+ interpreted_oor : bool = field (init = False , repr = False , default = False )
5862
59- def __init__ (self , data : dict [str , Any ]) -> None :
60- """Init the measurement object."""
61- for key , value in data .items ():
62- if "timestamp" in key :
63- setattr (self , key , datetime .fromtimestamp (value ))
64- else :
65- setattr (self , key , value )
63+ def __post_init__ (self ):
64+ """Post-initialization processing."""
65+ if isinstance (self .timestamp , (int , float )):
66+ self .timestamp = datetime .fromtimestamp (self .timestamp )
6667
67- self .interpreted_value = None
68- self .interpreted_oor = False
6968 if self .value and self .scenario in MEAS_RANGES_BY_SCENARIO :
7069 try :
71- value = float (self .value )
72- range_min = MEAS_RANGES_BY_SCENARIO [self .scenario ][0 ]
73- range_max = MEAS_RANGES_BY_SCENARIO [self .scenario ][1 ]
74- if value < range_min :
75- self .interpreted_value = float (range_min )
76- self .interpreted_oor = True
77- elif value > range_max :
78- self .interpreted_value = float (range_max )
79- self .interpreted_oor = True
80- else :
81- self .interpreted_value = value
82- except : # noqa: E722
70+ val = float (self .value )
71+ min_val , max_val = MEAS_RANGES_BY_SCENARIO [self .scenario ]
72+ self .interpreted_value = max (min (val , max_val ), min_val )
73+ self .interpreted_oor = not (min_val <= val <= max_val )
74+ except ValueError :
8375 pass
8476
8577 @staticmethod
8678 def get_schema (indent : str ) -> str :
87- """Return the schema for the measurement object."""
88- schema = ""
89- for attribute in Measurement .__dict__ :
90- if attribute [:2 ] != "__" :
91- value = getattr (Measurement , attribute )
92- if not callable (value ):
93- schema += indent + str (attribute ) + "\n "
94- return schema
79+ """Return the schema for the Measurement class."""
80+ return "" .join (
81+ f"{ indent } { f .name } \n "
82+ for f in fields (Measurement )
83+ if not f .name .startswith ("interpreted" )
84+ )
9585
86+ def as_dict (self ) -> dict [str , Any ]:
87+ """Return the Measurement as a dictionary."""
88+ return self .__dict__ .copy ()
9689
97- class Account (object ):
98- """Data class for decoded account data."""
9990
100- id : int = None
91+ @dataclass
92+ class Account :
93+ """Data class for PoolLab accounts."""
94+
95+ id : int | None = None
10196 forename : str = ""
10297 surname : str = ""
10398 street : str = ""
@@ -114,103 +109,95 @@ class Account(object):
114109 volume_unit : str = ""
115110 pooltext : str = ""
116111 gps : str = ""
117- Measurements : list [Measurement ] = []
118-
119- def __init__ (self , data : dict [str , Any ]) -> None :
120- """Init the account object."""
121- for key , value in data .items ():
122- if key == "Measurements" :
123- for m in data ["Measurements" ]:
124- self .Measurements .append (Measurement (m ))
125- else :
126- setattr (self , key , value )
112+ Measurements : list [Measurement ] = field (default_factory = list )
113+
114+ def __post_init__ (self ):
115+ """Post-initialization processing."""
116+ self .Measurements = [
117+ m if isinstance (m , Measurement ) else Measurement (** m )
118+ for m in self .Measurements
119+ ]
127120
128121 @property
129122 def full_name (self ) -> str :
130- """Compiled full name of account."""
131- _full_name = ""
132- if self .forename :
133- _full_name += self .forename
134- if self .surname :
135- if _full_name :
136- _full_name += " "
137- _full_name += self .surname
138- return _full_name
123+ """Return the full name of the account holder."""
124+ return f"{ self .forename } { self .surname } " .strip ()
139125
140126 @staticmethod
141- def get_schema (indent : str ) -> str :
142- """Return the schema for the account object ."""
127+ def get_schema (indent : str = "" ) -> str :
128+ """Return the schema for the Account class ."""
143129 schema = ""
144- for attribute in Account .__dict__ :
145- if attribute [:2 ] != "__" :
146- value = getattr (Account , attribute )
147- if not callable (value ):
148- if attribute == "Measurements" :
149- schema += indent + "Measurements {\n "
150- schema += Measurement .get_schema (indent + " " )
151- schema += indent + "}\n "
152- elif attribute == "full_name" :
153- pass
154- else :
155- schema += indent + str (attribute ) + "\n "
130+ for f in fields (Account ):
131+ if f .name == "Measurements" :
132+ schema += f"{ indent } Measurements {{\n "
133+ schema += Measurement .get_schema (indent + " " )
134+ schema += f"{ indent } }}\n "
135+ else :
136+ schema += f"{ indent } { f .name } \n "
156137 return schema
157138
139+ def as_dict (self ) -> dict [str , Any ]:
140+ """Return the Account as a dictionary."""
141+ return self .__dict__ .copy ()
142+
158143
159- class WaterTreatmentProduct (object ):
160- """Data class for decoded water treatment products."""
144+ @dataclass
145+ class WaterTreatmentProduct :
146+ """Data class for PoolLab water treatment products."""
161147
162148 id : int = None
163149 name : str = ""
164150 effect : str = ""
165151 phrase : str = ""
166152
167- def __init__ (self , data : dict [str , Any ]) -> None :
168- """Init the water treatment product object."""
169-
170- for key , value in data .items ():
171- setattr (self , key , value )
153+ def __post_init__ (self ):
154+ """Post-initialization processing."""
155+ pass # Nothing special for now, can keep or drop
172156
173157 @staticmethod
174158 def get_schema (indent : str ) -> str :
175- """Return the schema for the water treatment product object."""
176- schema = ""
177- for attribute in WaterTreatmentProduct .__dict__ :
178- if attribute [:2 ] != "__" :
179- value = getattr (WaterTreatmentProduct , attribute )
180- if not callable (value ):
181- schema += indent + str (attribute ) + "\n "
182- return schema
159+ """Return the schema for the WaterTreatmentProduct class."""
160+ return "" .join (f"{ indent } { f .name } \n " for f in fields (WaterTreatmentProduct ))
161+
162+ def as_dict (self ) -> dict [str , Any ]:
163+ """Return the WaterTreatmentProduct as a dictionary."""
164+ return self .__dict__ .copy ()
183165
184166
167+ @dataclass
185168class CloudAccount :
186- """Master class for PoolLab data ."""
169+ """Data class for PoolLab cloud account ."""
187170
188171 id : int = None
189172 email : str = ""
190- last_change_time : datetime = None
191- last_wtp_change : datetime = None
192- Accounts : list [Account ] = []
193- WaterTreatmentProducts : list [WaterTreatmentProduct ] = []
194-
195- def __init__ (self , data : dict [str , Any ]) -> None :
196- """Init the clound account object."""
197-
198- if data := data .get ("CloudAccount" ):
199- # data = data["CloudAccount"]
200- for key , value in data .items ():
201- if key == "Accounts" :
202- for a in data ["Accounts" ]:
203- self .Accounts .append (Account (a ))
204- elif key == "WaterTreatmentProducts" :
205- for w in data ["WaterTreatmentProducts" ]:
206- self .WaterTreatmentProducts .append (WaterTreatmentProduct (w ))
207- elif "last" in key :
208- setattr (self , key , datetime .fromtimestamp (value ))
209- else :
210- setattr (self , key , value )
211-
212- def get_measurement (self , account_id : int , meas_param : str ):
213- """Get a measurement."""
173+ last_change_time : datetime | None = None
174+ last_wtp_change : datetime | None = None
175+ Accounts : list [Account ] = field (default_factory = list )
176+ WaterTreatmentProducts : list [WaterTreatmentProduct ] = field (default_factory = list )
177+
178+ def __init__ (self , data : dict [str , Any ]):
179+ """Initialize the CloudAccount from a dictionary."""
180+ if cloud_data := data .get ("CloudAccount" ):
181+ self .id = cloud_data .get ("id" )
182+ self .email = cloud_data .get ("email" )
183+ self .last_change_time = (
184+ datetime .fromtimestamp (cloud_data ["last_change_time" ])
185+ if "last_change_time" in cloud_data
186+ else None
187+ )
188+ self .last_wtp_change = (
189+ datetime .fromtimestamp (cloud_data ["last_wtp_change" ])
190+ if "last_wtp_change" in cloud_data
191+ else None
192+ )
193+ self .Accounts = [Account (** a ) for a in cloud_data .get ("Accounts" , [])]
194+ self .WaterTreatmentProducts = [
195+ WaterTreatmentProduct (** w )
196+ for w in cloud_data .get ("WaterTreatmentProducts" , [])
197+ ]
198+
199+ def get_measurement (self , account_id : int , meas_param : str ) -> Measurement :
200+ """Get the latest measurement for a given account and parameter."""
214201 account = next (x for x in self .Accounts if x .id == account_id )
215202 sorted_meas = sorted (
216203 account .Measurements , key = lambda x : x .timestamp , reverse = True
@@ -219,24 +206,32 @@ def get_measurement(self, account_id: int, meas_param: str):
219206
220207 @staticmethod
221208 def get_schema (indent : str ) -> str :
222- """Return the schema for the cloud account object ."""
209+ """Return the schema for the CloudAccount class ."""
223210 schema = ""
224- for attribute in CloudAccount .__dict__ :
225- if attribute [:2 ] != "__" :
226- value = getattr (CloudAccount , attribute )
227- if not callable (value ):
228- if attribute == "Accounts" :
229- schema += indent + "Accounts {\n "
230- schema += Account .get_schema (indent + " " )
231- schema += indent + "}\n "
232- elif attribute == "WaterTreatmentProducts" :
233- schema += indent + "WaterTreatmentProducts {\n "
234- schema += WaterTreatmentProduct .get_schema (indent + " " )
235- schema += indent + "}\n "
236- else :
237- schema += indent + str (attribute ) + "\n "
211+ for attr in [
212+ "id" ,
213+ "email" ,
214+ "last_change_time" ,
215+ "last_wtp_change" ,
216+ "Accounts" ,
217+ "WaterTreatmentProducts" ,
218+ ]:
219+ if attr == "Accounts" :
220+ schema += f"{ indent } Accounts {{\n "
221+ schema += Account .get_schema (indent + " " )
222+ schema += f"{ indent } }}\n "
223+ elif attr == "WaterTreatmentProducts" :
224+ schema += f"{ indent } WaterTreatmentProducts {{\n "
225+ schema += WaterTreatmentProduct .get_schema (indent + " " )
226+ schema += f"{ indent } }}\n "
227+ else :
228+ schema += f"{ indent } { attr } \n "
238229 return schema
239230
231+ def as_dict (self ) -> dict [str , Any ]:
232+ """Return the CloudAccount as a dictionary."""
233+ return self .__dict__ .copy ()
234+
240235
241236class PoolLabApi :
242237 """Public API class for PoolLab."""
0 commit comments