gazpar2haws 0.2.0b1__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gazpar2haws/__main__.py CHANGED
@@ -1,10 +1,12 @@
1
1
  import argparse
2
2
  import asyncio
3
3
  import logging
4
+ import sys
4
5
  import traceback
5
6
 
6
- from gazpar2haws import __version__, config_utils
7
+ from gazpar2haws import __version__
7
8
  from gazpar2haws.bridge import Bridge
9
+ from gazpar2haws.configuration import Configuration
8
10
 
9
11
  Logger = logging.getLogger(__name__)
10
12
 
@@ -16,9 +18,7 @@ async def main():
16
18
  prog="gazpar2haws",
17
19
  description="Gateway that reads data history from the GrDF (French gas provider) meter and send it to Home Assistant using WebSocket interface.",
18
20
  )
19
- parser.add_argument(
20
- "-v", "--version", action="version", version="Gazpar2HAWS version"
21
- )
21
+ parser.add_argument("-v", "--version", action="version", version="Gazpar2HAWS version")
22
22
  parser.add_argument(
23
23
  "-c",
24
24
  "--config",
@@ -38,17 +38,16 @@ async def main():
38
38
 
39
39
  try:
40
40
  # Load configuration files
41
- config = config_utils.ConfigLoader(args.config, args.secrets)
42
- config.load_secrets()
43
- config.load_config()
41
+ config = Configuration.load(args.config, args.secrets)
44
42
 
45
43
  print(f"Gazpar2HAWS version: {__version__}")
44
+ print(f"Running on Python version: {sys.version}")
46
45
 
47
46
  # Set up logging
48
- logging_file = config.get("logging.file")
49
- logging_console = bool(config.get("logging.console"))
50
- logging_level = config.get("logging.level")
51
- logging_format = config.get("logging.format")
47
+ logging_file = config.logging.file
48
+ logging_console = config.logging.console
49
+ logging_level = config.logging.level
50
+ logging_format = config.logging.format
52
51
 
53
52
  # Convert logging level to integer
54
53
  if logging_level.upper() == "DEBUG":
@@ -70,14 +69,13 @@ async def main():
70
69
  # Add a console handler manually
71
70
  console_handler = logging.StreamHandler()
72
71
  console_handler.setLevel(level) # Set logging level for the console
73
- console_handler.setFormatter(
74
- logging.Formatter(logging_format)
75
- ) # Customize console format
72
+ console_handler.setFormatter(logging.Formatter(logging_format)) # Customize console format
76
73
 
77
74
  # Get the root logger and add the console handler
78
75
  logging.getLogger().addHandler(console_handler)
79
76
 
80
77
  Logger.info(f"Starting Gazpar2HAWS version {__version__}")
78
+ Logger.info(f"Running on Python version: {sys.version}")
81
79
 
82
80
  # Log configuration
83
81
  Logger.info(f"Configuration:\n{config.dumps()}")
@@ -91,12 +89,10 @@ async def main():
91
89
  return 0
92
90
 
93
91
  except Exception: # pylint: disable=broad-except
94
- errorMessage = (
95
- f"An error occured while running Gazpar2HAWS: {traceback.format_exc()}"
96
- )
92
+ errorMessage = f"An error occured while running Gazpar2HAWS: {traceback.format_exc()}"
97
93
  Logger.error(errorMessage)
98
94
  print(errorMessage)
99
- return 1
95
+ raise
100
96
 
101
97
 
102
98
  # ----------------------------------
gazpar2haws/bridge.py CHANGED
@@ -2,7 +2,7 @@ import asyncio
2
2
  import logging
3
3
  import signal
4
4
 
5
- from gazpar2haws import config_utils
5
+ from gazpar2haws.configuration import Configuration
6
6
  from gazpar2haws.gazpar import Gazpar
7
7
  from gazpar2haws.haws import HomeAssistantWS
8
8
 
@@ -13,24 +13,31 @@ Logger = logging.getLogger(__name__)
13
13
  class Bridge:
14
14
 
15
15
  # ----------------------------------
16
- def __init__(self, config: config_utils.ConfigLoader):
16
+ def __init__(self, config: Configuration):
17
17
 
18
18
  # GrDF scan interval (in seconds)
19
- self._grdf_scan_interval = int(config.get("grdf.scan_interval"))
19
+ self._grdf_scan_interval = config.grdf.scan_interval
20
20
 
21
- # Home Assistant configuration
22
- ha_host = config.get("homeassistant.host")
23
- ha_port = config.get("homeassistant.port")
24
- ha_endpoint = config.get("homeassistant.endpoint")
25
- ha_token = config.get("homeassistant.token")
21
+ # Home Assistant configuration: host
22
+ ha_host = config.homeassistant.host
23
+
24
+ # Home Assistant configuration: port
25
+ ha_port = config.homeassistant.port
26
+
27
+ # Home Assistant configuration: endpoint
28
+ ha_endpoint = config.homeassistant.endpoint
29
+
30
+ # Home Assistant configuration: token
31
+ ha_token = config.homeassistant.token.get_secret_value()
26
32
 
27
33
  # Initialize Home Assistant
28
34
  self._homeassistant = HomeAssistantWS(ha_host, ha_port, ha_endpoint, ha_token)
29
35
 
30
36
  # Initialize Gazpar
31
37
  self._gazpar = []
32
- for grdf_device_config in config.get("grdf.devices"):
33
- self._gazpar.append(Gazpar(grdf_device_config, self._homeassistant))
38
+
39
+ for grdf_device_config in config.grdf.devices:
40
+ self._gazpar.append(Gazpar(grdf_device_config, config.pricing, self._homeassistant))
34
41
 
35
42
  # Set up signal handler
36
43
  signal.signal(signal.SIGINT, self.handle_signal)
@@ -64,9 +71,7 @@ class Bridge:
64
71
  for gazpar in self._gazpar:
65
72
  Logger.info(f"Publishing data for device '{gazpar.name()}'...")
66
73
  await gazpar.publish()
67
- Logger.info(
68
- f"Device '{gazpar.name()}' data published to Home Assistant WS."
69
- )
74
+ Logger.info(f"Device '{gazpar.name()}' data published to Home Assistant WS.")
70
75
 
71
76
  Logger.info("Gazpar data published to Home Assistant WS.")
72
77
 
@@ -74,9 +79,7 @@ class Bridge:
74
79
  await self._homeassistant.disconnect()
75
80
 
76
81
  # Wait before next scan
77
- Logger.info(
78
- f"Waiting {self._grdf_scan_interval} minutes before next scan..."
79
- )
82
+ Logger.info(f"Waiting {self._grdf_scan_interval} minutes before next scan...")
80
83
 
81
84
  # Check if the scan interval is 0 and leave the loop.
82
85
  if self._grdf_scan_interval == 0:
@@ -1,18 +1,21 @@
1
1
  import os
2
+ from typing import Any
2
3
 
3
4
  import yaml
4
5
 
5
6
 
6
7
  class ConfigLoader:
7
- def __init__(self, config_file="config.yaml", secrets_file="secrets.yaml"):
8
+ def __init__(self, config_file: str, secrets_file: str):
8
9
  self.config_file = config_file
9
10
  self.secrets_file = secrets_file
10
- self.config = {}
11
- self.secrets = {}
11
+ self.config = dict[str, Any]()
12
+ self.secrets = dict[str, Any]()
12
13
  self.raw_config = None
13
14
 
14
15
  def load_secrets(self):
15
16
  """Load the secrets file."""
17
+ if self.secrets_file is None:
18
+ return
16
19
  if os.path.exists(self.secrets_file):
17
20
  with open(self.secrets_file, "r", encoding="utf-8") as file:
18
21
  self.secrets = yaml.safe_load(file)
@@ -26,9 +29,7 @@ class ConfigLoader:
26
29
  self.raw_config = yaml.safe_load(file)
27
30
  self.config = self._resolve_secrets(self.raw_config)
28
31
  else:
29
- raise FileNotFoundError(
30
- f"Configuration file '{self.config_file}' not found."
31
- )
32
+ raise FileNotFoundError(f"Configuration file '{self.config_file}' not found.")
32
33
 
33
34
  def _resolve_secrets(self, data):
34
35
  """Recursively resolve `!secret` keys in the configuration."""
@@ -54,5 +55,8 @@ class ConfigLoader:
54
55
  except (KeyError, TypeError):
55
56
  return default
56
57
 
58
+ def dict(self) -> dict:
59
+ return self.config
60
+
57
61
  def dumps(self) -> str:
58
62
  return yaml.dump(self.raw_config)
@@ -0,0 +1,28 @@
1
+ from typing import Optional
2
+
3
+ import yaml
4
+ from pydantic import BaseModel
5
+
6
+ from gazpar2haws import config_utils
7
+ from gazpar2haws.model import Grdf, HomeAssistant, Logging, Pricing
8
+
9
+
10
+ class Configuration(BaseModel):
11
+
12
+ logging: Logging
13
+ grdf: Grdf
14
+ homeassistant: HomeAssistant
15
+ pricing: Optional[Pricing] = None
16
+
17
+ @classmethod
18
+ def load(cls, config_file: str, secrets_file: str):
19
+
20
+ # Load configuration
21
+ config = config_utils.ConfigLoader(config_file, secrets_file)
22
+ config.load_secrets()
23
+ config.load_config()
24
+
25
+ return cls(**config.dict())
26
+
27
+ def dumps(self) -> str:
28
+ return yaml.dump(self.model_dump(mode="json"), allow_unicode=True)
@@ -0,0 +1,259 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as dt
4
+ from datetime import timedelta
5
+ from typing import Optional, overload
6
+
7
+ import numpy as np
8
+ from pydantic import BaseModel, ConfigDict, model_validator
9
+
10
+
11
+ class DateArray(BaseModel): # pylint: disable=too-few-public-methods
12
+ model_config = ConfigDict(arbitrary_types_allowed=True)
13
+
14
+ name: Optional[str] = None
15
+ start_date: dt.date
16
+ end_date: dt.date
17
+ array: Optional[np.ndarray] = None
18
+ initial_value: Optional[float] = None
19
+
20
+ @model_validator(mode="after")
21
+ def set_array(self):
22
+ if self.array is None:
23
+ if self.initial_value is not None:
24
+ self.array = np.full((self.end_date - self.start_date).days + 1, self.initial_value)
25
+ else:
26
+ self.array = np.zeros((self.end_date - self.start_date).days + 1)
27
+ return self
28
+
29
+ # ----------------------------------
30
+ def get(self, date: dt.date) -> float:
31
+
32
+ if self.array is None:
33
+ raise ValueError("Array is not initialized")
34
+
35
+ return self.array[(date - self.start_date).days]
36
+
37
+ # ----------------------------------
38
+ def cumsum(self) -> DateArray:
39
+
40
+ if self.array is None:
41
+ raise ValueError("Array is not initialized")
42
+
43
+ result = DateArray(name=f"cumsum_{self.name}", start_date=self.start_date, end_date=self.end_date)
44
+ result.array = np.cumsum(self.array)
45
+ return result
46
+
47
+ # ----------------------------------
48
+ def is_aligned_with(self, other: DateArray) -> bool:
49
+
50
+ return (
51
+ self.start_date == other.start_date and self.end_date == other.end_date and len(self) == len(other)
52
+ ) # pylint: disable=protected-access
53
+
54
+ # ----------------------------------
55
+ @overload
56
+ def __getitem__(self, index: int) -> float: ...
57
+
58
+ @overload
59
+ def __getitem__(self, date: dt.date) -> float: ...
60
+
61
+ @overload
62
+ def __getitem__(self, date_slice: slice) -> DateArray: ...
63
+
64
+ def __getitem__(self, key):
65
+ if self.array is None:
66
+ raise ValueError("Array is not initialized")
67
+ if isinstance(key, int):
68
+ return self.array[key]
69
+ if isinstance(key, dt.date):
70
+ return self.get(key)
71
+ if isinstance(key, slice):
72
+ start_date: dt.date = key.start # type: ignore
73
+ end_date: dt.date = key.stop # type: ignore
74
+ start_index: int = (start_date - self.start_date).days
75
+ end_index: int = (end_date - self.start_date).days
76
+ if start_index < 0 or end_index > len(self.array):
77
+ raise ValueError(
78
+ f"Date slice [{start_date}:{end_date}] is out of range [{self.start_date}:{self.end_date}]"
79
+ )
80
+ return DateArray(
81
+ name=self.name,
82
+ start_date=start_date,
83
+ end_date=end_date + timedelta(-1),
84
+ array=self.array[start_index:end_index],
85
+ )
86
+ raise TypeError("Key must be a date or a slice of dates")
87
+
88
+ # ----------------------------------
89
+ @overload
90
+ def __setitem__(self, index: int, value: float): ...
91
+
92
+ @overload
93
+ def __setitem__(self, date: dt.date, value: float): ...
94
+
95
+ @overload
96
+ def __setitem__(self, date_slice: slice, value: float): ...
97
+
98
+ @overload
99
+ def __setitem__(self, date_slice: slice, value: DateArray): ...
100
+
101
+ def __setitem__(self, key, value):
102
+ if self.array is None:
103
+ raise ValueError("Array is not initialized")
104
+ if isinstance(key, int):
105
+ self.array[key] = value
106
+ elif isinstance(key, dt.date):
107
+ self.array[(key - self.start_date).days] = value
108
+ elif isinstance(key, slice):
109
+ start_date: dt.date = key.start # type: ignore
110
+ end_date: dt.date = key.stop # type: ignore
111
+ start_index: int = (start_date - self.start_date).days
112
+ end_index: int = (end_date - self.start_date).days
113
+ if start_index < 0 or end_index > len(self.array):
114
+ raise ValueError(
115
+ f"Date slice [{start_date}:{end_date}] is out of range [{self.start_date}:{self.end_date}]"
116
+ )
117
+ if isinstance(value, float):
118
+ self.array[start_index:end_index] = value
119
+ elif isinstance(value, DateArray):
120
+ self.array[start_index:end_index] = value.array
121
+ else:
122
+ raise TypeError("Value must be a float or a DateArray")
123
+ else:
124
+ raise TypeError("Key must be a date or a slice of dates")
125
+
126
+ # ----------------------------------
127
+ def __len__(self) -> int:
128
+
129
+ if self.array is None:
130
+ raise ValueError("Array is not initialized")
131
+
132
+ return len(self.array)
133
+
134
+ # ----------------------------------
135
+ def __iter__(self):
136
+ self._index = 0 # pylint: disable=attribute-defined-outside-init
137
+ return self
138
+
139
+ # ----------------------------------
140
+ def __next__(self):
141
+ if self._index < len(self.array):
142
+ current_date = self.start_date + dt.timedelta(days=self._index)
143
+ result = (current_date, self.array[self._index])
144
+ self._index += 1
145
+ return result
146
+ raise StopIteration
147
+
148
+ # ----------------------------------
149
+ @overload
150
+ def __add__(self, other: DateArray) -> DateArray: ...
151
+
152
+ @overload
153
+ def __add__(self, other: float) -> DateArray: ...
154
+
155
+ def __add__(self, other) -> DateArray:
156
+
157
+ if self.array is None:
158
+ raise ValueError("Array is not initialized")
159
+
160
+ if isinstance(other, (int, float)):
161
+ result = DateArray(name=self.name, start_date=self.start_date, end_date=self.end_date)
162
+ result.array = self.array + other
163
+ return result
164
+ if isinstance(other, DateArray):
165
+ if other.array is None:
166
+ raise ValueError("Array is not initialized")
167
+ if not self.is_aligned_with(other):
168
+ raise ValueError(f"Date arrays {self} and {other} are not aligned")
169
+ result = DateArray(name=self.name, start_date=self.start_date, end_date=self.end_date)
170
+ result.array = self.array + other.array # pylint: disable=protected-access
171
+ return result
172
+
173
+ raise TypeError("Other must be a date array or a number")
174
+
175
+ # ----------------------------------
176
+ @overload
177
+ def __sub__(self, other: DateArray) -> DateArray: ...
178
+
179
+ @overload
180
+ def __sub__(self, other: float) -> DateArray: ...
181
+
182
+ def __sub__(self, other) -> DateArray:
183
+
184
+ if self.array is None:
185
+ raise ValueError("Array is not initialized")
186
+
187
+ if isinstance(other, (int, float)):
188
+ result = DateArray(name=self.name, start_date=self.start_date, end_date=self.end_date)
189
+ result.array = self.array - other
190
+ return result
191
+ if isinstance(other, DateArray):
192
+ if other.array is None:
193
+ raise ValueError("Array is not initialized")
194
+ if not self.is_aligned_with(other):
195
+ raise ValueError(f"Date arrays {self} and {other} are not aligned")
196
+ result = DateArray(name=self.name, start_date=self.start_date, end_date=self.end_date)
197
+ result.array = self.array - other.array # pylint: disable=protected-access
198
+ return result
199
+
200
+ raise TypeError("Other must be a date array or a number")
201
+
202
+ # ----------------------------------
203
+ @overload
204
+ def __mul__(self, other: DateArray) -> DateArray: ...
205
+
206
+ @overload
207
+ def __mul__(self, other: float) -> DateArray: ...
208
+
209
+ def __mul__(self, other) -> DateArray:
210
+
211
+ if self.array is None:
212
+ raise ValueError("Array is not initialized")
213
+
214
+ if isinstance(other, (int, float)):
215
+ result = DateArray(name=self.name, start_date=self.start_date, end_date=self.end_date)
216
+ result.array = self.array * other
217
+ return result
218
+ if isinstance(other, DateArray):
219
+ if other.array is None:
220
+ raise ValueError("Array is not initialized")
221
+ if not self.is_aligned_with(other):
222
+ raise ValueError(f"Date arrays {self} and {other} are not aligned")
223
+ result = DateArray(name=self.name, start_date=self.start_date, end_date=self.end_date)
224
+ result.array = self.array * other.array # pylint: disable=protected-access
225
+ return result
226
+
227
+ raise TypeError("Other must be a date array or a number")
228
+
229
+ # ----------------------------------
230
+ @overload
231
+ def __truediv__(self, other: DateArray) -> DateArray: ...
232
+
233
+ @overload
234
+ def __truediv__(self, other: float) -> DateArray: ...
235
+
236
+ def __truediv__(self, other) -> DateArray:
237
+
238
+ if self.array is None:
239
+ raise ValueError("Array is not initialized")
240
+
241
+ if isinstance(other, (int, float)):
242
+ result = DateArray(name=self.name, start_date=self.start_date, end_date=self.end_date)
243
+ result.array = self.array / other
244
+ return result
245
+ if isinstance(other, DateArray):
246
+ if other.array is None:
247
+ raise ValueError("Array is not initialized")
248
+ if not self.is_aligned_with(other):
249
+ raise ValueError(f"Date arrays {self} and {other} are not aligned")
250
+ result = DateArray(name=self.name, start_date=self.start_date, end_date=self.end_date)
251
+ result.array = self.array / other.array # pylint: disable=protected-access
252
+ return result
253
+
254
+ raise TypeError("Other must be a date array or a number")
255
+
256
+ # ----------------------------------
257
+ def __str__(self) -> str:
258
+
259
+ return f"DateArray(name={self.name}, start_date={self.start_date}, end_date={self.end_date}, array={self.array}, slots={(self.end_date - self.start_date).days + 1}, length={len(self)})"