gazpar2haws 0.2.0b1__py3-none-any.whl → 0.3.0b15__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 +11 -18
- gazpar2haws/bridge.py +19 -16
- gazpar2haws/config_utils.py +10 -6
- gazpar2haws/configuration.py +28 -0
- gazpar2haws/date_array.py +236 -0
- gazpar2haws/gazpar.py +216 -105
- gazpar2haws/haws.py +10 -28
- gazpar2haws/model.py +234 -0
- gazpar2haws/pricer.py +571 -0
- gazpar2haws-0.3.0b15.dist-info/METADATA +541 -0
- gazpar2haws-0.3.0b15.dist-info/RECORD +15 -0
- gazpar2haws-0.2.0b1.dist-info/METADATA +0 -278
- gazpar2haws-0.2.0b1.dist-info/RECORD +0 -11
- {gazpar2haws-0.2.0b1.dist-info → gazpar2haws-0.3.0b15.dist-info}/LICENSE +0 -0
- {gazpar2haws-0.2.0b1.dist-info → gazpar2haws-0.3.0b15.dist-info}/WHEEL +0 -0
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__
|
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 =
|
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.
|
49
|
-
logging_console =
|
50
|
-
logging_level = config.
|
51
|
-
logging_format = config.
|
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
|
-
|
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
|
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:
|
16
|
+
def __init__(self, config: Configuration):
|
17
17
|
|
18
18
|
# GrDF scan interval (in seconds)
|
19
|
-
self._grdf_scan_interval =
|
19
|
+
self._grdf_scan_interval = config.grdf.scan_interval
|
20
20
|
|
21
|
-
# Home Assistant configuration
|
22
|
-
ha_host = config.
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
33
|
-
|
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:
|
gazpar2haws/config_utils.py
CHANGED
@@ -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
|
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,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})"
|