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